From d5285f3a9a2720ca711ee073ec237ba6ff7619ef Mon Sep 17 00:00:00 2001 From: Donovan Solms Date: Fri, 14 Jul 2023 13:23:36 +0200 Subject: [PATCH 01/47] fix(workflow): Add CARGO_NET_GIT_FETCH_WITH_CLI to environment --- .github/workflows/code_coverage.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index ba04a708..81008ffd 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -8,6 +8,10 @@ on: branches: - main +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: true + jobs: code-coverage: name: Code coverage From b8ea4ed09e52577a525c42072a53714d18388ab9 Mon Sep 17 00:00:00 2001 From: Donovan Solms Date: Thu, 3 Aug 2023 16:10:11 +0200 Subject: [PATCH 02/47] feat(vxastro-lite) Implement vxASTRO lite * Add ability for generator controller to create immediate proposals * Add main vxASTRO lite contract * Add Generator Controller contract for vxASTRO lite * Update contracts/voting_escrow_lite/src/execute.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * Move check_controller_supports_channel to package for reuse * Rework update_networks, tune_pools and group_pools_by_network * Set generator-controller-lite to v1.0.0 * Set vxASTRO lite to v1.0.0 * Update terra to use wasm instead * Update tune_pools attributes * Restructure attributes in tune_pools * Enforce emissions proposals to have messages * Update ExecuteEmissionsProposal to confirm to updated Assembly * Make Assembly address required on instantiate * Make Assembly required in generator controller * Rework vote cooldown logic * Add automatic kicking of unlocked vxASTRO holder * Update packages/astroport-tests-lite/src/lib.rs Co-authored-by: Sergei Shadoy * feat(vxastro_lite): Enable interchain voting and governance * fix(vxastro_lite): Finalise Outpost unlock on confirmation * Update contracts/voting_escrow_lite/src/execute.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * Update contracts/generator_controller_lite/src/utils.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * fix(assembly): Change Addr to String * fix(generator_controller_lite): Remove use of Addr in favour of String * Update contracts/generator_controller_lite/src/contract.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * fix(generator_controller_lite): Reject removing of native network * fix(generator_controller_lite): Implement custom address generator --------- Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> Co-authored-by: Sergei Shadoy --- Cargo.lock | 272 +- contracts/assembly/Cargo.toml | 3 +- contracts/assembly/src/contract.rs | 242 +- contracts/assembly/src/error.rs | 9 + contracts/assembly/src/migration.rs | 24 +- contracts/assembly/tests/integration.rs | 796 ++++-- .../assembly/tests/integration.vxastro-full | 2517 +++++++++++++++++ .../generator_controller_lite/.cargo/config | 6 + .../generator_controller_lite/Cargo.toml | 48 + contracts/generator_controller_lite/README.md | 310 ++ .../generator_controller_lite/src/bps.rs | 89 + .../generator_controller_lite/src/contract.rs | 937 ++++++ .../generator_controller_lite/src/error.rs | 69 + .../generator_controller_lite/src/lib.rs | 10 + .../generator_controller_lite/src/state.rs | 67 + .../generator_controller_lite/src/utils.rs | 295 ++ .../tests/integration.rs | 1621 +++++++++++ .../tests/math_test.rs | 394 +++ contracts/voting_escrow_lite/.cargo/config | 6 + contracts/voting_escrow_lite/Cargo.toml | 41 + contracts/voting_escrow_lite/README.md | 348 +++ .../voting_escrow_lite/examples/schema.rs | 11 + contracts/voting_escrow_lite/src/contract.rs | 113 + contracts/voting_escrow_lite/src/error.rs | 52 + contracts/voting_escrow_lite/src/execute.rs | 596 ++++ contracts/voting_escrow_lite/src/lib.rs | 12 + .../src/marketing_validation.rs | 75 + contracts/voting_escrow_lite/src/query.rs | 273 ++ contracts/voting_escrow_lite/src/state.rs | 35 + contracts/voting_escrow_lite/src/utils.rs | 44 + .../voting_escrow_lite/tests/integration.rs | 1164 ++++++++ .../voting_escrow_lite/tests/simulation.todo | 394 +++ .../voting_escrow_lite/tests/test_utils.rs | 625 ++++ packages/astroport-governance/Cargo.toml | 5 +- packages/astroport-governance/src/assembly.rs | 36 +- .../src/generator_controller_lite.rs | 184 ++ packages/astroport-governance/src/lib.rs | 3 + packages/astroport-governance/src/outpost.rs | 12 + packages/astroport-governance/src/utils.rs | 48 +- .../src/voting_escrow_lite.rs | 340 +++ packages/astroport-tests-lite/Cargo.toml | 34 + .../src/address_generator.rs | 19 + packages/astroport-tests-lite/src/base.rs | 359 +++ .../src/controller_helper.rs | 493 ++++ .../astroport-tests-lite/src/escrow_helper.rs | 397 +++ packages/astroport-tests-lite/src/lib.rs | 54 + 46 files changed, 13084 insertions(+), 398 deletions(-) create mode 100644 contracts/assembly/tests/integration.vxastro-full create mode 100644 contracts/generator_controller_lite/.cargo/config create mode 100644 contracts/generator_controller_lite/Cargo.toml create mode 100644 contracts/generator_controller_lite/README.md create mode 100644 contracts/generator_controller_lite/src/bps.rs create mode 100644 contracts/generator_controller_lite/src/contract.rs create mode 100644 contracts/generator_controller_lite/src/error.rs create mode 100644 contracts/generator_controller_lite/src/lib.rs create mode 100644 contracts/generator_controller_lite/src/state.rs create mode 100644 contracts/generator_controller_lite/src/utils.rs create mode 100644 contracts/generator_controller_lite/tests/integration.rs create mode 100644 contracts/generator_controller_lite/tests/math_test.rs create mode 100644 contracts/voting_escrow_lite/.cargo/config create mode 100644 contracts/voting_escrow_lite/Cargo.toml create mode 100644 contracts/voting_escrow_lite/README.md create mode 100644 contracts/voting_escrow_lite/examples/schema.rs create mode 100644 contracts/voting_escrow_lite/src/contract.rs create mode 100644 contracts/voting_escrow_lite/src/error.rs create mode 100644 contracts/voting_escrow_lite/src/execute.rs create mode 100644 contracts/voting_escrow_lite/src/lib.rs create mode 100644 contracts/voting_escrow_lite/src/marketing_validation.rs create mode 100644 contracts/voting_escrow_lite/src/query.rs create mode 100644 contracts/voting_escrow_lite/src/state.rs create mode 100644 contracts/voting_escrow_lite/src/utils.rs create mode 100644 contracts/voting_escrow_lite/tests/integration.rs create mode 100644 contracts/voting_escrow_lite/tests/simulation.todo create mode 100644 contracts/voting_escrow_lite/tests/test_utils.rs create mode 100644 packages/astroport-governance/src/generator_controller_lite.rs create mode 100644 packages/astroport-governance/src/outpost.rs create mode 100644 packages/astroport-governance/src/voting_escrow_lite.rs create mode 100644 packages/astroport-tests-lite/Cargo.toml create mode 100644 packages/astroport-tests-lite/src/address_generator.rs create mode 100644 packages/astroport-tests-lite/src/base.rs create mode 100644 packages/astroport-tests-lite/src/controller_helper.rs create mode 100644 packages/astroport-tests-lite/src/escrow_helper.rs create mode 100644 packages/astroport-tests-lite/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ad24f22d..c33e9aff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,15 +26,15 @@ source = "git+https://github.com/astroport-fi/astroport-core?branch=merge/releas dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", + "cw-storage-plus 0.15.1", ] [[package]] name = "astro-assembly" -version = "1.5.0" +version = "1.6.0" dependencies = [ "anyhow", - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "astroport-nft", "astroport-staking", "astroport-token", @@ -42,14 +42,15 @@ dependencies = [ "builder-unlock", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus", - "cw2", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "cw20", "ibc-controller-package", "thiserror", "voting-escrow", "voting-escrow-delegation", + "voting-escrow-lite", ] [[package]] @@ -60,8 +61,8 @@ dependencies = [ "ap-native-coin-registry", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw-utils", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", "cw20", "itertools", "uint", @@ -74,8 +75,8 @@ source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/ dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw-utils", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", "cw20", "itertools", "uint", @@ -85,14 +86,14 @@ dependencies = [ name = "astroport-escrow-fee-distributor" version = "1.0.2" dependencies = [ - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "astroport-tests", "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus", - "cw2", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "cw20", "thiserror", ] @@ -105,8 +106,8 @@ dependencies = [ "astroport 2.9.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw2", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "itertools", "protobuf", "thiserror", @@ -118,12 +119,12 @@ version = "2.3.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" dependencies = [ "astroport 2.9.0", - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", + "cw-storage-plus 0.15.1", "cw1-whitelist", - "cw2", + "cw2 0.15.1", "cw20", "protobuf", "thiserror", @@ -132,24 +133,25 @@ dependencies = [ [[package]] name = "astroport-governance" version = "1.2.0" +source = "git+https://github.com/astroport-fi/astroport-governance.git?branch=merge/release#f7381bb095c15a34c8cb4ba51911506f4d216462" dependencies = [ - "astroport 2.9.0", + "astroport 2.4.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", + "cw-storage-plus 0.15.1", "cw20", ] [[package]] name = "astroport-governance" -version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance.git?branch=merge/release#f7381bb095c15a34c8cb4ba51911506f4d216462" +version = "1.3.0" dependencies = [ - "astroport 2.4.0", + "astroport 2.9.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", + "cw-storage-plus 0.15.1", "cw20", + "thiserror", ] [[package]] @@ -164,10 +166,10 @@ dependencies = [ name = "astroport-nft" version = "1.0.0" dependencies = [ - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "cosmwasm-schema", "cosmwasm-std", - "cw2", + "cw2 0.15.1", "cw721", "cw721-base", ] @@ -180,8 +182,8 @@ dependencies = [ "astroport 2.9.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw2", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "cw20", "integer-sqrt", "protobuf", @@ -196,8 +198,8 @@ dependencies = [ "astroport 2.9.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw2", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "cw20", "protobuf", "thiserror", @@ -212,20 +214,44 @@ dependencies = [ "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "astroport-pair", "astroport-staking", "astroport-token", "astroport-whitelist", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw2", + "cw-multi-test 0.15.1", + "cw2 0.15.1", "cw20", "generator-controller", "voting-escrow", ] +[[package]] +name = "astroport-tests-lite" +version = "1.0.0" +dependencies = [ + "anyhow", + "astro-assembly", + "astroport 2.9.0", + "astroport-escrow-fee-distributor", + "astroport-factory", + "astroport-generator", + "astroport-governance 1.3.0", + "astroport-pair", + "astroport-staking", + "astroport-token", + "astroport-whitelist", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.16.5", + "cw2 0.15.1", + "cw20", + "generator-controller-lite", + "voting-escrow-lite", +] + [[package]] name = "astroport-token" version = "1.1.1" @@ -234,7 +260,7 @@ dependencies = [ "astroport 2.9.0", "cosmwasm-schema", "cosmwasm-std", - "cw2", + "cw2 0.15.1", "cw20", "cw20-base", "snafu", @@ -249,7 +275,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist", - "cw2", + "cw2 0.15.1", "thiserror", ] @@ -261,8 +287,8 @@ dependencies = [ "astroport 2.9.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw2", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "cw20", "cw20-base", "snafu", @@ -336,13 +362,13 @@ name = "builder-unlock" version = "1.2.3" dependencies = [ "astroport 2.9.0", - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus", - "cw2", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "cw20", "thiserror", ] @@ -512,10 +538,29 @@ dependencies = [ "anyhow", "cosmwasm-std", "cosmwasm-storage", - "cw-storage-plus", - "cw-utils", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "derivative", + "itertools", + "prost", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-multi-test" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", "derivative", "itertools", + "k256", "prost", "schemars", "serde", @@ -533,6 +578,17 @@ dependencies = [ "serde", ] +[[package]] +name = "cw-storage-plus" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + [[package]] name = "cw-utils" version = "0.15.1" @@ -541,7 +597,22 @@ checksum = "0ae0b69fa7679de78825b4edeeec045066aa2b2c4b6e063d80042e565bb4da5c" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw2", + "cw2 0.15.1", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw-utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 1.1.0", "schemars", "semver", "serde", @@ -568,10 +639,10 @@ checksum = "233dd13f61495f1336da57c8bdca0536fa9f8dd59c12d2bbfc59928ea580e478" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw-utils", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", "cw1", - "cw2", + "cw2 0.15.1", "schemars", "serde", "thiserror", @@ -585,9 +656,23 @@ checksum = "5abb8ecea72e09afff830252963cb60faf945ce6cef2c20a43814516082653da" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", + "cw-storage-plus 0.15.1", + "schemars", + "serde", +] + +[[package]] +name = "cw2" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.1.0", "schemars", "serde", + "thiserror", ] [[package]] @@ -598,7 +683,7 @@ checksum = "f6025276fb6e603e974c21f3e4606982cdc646080e8fba3198816605505e1d9a" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils", + "cw-utils 0.15.1", "schemars", "serde", ] @@ -611,9 +696,9 @@ checksum = "0909c56d0c14601fbdc69382189799482799dcad87587926aec1f3aa321abc41" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw-utils", - "cw2", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw2 0.15.1", "cw20", "schemars", "semver", @@ -629,7 +714,7 @@ checksum = "20dfe04f86e5327956b559ffcc86d9a43167391f37402afd8bf40b0be16bee4d" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils", + "cw-utils 0.15.1", "schemars", "serde", ] @@ -642,9 +727,9 @@ checksum = "62c3ee3b669fc2a8094301a73fd7be97a7454d4df2650c33599f737e8f254d24" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus", - "cw-utils", - "cw2", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw2 0.15.1", "cw721", "schemars", "serde", @@ -816,7 +901,7 @@ dependencies = [ "anyhow", "astroport-factory", "astroport-generator", - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "astroport-pair", "astroport-staking", "astroport-tests", @@ -824,9 +909,34 @@ dependencies = [ "astroport-whitelist", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus", - "cw2", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", + "cw20", + "itertools", + "proptest", + "thiserror", + "voting-escrow", +] + +[[package]] +name = "generator-controller-lite" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport-factory", + "astroport-generator", + "astroport-governance 1.3.0", + "astroport-pair", + "astroport-staking", + "astroport-tests-lite", + "astroport-token", + "astroport-whitelist", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.16.5", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "cw20", "itertools", "proptest", @@ -901,7 +1011,7 @@ name = "ibc-controller-package" version = "0.1.0" source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#2b98128012c373d4ed1cb5459e69f38461e262e2" dependencies = [ - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance.git?branch=merge/release)", + "astroport-governance 1.2.0", "astroport-ibc", "cosmwasm-schema", "cosmwasm-std", @@ -1480,14 +1590,14 @@ version = "1.3.0" dependencies = [ "anyhow", "astroport-escrow-fee-distributor", - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "astroport-staking", "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus", - "cw2", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", "cw20", "cw20-base", "proptest", @@ -1499,21 +1609,41 @@ name = "voting-escrow-delegation" version = "1.0.0" dependencies = [ "anyhow", - "astroport-governance 1.2.0", + "astroport-governance 1.3.0", "astroport-nft", "astroport-tests", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus", - "cw-utils", - "cw2", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw2 0.15.1", "cw721", "cw721-base", "proptest", "thiserror", ] +[[package]] +name = "voting-escrow-lite" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport-governance 1.3.0", + "astroport-staking", + "astroport-token", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw2 0.15.1", + "cw20", + "cw20-base", + "generator-controller-lite", + "proptest", + "thiserror", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 67e04252..3f9175f7 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astro-assembly" -version = "1.5.0" +version = "1.6.0" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" @@ -29,6 +29,7 @@ cw-multi-test = "0.15" astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } astroport-xastro-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } voting-escrow = { path = "../voting_escrow" } +voting-escrow-lite = { path = "../voting_escrow_lite" } voting-escrow-delegation = { path = "../voting_escrow_delegation" } astroport-nft = { path = "../nft" } astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 81cdf60d..3218d29e 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -1,7 +1,6 @@ use cosmwasm_std::{ attr, entry_point, from_binary, to_binary, wasm_execute, Addr, Binary, CosmosMsg, Decimal, - Deps, DepsMut, Env, IbcQuery, ListChannelsResponse, MessageInfo, Order, QuerierWrapper, - QueryRequest, Response, StdResult, Uint128, Uint64, WasmMsg, + Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, Uint128, Uint64, WasmMsg, }; use cw2::{get_contract_version, set_contract_version}; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20ReceiveMsg}; @@ -20,12 +19,14 @@ use astroport::xastro_token::QueryMsg as XAstroTokenQueryMsg; use astroport_governance::builder_unlock::msg::{ AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, StateResponse, }; -use astroport_governance::utils::WEEK; -use astroport_governance::voting_escrow::{QueryMsg as VotingEscrowQueryMsg, VotingPowerResponse}; +use astroport_governance::utils::{check_controller_supports_channel, WEEK}; use astroport_governance::voting_escrow_delegation::QueryMsg::AdjustedBalance; +use astroport_governance::voting_escrow_lite::{ + QueryMsg as VotingEscrowQueryMsg, VotingPowerResponse, +}; use crate::error::ContractError; -use crate::migration::{migrate_config_to_140, migrate_proposals_to_v140, MigrateMsg}; +use crate::migration::{migrate_config_to_160, migrate_proposals_to_v160, MigrateMsg}; use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT}; use ibc_controller_package::ExecuteMsg as ControllerExecuteMsg; @@ -64,6 +65,8 @@ pub fn instantiate( &msg.voting_escrow_delegator_addr, )?, ibc_controller: addr_opt_validate(deps.api, &msg.ibc_controller)?, + generator_controller: addr_opt_validate(deps.api, &msg.generator_controller_addr)?, + hub: addr_opt_validate(deps.api, &msg.hub_addr)?, builder_unlock_addr: deps.api.addr_validate(&msg.builder_unlock_addr)?, proposal_voting_period: msg.proposal_voting_period, proposal_effective_delay: msg.proposal_effective_delay, @@ -91,10 +94,15 @@ pub fn instantiate( /// /// * **ExecuteMsg::CastVote { proposal_id, vote }** Cast a vote on a specific proposal. /// +/// * **ExecuteMsg::CastOutpostVote { proposal_id, voter, vote, voting_power }** Cast a vote on a specific proposal from an Outpost. +/// /// * **ExecuteMsg::EndProposal { proposal_id }** Sets the status of an expired/finalized proposal. /// /// * **ExecuteMsg::ExecuteProposal { proposal_id }** Executes a successful proposal. /// +/// * **ExecuteMsg::ExecuteEmissionsProposal { title, description, link, messages, ibc_channel }** Loads and executes an +/// emissions proposal from the generator controller +/// /// * **ExecuteMsg::RemoveCompletedProposal { proposal_id }** Removes a finalized proposal from the proposal list. /// /// * **ExecuteMsg::UpdateConfig(config)** Updates the contract configuration. @@ -108,8 +116,28 @@ pub fn execute( match msg { ExecuteMsg::Receive(cw20_msg) => receive_cw20(deps, env, info, cw20_msg), ExecuteMsg::CastVote { proposal_id, vote } => cast_vote(deps, env, info, proposal_id, vote), + ExecuteMsg::CastOutpostVote { + proposal_id, + voter, + vote, + voting_power, + } => cast_outpost_vote(deps, env, info, proposal_id, voter, vote, voting_power), ExecuteMsg::EndProposal { proposal_id } => end_proposal(deps, env, proposal_id), ExecuteMsg::ExecuteProposal { proposal_id } => execute_proposal(deps, env, proposal_id), + ExecuteMsg::ExecuteEmissionsProposal { + title, + description, + messages, + ibc_channel, + } => submit_execute_emissions_proposal( + deps, + env, + info, + title, + description, + messages, + ibc_channel, + ), ExecuteMsg::CheckMessages { messages } => check_messages(env, messages), ExecuteMsg::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}), ExecuteMsg::RemoveCompletedProposal { proposal_id } => { @@ -271,7 +299,8 @@ pub fn cast_vote( return Err(ContractError::VotingPeriodEnded {}); } - if proposal.for_voters.contains(&info.sender) || proposal.against_voters.contains(&info.sender) + if proposal.for_voters.contains(&info.sender.to_string()) + || proposal.against_voters.contains(&info.sender.to_string()) { return Err(ContractError::UserAlreadyVoted {}); } @@ -285,11 +314,11 @@ pub fn cast_vote( match vote_option { ProposalVoteOption::For => { proposal.for_power = proposal.for_power.checked_add(voting_power)?; - proposal.for_voters.push(info.sender.clone()); + proposal.for_voters.push(info.sender.to_string()); } ProposalVoteOption::Against => { proposal.against_power = proposal.against_power.checked_add(voting_power)?; - proposal.against_voters.push(info.sender.clone()); + proposal.against_voters.push(info.sender.to_string()); } }; @@ -304,6 +333,85 @@ pub fn cast_vote( ])) } +/// Cast a vote on a proposal from an Outpost. +/// This is a special case of `cast_vote` that allows Outposts to forward votes on +/// behalf of their users. The Hub contract is the only one allowed to call this method. +/// +/// * **proposal_id** is the identifier of the proposal. +/// +/// * **voter** is the address of the voter on the Outpost. +/// +/// * **vote_option** contains the vote option. +/// +/// * **voting_power** contains the voting power applied to this vote. +pub fn cast_outpost_vote( + deps: DepsMut, + env: Env, + info: MessageInfo, + proposal_id: u64, + voter: String, + vote_option: ProposalVoteOption, + voting_power: Uint128, +) -> Result { + let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?; + + let config = CONFIG.load(deps.storage)?; + + // We only allow the Hub to submit votes on behalf of Outpost user + // The Hub is responsible for validating the Hub vote with the Outpost + let hub = match config.hub { + Some(hub) => hub, + None => return Err(ContractError::InvalidHub {}), + }; + + if info.sender != hub { + return Err(ContractError::Unauthorized {}); + } + + if proposal.status != ProposalStatus::Active { + return Err(ContractError::ProposalNotActive {}); + } + + if proposal.submitter == voter { + return Err(ContractError::Unauthorized {}); + } + + if env.block.height > proposal.end_block { + return Err(ContractError::VotingPeriodEnded {}); + } + + if proposal.for_voters.contains(&voter) || proposal.against_voters.contains(&voter) { + return Err(ContractError::UserAlreadyVoted {}); + } + + if voting_power.is_zero() { + return Err(ContractError::NoVotingPower {}); + } + + // Voting power provided is used as is from the Hub. Validation of the voting + // power is done by the Hub contract with the Outpost. + match vote_option { + ProposalVoteOption::For => { + proposal.for_power = proposal.for_power.checked_add(voting_power)?; + proposal.for_voters.push(voter.clone()); + } + ProposalVoteOption::Against => { + proposal.against_power = proposal.against_power.checked_add(voting_power)?; + proposal.against_voters.push(voter.clone()); + } + }; + + PROPOSALS.save(deps.storage, proposal_id, &proposal)?; + + Ok(Response::new().add_attributes(vec![ + attr("action", "cast_outpost_vote"), + attr("proposal_id", proposal_id.to_string()), + attr("voter", &voter), + attr("vote", vote_option.to_string()), + attr("voting_power", voting_power), + ])) +} + /// Ends proposal voting period and sets the proposal status by id. pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result { let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?; @@ -428,6 +536,80 @@ pub fn execute_proposal( .add_messages(messages)) } +/// Load and execute a special emissions proposal. This proposal is passed +/// immediately and is not subject to voting as it is coming from the +/// generator controller based on emission votes. +#[allow(clippy::too_many_arguments)] +pub fn submit_execute_emissions_proposal( + deps: DepsMut, + env: Env, + info: MessageInfo, + title: String, + description: String, + messages: Vec, + ibc_channel: Option, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Verify that only the generator controller has been set + let generator_controller = match config.generator_controller { + Some(config_generator_controller) => config_generator_controller, + None => return Err(ContractError::InvalidGeneratorController {}), + }; + + // Only the generator controller may create these proposals. These proposals + // are typically for setting alloc points on Outposts + if info.sender != generator_controller { + return Err(ContractError::Unauthorized {}); + } + + // Ensure that we have messages to execute + if messages.is_empty() { + return Err(ContractError::InvalidProposalMessages {}); + } + + // Check that controller exists and it supports this channel + if let Some(ibc_channel) = &ibc_channel { + if let Some(ibc_controller) = &config.ibc_controller { + check_controller_supports_channel(deps.querier, ibc_controller, ibc_channel)?; + } else { + return Err(ContractError::MissingIBCController {}); + } + } + + // Update the proposal count + let count = PROPOSAL_COUNT.update(deps.storage, |c| -> StdResult<_> { + Ok(c.checked_add(Uint64::new(1))?) + })?; + + let proposal = Proposal { + proposal_id: count, + submitter: info.sender, + status: ProposalStatus::Passed, + for_power: Uint128::zero(), + against_power: Uint128::zero(), + for_voters: vec![generator_controller.to_string()], + against_voters: Vec::new(), + start_block: env.block.height, + start_time: env.block.time.seconds(), + end_block: env.block.height, + delayed_end_block: env.block.height, + expiration_block: env.block.height + config.proposal_expiration_period, + title, + description, + link: None, + messages: Some(messages), + deposit_amount: Uint128::zero(), + ibc_channel, + }; + + proposal.validate(config.whitelisted_links)?; + + PROPOSALS.save(deps.storage, count.u64(), &proposal)?; + + execute_proposal(deps, env, proposal.proposal_id.u64()) +} + /// Checks that proposal messages are correct. pub fn check_messages(env: Env, mut messages: Vec) -> Result { messages.push(CosmosMsg::Wasm(WasmMsg::Execute { @@ -503,6 +685,14 @@ pub fn update_config( config.ibc_controller = Some(deps.api.addr_validate(&ibc_controller)?) } + if let Some(generator_controller) = updated_config.generator_controller { + config.generator_controller = Some(deps.api.addr_validate(&generator_controller)?) + } + + if let Some(hub) = updated_config.hub { + config.hub = Some(deps.api.addr_validate(&hub)?) + } + if let Some(builder_unlock_addr) = updated_config.builder_unlock_addr { config.builder_unlock_addr = deps.api.addr_validate(&builder_unlock_addr)?; } @@ -687,7 +877,7 @@ pub fn query_proposal_voters( vote_option: ProposalVoteOption, start: Option, limit: Option, -) -> StdResult> { +) -> StdResult> { let limit = limit.unwrap_or(DEFAULT_VOTERS_LIMIT).min(MAX_VOTERS_LIMIT); let start = start.unwrap_or_default(); @@ -736,7 +926,6 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std block: proposal.start_block, }, )?; - let mut total = xastro_amount.balance; let locked_amount: AllocationResponse = deps.querier.query_wasm_smart( @@ -763,6 +952,7 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std }, )? } else { + // For vxASTRO lite, this will always be 0 let res: VotingPowerResponse = deps.querier.query_wasm_smart( &vxastro_token_addr, &VotingEscrowQueryMsg::UserVotingPowerAt { @@ -770,7 +960,6 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std time: proposal.start_time - WEEK, }, )?; - res.voting_power }; @@ -780,9 +969,9 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std let locked_xastro: Uint128 = deps.querier.query_wasm_smart( vxastro_token_addr, - &VotingEscrowQueryMsg::UserDepositAtHeight { + &VotingEscrowQueryMsg::UserDepositAt { user: sender, - height: proposal.start_block, + timestamp: Uint64::from(proposal.start_time), }, )?; @@ -818,6 +1007,7 @@ pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult< if let Some(vxastro_token_addr) = config.vxastro_token_addr { // Total vxASTRO voting power + // For vxASTRO lite, this will always be 0 let vxastro: VotingPowerResponse = deps.querier.query_wasm_smart( vxastro_token_addr, &VotingEscrowQueryMsg::TotalVotingPowerAt { @@ -832,28 +1022,6 @@ pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult< Ok(total) } -/// Checks that controller supports given IBC-channel. -/// ## Params -/// * **querier** is an object of type [`QuerierWrapper`]. -/// -/// * **ibc_controller** is an ibc controller contract address. -/// -/// * **given_channel** is an IBC channel id the function needs to check. -pub fn check_controller_supports_channel( - querier: QuerierWrapper, - ibc_controller: &Addr, - given_channel: &String, -) -> Result<(), ContractError> { - let port_id = Some(format!("wasm.{ibc_controller}")); - let ListChannelsResponse { channels } = - querier.query(&QueryRequest::Ibc(IbcQuery::ListChannels { port_id }))?; - channels - .iter() - .find(|channel| &channel.endpoint.channel_id == given_channel) - .map(|_| ()) - .ok_or_else(|| ContractError::InvalidChannel(given_channel.to_string())) -} - /// Manages contract migration. #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(mut deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { @@ -862,8 +1030,8 @@ pub fn migrate(mut deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result match contract_version.version.as_ref() { "1.3.0" => { - let cfg = migrate_config_to_140(deps.branch(), msg)?; - migrate_proposals_to_v140(deps.branch(), &cfg)?; + let cfg = migrate_config_to_160(deps.branch(), msg)?; + migrate_proposals_to_v160(deps.branch(), &cfg)?; } _ => return Err(ContractError::MigrationError {}), }, diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index a89cdf17..d6cec35e 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -67,6 +67,15 @@ pub enum ContractError { #[error("Sender is not an IBC controller installed in the assembly")] InvalidIBCController {}, + + #[error("Sender is not the Generator controller installed in the assembly")] + InvalidGeneratorController {}, + + #[error("Sender is not the Hub installed in the assembly")] + InvalidHub {}, + + #[error("The proposal has no messages to execute")] + InvalidProposalMessages {}, } impl From for ContractError { diff --git a/contracts/assembly/src/migration.rs b/contracts/assembly/src/migration.rs index 15cc4a18..a8b43384 100644 --- a/contracts/assembly/src/migration.rs +++ b/contracts/assembly/src/migration.rs @@ -14,6 +14,8 @@ pub struct MigrateMsg { voting_escrow_delegator_addr: Option, vxastro_token_addr: Option, ibc_controller: Option, + generator_controller: Option, + hub: Option, } #[cw_serde] @@ -84,8 +86,8 @@ pub struct ConfigV130 { pub const CONFIG_V130: Item = Item::new("config"); -/// Migrate proposals to V1.4.0 -pub(crate) fn migrate_proposals_to_v140(deps: DepsMut, cfg: &Config) -> StdResult<()> { +/// Migrate proposals to V1.6.0 +pub(crate) fn migrate_proposals_to_v160(deps: DepsMut, cfg: &Config) -> StdResult<()> { let v130_proposals_interface: Map = Map::new("proposals"); let proposals_v130 = v130_proposals_interface .range(deps.storage, None, None, cosmwasm_std::Order::Ascending {}) @@ -101,8 +103,16 @@ pub(crate) fn migrate_proposals_to_v140(deps: DepsMut, cfg: &Config) -> StdResul status: proposal.status, for_power: proposal.for_power, against_power: proposal.against_power, - for_voters: proposal.for_voters, - against_voters: proposal.against_voters, + for_voters: proposal + .for_voters + .into_iter() + .map(|addr| addr.to_string()) + .collect(), + against_voters: proposal + .against_voters + .into_iter() + .map(|addr| addr.to_string()) + .collect(), start_block: proposal.start_block, start_time: proposal.start_time, end_block: proposal.end_block, @@ -123,8 +133,8 @@ pub(crate) fn migrate_proposals_to_v140(deps: DepsMut, cfg: &Config) -> StdResul Ok(()) } -/// Migrate contract config to V1.4.0 -pub(crate) fn migrate_config_to_140(deps: DepsMut, msg: MigrateMsg) -> StdResult { +/// Migrate contract config to V1.6.0 +pub(crate) fn migrate_config_to_160(deps: DepsMut, msg: MigrateMsg) -> StdResult { let cfg_v130 = CONFIG_V130.load(deps.storage)?; let cfg = Config { @@ -135,6 +145,8 @@ pub(crate) fn migrate_config_to_140(deps: DepsMut, msg: MigrateMsg) -> StdResult &msg.voting_escrow_delegator_addr, )?, ibc_controller: cfg_v130.ibc_controller, + generator_controller: addr_opt_validate(deps.api, &msg.generator_controller)?, + hub: addr_opt_validate(deps.api, &msg.hub)?, builder_unlock_addr: cfg_v130.builder_unlock_addr, proposal_voting_period: cfg_v130.proposal_voting_period, proposal_effective_delay: cfg_v130.proposal_effective_delay, diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 7e05f0ca..cd976fdf 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -7,10 +7,11 @@ use astroport_governance::assembly::{ ProposalStatus, ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, DEPOSIT_INTERVAL, VOTING_PERIOD_INTERVAL, }; +use cosmwasm_std::coins; use std::str::FromStr; -use astroport_governance::voting_escrow::{ +use astroport_governance::voting_escrow_lite::{ Cw20HookMsg as VXAstroCw20HookMsg, InstantiateMsg as VXAstroInstantiateMsg, }; @@ -19,10 +20,6 @@ use astroport_governance::builder_unlock::msg::{ }; use astroport_governance::builder_unlock::{AllocationParams, Schedule}; use astroport_governance::utils::{EPOCH_START, WEEK}; -use astroport_governance::voting_escrow_delegation::{ - ExecuteMsg as DelegatorExecuteMsg, InstantiateMsg as DelegatorInstantiateMsg, - QueryMsg as DelegatorQueryMsg, -}; use cosmwasm_std::{ testing::{mock_env, MockApi, MockStorage}, to_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, @@ -66,6 +63,8 @@ fn test_contract_instantiation() { vxastro_token_addr: Some(vxastro_token_addr.to_string()), voting_escrow_delegator_addr: None, ibc_controller: None, + generator_controller_addr: None, + hub_addr: None, builder_unlock_addr: builder_unlock_addr.to_string(), proposal_voting_period: PROPOSAL_VOTING_PERIOD, proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, @@ -219,7 +218,7 @@ fn test_proposal_submitting() { let user = Addr::unchecked("user1"); let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false); + instantiate_contracts(&mut app, owner, false, false); let proposals: ProposalListResponse = app .wrap() @@ -487,6 +486,8 @@ fn test_proposal_submitting() { vxastro_token_addr: None, voting_escrow_delegator_addr: None, ibc_controller: None, + generator_controller: None, + hub: None, builder_unlock_addr: None, proposal_voting_period: Some(750), proposal_effective_delay: None, @@ -536,6 +537,8 @@ fn test_proposal_submitting() { vxastro_token_addr: None, voting_escrow_delegator_addr: None, ibc_controller: None, + generator_controller: None, + hub: None, builder_unlock_addr: None, proposal_voting_period: Some(750), proposal_effective_delay: None, @@ -571,7 +574,7 @@ fn test_successful_proposal() { builder_unlock_addr, assembly_addr, _, - ) = instantiate_contracts(&mut app, owner, false); + ) = instantiate_contracts(&mut app, owner, false, false); // Init voting power for users let balances: Vec<(&str, u128, u128)> = vec![ @@ -626,7 +629,7 @@ fn test_successful_proposal() { "user10".to_string(), AllocationParams { amount: Uint128::from(30u32), - ..default_allocation_params.clone() + ..default_allocation_params }, ), ]; @@ -675,6 +678,8 @@ fn test_successful_proposal() { vxastro_token_addr: None, voting_escrow_delegator_addr: None, ibc_controller: None, + generator_controller: None, + hub: None, builder_unlock_addr: None, proposal_voting_period: Some(PROPOSAL_VOTING_PERIOD + 1000), proposal_effective_delay: None, @@ -694,21 +699,21 @@ fn test_successful_proposal() { ); let votes: Vec<(&str, ProposalVoteOption, u128)> = vec![ - ("user1", ProposalVoteOption::For, 280u128), - ("user2", ProposalVoteOption::For, 350u128), - ("user3", ProposalVoteOption::For, 550u128), - ("user4", ProposalVoteOption::For, 350u128), - ("user5", ProposalVoteOption::For, 240u128), - ("user6", ProposalVoteOption::For, 600u128), + ("user1", ProposalVoteOption::For, 180u128), + ("user2", ProposalVoteOption::For, 200u128), + ("user3", ProposalVoteOption::For, 400u128), + ("user4", ProposalVoteOption::For, 300u128), + ("user5", ProposalVoteOption::For, 90u128), + ("user6", ProposalVoteOption::For, 300u128), ("user7", ProposalVoteOption::For, 130u128), - ("user8", ProposalVoteOption::Against, 330u128), + ("user8", ProposalVoteOption::Against, 180u128), ("user9", ProposalVoteOption::Against, 50u128), - ("user10", ProposalVoteOption::Against, 270u128), + ("user10", ProposalVoteOption::Against, 120u128), ("user11", ProposalVoteOption::Against, 500u128), ("user12", ProposalVoteOption::For, 10000_000000u128), ]; - check_total_vp(&mut app, &assembly_addr, 1, 20000003650); + check_total_vp(&mut app, &assembly_addr, 1, 20000002450); for (addr, option, expected_vp) in votes { let sender = Addr::unchecked(addr); @@ -761,11 +766,11 @@ fn test_successful_proposal() { .unwrap(); // Check proposal votes - assert_eq!(proposal.for_power, Uint128::from(10000002500u128)); - assert_eq!(proposal.against_power, Uint128::from(1150u32)); + assert_eq!(proposal.for_power, Uint128::from(10000001600u128)); + assert_eq!(proposal.against_power, Uint128::from(850u32)); - assert_eq!(proposal_votes.for_power, Uint128::from(10000002500u128)); - assert_eq!(proposal_votes.against_power, Uint128::from(1150u32)); + assert_eq!(proposal_votes.for_power, Uint128::from(10000001600u128)); + assert_eq!(proposal_votes.against_power, Uint128::from(850u32)); assert_eq!( proposal_for_voters, @@ -953,6 +958,143 @@ fn test_successful_proposal() { assert_eq!(res.proposal_count, Uint64::from(1u32)); } +#[cfg(not(feature = "testnet"))] +#[test] +fn test_successful_emissions_proposal() { + use cosmwasm_std::{coins, BankMsg}; + + let mut app = mock_app(); + let owner = Addr::unchecked("generator_controller"); + + let (_, _, _, _, _, assembly_addr, _) = instantiate_contracts(&mut app, owner, true, false); + + // Provide some funds to the Assembly contract to use in the proposal messages + app.init_modules(|router, _, storage| { + router.bank.init_balance( + storage, + &Addr::unchecked(assembly_addr.clone()), + coins(1000, "uluna"), + ) + }) + .unwrap(); + + let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { + title: "Emissions Test title".to_string(), + description: "Emissions Test description".to_string(), + // Sample message to use as we don't have IBC or the Generator to set emissions on + messages: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: "generator_controller".into(), + amount: coins(1, "uluna"), + })], + ibc_channel: None, + }; + + app.execute_contract( + Addr::unchecked("generator_controller"), + assembly_addr.clone(), + &emissions_proposal_msg, + &[], + ) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart(assembly_addr, &QueryMsg::Proposal { proposal_id: 1 }) + .unwrap(); + + assert_eq!(proposal.status, ProposalStatus::Executed); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_no_generator_controller_emissions_proposal() { + let mut app = mock_app(); + let owner = Addr::unchecked("generator_controller"); + + let (_, _, _, _, _, assembly_addr, _) = instantiate_contracts(&mut app, owner, false, false); + let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { + title: "Emissions Test title!".to_string(), + description: "Emissions Test description!".to_string(), + messages: vec![], + ibc_channel: None, + }; + + let err = app + .execute_contract( + Addr::unchecked("generator_controller"), + assembly_addr, + &emissions_proposal_msg, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Sender is not the Generator controller installed in the assembly" + ); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_empty_messages_emissions_proposal() { + let mut app = mock_app(); + let owner = Addr::unchecked("generator_controller"); + + let (_, _, _, _, _, assembly_addr, _) = instantiate_contracts(&mut app, owner, true, false); + let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { + title: "Emissions Test title!".to_string(), + description: "Emissions Test description!".to_string(), + messages: vec![], + ibc_channel: None, + }; + + let err = app + .execute_contract( + Addr::unchecked("generator_controller"), + assembly_addr, + &emissions_proposal_msg, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "The proposal has no messages to execute" + ); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_unauthorised_emissions_proposal() { + use cosmwasm_std::BankMsg; + + let mut app = mock_app(); + let owner = Addr::unchecked("generator_controller"); + + let (_, _, _, _, _, assembly_addr, _) = instantiate_contracts(&mut app, owner, true, false); + let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { + title: "Emissions Test title!".to_string(), + description: "Emissions Test description!".to_string(), + // Sample message to use as we don't have IBC or the Generator to set emissions on + messages: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: "generator_controller".into(), + amount: coins(1, "uluna"), + })], + ibc_channel: None, + }; + + let err = app + .execute_contract( + Addr::unchecked("not_generator_controller"), + assembly_addr, + &emissions_proposal_msg, + &[], + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Unauthorized"); +} + #[test] fn test_voting_power_changes() { let mut app = mock_app(); @@ -960,7 +1102,7 @@ fn test_voting_power_changes() { let owner = Addr::unchecked("owner"); let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false); + instantiate_contracts(&mut app, owner, false, false); // Mint tokens for submitting proposal mint_tokens( @@ -998,6 +1140,8 @@ fn test_voting_power_changes() { vxastro_token_addr: None, voting_escrow_delegator_addr: None, ibc_controller: None, + generator_controller: None, + hub: None, builder_unlock_addr: None, proposal_voting_period: Some(750), proposal_effective_delay: None, @@ -1084,6 +1228,290 @@ fn test_voting_power_changes() { assert_eq!(proposal.status, ProposalStatus::Passed); } +#[test] +fn test_fail_outpost_vote_without_hub() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, false); + + // Mint tokens for submitting proposal + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user0"), + PROPOSAL_REQUIRED_DEPOSIT, + ); + + // Mint tokens for casting votes at start block + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user1"), + 40000_000000, + ); + + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(750), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: None, + whitelist_remove: None, + }))) + .unwrap(), + funds: vec![], + })]), + ); + // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user2"), + 5000_000000, + ); + + app.update_block(next_block); + + // user1 can not vote from an Outpost due to no Hub contract set + let err = cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("invalid_contract"), + Addr::unchecked("user1"), + ProposalVoteOption::For, + Uint128::from(100u64), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Sender is not the Hub installed in the assembly" + ); +} + +#[test] +fn test_outpost_vote() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner.clone(), false, true); + + let user1_voting_power = 10_000_000_000; + let user2_voting_power = 5_000_000_000; + let remote_user1_voting_power = 80_000_000_000u128; + let remote_user2_voting_power = 3_000_000_000u128; + + // Mint tokens for submitting proposal + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user0"), + PROPOSAL_REQUIRED_DEPOSIT, + ); + + // Mint tokens for casting votes at start block + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user1"), + user1_voting_power, + ); + + // Mint tokens for casting votes against vote at start block + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user2"), + user2_voting_power, + ); + + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(750), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: None, + whitelist_remove: None, + }))) + .unwrap(), + funds: vec![], + })]), + ); + + app.update_block(next_block); + + // Outpost votes won't be accepted from other addresses + let err = cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("other_contract"), + Addr::unchecked("remote1"), + ProposalVoteOption::For, + Uint128::from(remote_user1_voting_power), + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + owner.clone(), + Addr::unchecked("remote1"), + ProposalVoteOption::For, + Uint128::from(remote_user1_voting_power), + ) + .unwrap(); + + cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + owner.clone(), + Addr::unchecked("remote2"), + ProposalVoteOption::Against, + Uint128::from(remote_user2_voting_power), + ) + .unwrap(); + + let err = cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + owner, + Addr::unchecked("remote1"), + ProposalVoteOption::For, + Uint128::from(remote_user2_voting_power), + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "User already voted!"); + + // user1 can vote as he had voting power before the proposal submitting. + cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user1"), + ProposalVoteOption::For, + ) + .unwrap(); + + let err = cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user1"), + ProposalVoteOption::For, + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "User already voted!"); + + // user2 can vote as he had voting power before the proposal submitting. + cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user2"), + ProposalVoteOption::Against, + ) + .unwrap(); + + app.update_block(next_block); + + // Skip voting period and delay + app.update_block(|bi| { + bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; + bi.time = bi + .time + .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); + }); + + // End proposal + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::EndProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + // Check proposal votes, Outpost and Hub votes should be counted + let total_for_voting_power = user1_voting_power + remote_user1_voting_power; + let total_against_voting_power = user2_voting_power + remote_user2_voting_power; + assert_eq!(proposal.for_power, Uint128::from(total_for_voting_power)); + assert_eq!( + proposal.against_power, + Uint128::from(total_against_voting_power) + ); + // Should be passed + assert_eq!(proposal.status, ProposalStatus::Passed); +} + #[cfg(not(feature = "testnet"))] #[test] fn test_block_height_selection() { @@ -1096,7 +1524,7 @@ fn test_block_height_selection() { let user3 = Addr::unchecked("user3"); let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false); + instantiate_contracts(&mut app, owner, false, false); // Mint tokens for submitting proposal mint_tokens( @@ -1216,7 +1644,7 @@ fn test_unsuccessful_proposal() { let owner = Addr::unchecked("owner"); let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false); + instantiate_contracts(&mut app, owner, false, false); // Init voting power for users let xastro_balances: Vec<(&str, u128)> = vec![ @@ -1355,7 +1783,7 @@ fn test_check_messages() { let mut app = mock_app(); let owner = Addr::unchecked("owner"); let (_, _, _, vxastro_addr, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false); + instantiate_contracts(&mut app, owner, false, false); change_owner(&mut app, &vxastro_addr, &assembly_addr); let user = Addr::unchecked("user"); @@ -1373,24 +1801,28 @@ fn test_check_messages() { ExecuteMsg::CheckMessages { messages } }; - let config_before: astroport_governance::voting_escrow::ConfigResponse = app + let config_before: astroport_governance::voting_escrow_lite::Config = app .wrap() .query_wasm_smart( &vxastro_addr, - &astroport_governance::voting_escrow::QueryMsg::Config {}, + &astroport_governance::voting_escrow_lite::QueryMsg::Config {}, ) .unwrap(); let vxastro_blacklist_msg = vec![( vxastro_addr.to_string(), to_binary( - &astroport_governance::voting_escrow::ExecuteMsg::UpdateConfig { new_guardian: None }, + &astroport_governance::voting_escrow_lite::ExecuteMsg::UpdateConfig { + new_guardian: None, + generator_controller: None, + outpost: None, + }, ) .unwrap(), )]; let err = app .execute_contract( - user.clone(), + user, assembly_addr.clone(), &into_check_msg(vxastro_blacklist_msg), &[], @@ -1401,191 +1833,16 @@ fn test_check_messages() { "Messages check passed. Nothing was committed to the blockchain" ); - let config_after: astroport_governance::voting_escrow::ConfigResponse = app + let config_after: astroport_governance::voting_escrow_lite::Config = app .wrap() .query_wasm_smart( &vxastro_addr, - &astroport_governance::voting_escrow::QueryMsg::Config {}, + &astroport_governance::voting_escrow_lite::QueryMsg::Config {}, ) .unwrap(); assert_eq!(config_before, config_after); } -#[test] -fn test_delegated_vp() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, vxastro_addr, _, assembly_addr, delegator) = - instantiate_contracts(&mut app, owner, true); - let delegator = delegator.unwrap(); - - let users = vec![ - ( - "user1", - 103_000_000_000u128, - 1000u16, - "user4", - 177_278_846_150u128, - ), - ( - "user2", - 612_000_000_000u128, - 2000u16, - "user5", - 1_053_346_153_800u128, - ), - ( - "user3", - 205_000_000_000u128, - 3000u16, - "user6", - 352_836_538_450u128, - ), - ]; - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - // Mint vxASTRO and delegate it to the other users - for (from, amount, bps, to, exp_vp) in users { - mint_vxastro( - &mut app, - &staking_instance, - xastro_addr.clone(), - &vxastro_addr, - Addr::unchecked(from), - amount, - ); - delegate_vxastro( - &mut app, - delegator.clone(), - Addr::unchecked(from), - Addr::unchecked(to), - bps, - ); - - let from_amount: Uint128 = app - .wrap() - .query_wasm_smart( - &delegator, - &DelegatorQueryMsg::AdjustedBalance { - account: from.to_string(), - timestamp: None, - }, - ) - .unwrap(); - - let to_amount: Uint128 = app - .wrap() - .query_wasm_smart( - &delegator, - &DelegatorQueryMsg::AdjustedBalance { - account: to.to_string(), - timestamp: None, - }, - ) - .unwrap(); - - assert_eq!(from_amount + to_amount, Uint128::from(exp_vp)); - } - - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - - let votes: Vec<(&str, ProposalVoteOption)> = vec![ - ("user1", ProposalVoteOption::Against), - ("user2", ProposalVoteOption::For), - ("user3", ProposalVoteOption::Against), - ("user4", ProposalVoteOption::For), - ("user5", ProposalVoteOption::Against), - ("user6", ProposalVoteOption::For), - ]; - - for (user, vote) in votes { - cast_vote( - &mut app, - assembly_addr.clone(), - 1u64, - Addr::unchecked(user), - vote, - ) - .unwrap(); - } - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.for_power, Uint128::from(1_578_255_769_188u128)); - assert_eq!(proposal.against_power, Uint128::from(925_205_769_212u128)); - - // Skip voting period - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); - }); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.status, ProposalStatus::Passed); -} - fn mock_app() -> App { let mut env = mock_env(); env.block.time = Timestamp::from_seconds(EPOCH_START); @@ -1604,21 +1861,29 @@ fn mock_app() -> App { fn instantiate_contracts( router: &mut App, owner: Addr, - with_delegator: bool, + with_generator_controller: bool, + with_hub: bool, ) -> (Addr, Addr, Addr, Addr, Addr, Addr, Option) { let token_addr = instantiate_astro_token(router, &owner); let (staking_addr, xastro_token_addr) = instantiate_xastro_token(router, &owner, &token_addr); let vxastro_token_addr = instantiate_vxastro_token(router, &owner, &xastro_token_addr); let builder_unlock_addr = instantiate_builder_unlock_contract(router, &owner, &token_addr); - let mut delegator_addr = None; + // If we want to test immediate proposals we need to set the address + // for the generator controller. Deploying the generator controller in this + // test would require deploying factory, tokens and pools. That test is + // better suited in the generator controller itself. Thus, we use the owner + // address as the generator controller address to test immediate proposals. + let mut generator_controller_addr = None; - if with_delegator { - delegator_addr = Some(instantiate_delegator_contract( - router, - &owner, - &vxastro_token_addr, - )); + if with_generator_controller { + generator_controller_addr = Some(owner.to_string()); + } + + let mut hub_addr = None; + + if with_hub { + hub_addr = Some(owner.to_string()); } let assembly_addr = instantiate_assembly_contract( @@ -1627,7 +1892,9 @@ fn instantiate_contracts( &xastro_token_addr, &vxastro_token_addr, &builder_unlock_addr, - delegator_addr.clone().map(String::from), + None, + generator_controller_addr, + hub_addr, ); ( @@ -1637,7 +1904,7 @@ fn instantiate_contracts( vxastro_token_addr, builder_unlock_addr, assembly_addr, - delegator_addr, + None, ) } @@ -1724,9 +1991,9 @@ fn instantiate_xastro_token(router: &mut App, owner: &Addr, astro_token: &Addr) fn instantiate_vxastro_token(router: &mut App, owner: &Addr, xastro: &Addr) -> Addr { let vxastro_token_contract = Box::new(ContractWrapper::new_with_empty( - voting_escrow::contract::execute, - voting_escrow::contract::instantiate, - voting_escrow::contract::query, + voting_escrow_lite::execute::execute, + voting_escrow_lite::contract::instantiate, + voting_escrow_lite::query::query, )); let vxastro_token_code_id = router.store_code(vxastro_token_contract); @@ -1735,6 +2002,8 @@ fn instantiate_vxastro_token(router: &mut App, owner: &Addr, xastro: &Addr) -> A owner: owner.to_string(), guardian_addr: Some(owner.to_string()), deposit_token_addr: xastro.to_string(), + generator_controller_addr: None, + outpost_addr: None, marketing: None, logo_urls_whitelist: vec![], }; @@ -1778,6 +2047,7 @@ fn instantiate_builder_unlock_contract(router: &mut App, owner: &Addr, astro_tok .unwrap() } +#[allow(clippy::too_many_arguments)] fn instantiate_assembly_contract( router: &mut App, owner: &Addr, @@ -1785,6 +2055,8 @@ fn instantiate_assembly_contract( vxastro: &Addr, builder: &Addr, delegator: Option, + generator_controller_addr: Option, + hub_addr: Option, ) -> Addr { let assembly_contract = Box::new(ContractWrapper::new_with_empty( astro_assembly::contract::execute, @@ -1799,6 +2071,8 @@ fn instantiate_assembly_contract( vxastro_token_addr: Some(vxastro.to_string()), voting_escrow_delegator_addr: delegator, ibc_controller: None, + generator_controller_addr, + hub_addr, builder_unlock_addr: builder.to_string(), proposal_voting_period: PROPOSAL_VOTING_PERIOD, proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, @@ -1821,44 +2095,6 @@ fn instantiate_assembly_contract( .unwrap() } -fn instantiate_delegator_contract(router: &mut App, owner: &Addr, vxastro: &Addr) -> Addr { - let nft_contract = Box::new(ContractWrapper::new_with_empty( - astroport_nft::contract::execute, - astroport_nft::contract::instantiate, - astroport_nft::contract::query, - )); - - let nft_code_id = router.store_code(nft_contract); - - let delegator_contract = Box::new( - ContractWrapper::new_with_empty( - voting_escrow_delegation::contract::execute, - voting_escrow_delegation::contract::instantiate, - voting_escrow_delegation::contract::query, - ) - .with_reply_empty(voting_escrow_delegation::contract::reply), - ); - - let delegator_code_id = router.store_code(delegator_contract); - - let msg = DelegatorInstantiateMsg { - owner: owner.to_string(), - nft_code_id, - voting_escrow_addr: vxastro.to_string(), - }; - - router - .instantiate_contract( - delegator_code_id, - owner.clone(), - &msg, - &[], - "Voting Escrow Delegator", - Some(owner.to_string()), - ) - .unwrap() -} - fn mint_tokens(app: &mut App, minter: &Addr, token: &Addr, recipient: &Addr, amount: u128) { let msg = Cw20ExecuteMsg::Mint { recipient: recipient.to_string(), @@ -1877,13 +2113,7 @@ fn mint_vxastro( recipient: Addr, amount: u128, ) { - mint_tokens( - app, - staking_instance, - &xastro.clone(), - &recipient.clone(), - amount, - ); + mint_tokens(app, staking_instance, &xastro, &recipient, amount); let msg = Cw20ExecuteMsg::Send { contract: vxastro.to_string(), @@ -1894,18 +2124,6 @@ fn mint_vxastro( app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); } -fn delegate_vxastro(app: &mut App, delegator_addr: Addr, from: Addr, to: Addr, bps: u16) { - let msg = DelegatorExecuteMsg::CreateDelegation { - bps, - expire_time: 2 * 7 * 86400, - token_id: format!("{}-{}-{}", from, to, bps), - recipient: to.to_string(), - }; - - app.execute_contract(from.clone(), delegator_addr, &msg, &[]) - .unwrap(); -} - fn create_allocations( app: &mut App, token: Addr, @@ -2019,8 +2237,30 @@ fn cast_vote( ) } +fn cast_outpost_vote( + app: &mut App, + assembly: Addr, + proposal_id: u64, + sender: Addr, + voter: Addr, + option: ProposalVoteOption, + voting_power: Uint128, +) -> anyhow::Result { + app.execute_contract( + sender, + assembly, + &ExecuteMsg::CastOutpostVote { + proposal_id, + voter: voter.to_string(), + vote: option, + voting_power, + }, + &[], + ) +} + fn change_owner(app: &mut App, contract: &Addr, assembly: &Addr) { - let msg = astroport_governance::voting_escrow::ExecuteMsg::ProposeNewOwner { + let msg = astroport_governance::voting_escrow_lite::ExecuteMsg::ProposeNewOwner { new_owner: assembly.to_string(), expires_in: 100, }; @@ -2030,7 +2270,7 @@ fn change_owner(app: &mut App, contract: &Addr, assembly: &Addr) { app.execute_contract( assembly.clone(), contract.clone(), - &astroport_governance::voting_escrow::ExecuteMsg::ClaimOwnership {}, + &astroport_governance::voting_escrow_lite::ExecuteMsg::ClaimOwnership {}, &[], ) .unwrap(); diff --git a/contracts/assembly/tests/integration.vxastro-full b/contracts/assembly/tests/integration.vxastro-full new file mode 100644 index 00000000..e023f256 --- /dev/null +++ b/contracts/assembly/tests/integration.vxastro-full @@ -0,0 +1,2517 @@ +use astro_assembly::astroport; +use astroport::{ + token::InstantiateMsg as TokenInstantiateMsg, xastro_token::QueryMsg as XAstroQueryMsg, +}; +use astroport_governance::assembly::{ + Config, Cw20HookMsg, ExecuteMsg, InstantiateMsg, Proposal, ProposalListResponse, + ProposalStatus, ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, + DEPOSIT_INTERVAL, VOTING_PERIOD_INTERVAL, +}; +use cosmwasm_std::coins; + +use std::str::FromStr; + +use astroport_governance::voting_escrow::{ + Cw20HookMsg as VXAstroCw20HookMsg, InstantiateMsg as VXAstroInstantiateMsg, +}; + +use astroport_governance::builder_unlock::msg::{ + InstantiateMsg as BuilderUnlockInstantiateMsg, ReceiveMsg as BuilderUnlockReceiveMsg, +}; +use astroport_governance::builder_unlock::{AllocationParams, Schedule}; +use astroport_governance::utils::{EPOCH_START, WEEK}; +use astroport_governance::voting_escrow_delegation::{ + ExecuteMsg as DelegatorExecuteMsg, InstantiateMsg as DelegatorInstantiateMsg, + QueryMsg as DelegatorQueryMsg, +}; +use cosmwasm_std::{ + testing::{mock_env, MockApi, MockStorage}, + to_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, + Uint64, WasmMsg, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20ExecuteMsg, MinterResponse}; +use cw_multi_test::{ + next_block, App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor, +}; + +const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); +const PROPOSAL_EFFECTIVE_DELAY: u64 = 12_342; +const PROPOSAL_EXPIRATION_PERIOD: u64 = 86_399; +const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); +const PROPOSAL_REQUIRED_QUORUM: &str = "0.50"; +const PROPOSAL_REQUIRED_THRESHOLD: &str = "0.60"; + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_contract_instantiation() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + // Instantiate needed contracts + let token_addr = instantiate_astro_token(&mut app, &owner); + let (_, xastro_token_addr) = instantiate_xastro_token(&mut app, &owner, &token_addr); + let vxastro_token_addr = instantiate_vxastro_token(&mut app, &owner, &xastro_token_addr); + let builder_unlock_addr = instantiate_builder_unlock_contract(&mut app, &owner, &token_addr); + + let assembly_contract = Box::new(ContractWrapper::new_with_empty( + astro_assembly::contract::execute, + astro_assembly::contract::instantiate, + astro_assembly::contract::query, + )); + + let assembly_code = app.store_code(assembly_contract); + + let assembly_default_instantiate_msg = InstantiateMsg { + xastro_token_addr: xastro_token_addr.to_string(), + vxastro_token_addr: Some(vxastro_token_addr.to_string()), + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller_addr: None, + hub_addr: None, + builder_unlock_addr: builder_unlock_addr.to_string(), + proposal_voting_period: PROPOSAL_VOTING_PERIOD, + proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, + proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, + proposal_required_deposit: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), + proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), + whitelisted_links: vec!["https://some.link/".to_string()], + }; + + // Try to instantiate assembly with wrong threshold + let err = app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_required_threshold: "0.3".to_string(), + ..assembly_default_instantiate_msg.clone() + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" + ); + + let err = app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_required_threshold: "1.1".to_string(), + ..assembly_default_instantiate_msg.clone() + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" + ); + + let err = app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_required_quorum: "1.1".to_string(), + ..assembly_default_instantiate_msg.clone() + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The required quorum for a proposal cannot be lower than 1% or higher than 100%" + ); + + let err = app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_expiration_period: 500, + ..assembly_default_instantiate_msg.clone() + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The expiration period for a proposal cannot be lower than 12342 or higher than 100800" + ); + + let err = app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_effective_delay: 400, + ..assembly_default_instantiate_msg.clone() + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The effective delay for a proposal cannot be lower than 6171 or higher than 14400" + ); + + let assembly_instance = app + .instantiate_contract( + assembly_code, + owner.clone(), + &assembly_default_instantiate_msg, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap(); + + let res: Config = app + .wrap() + .query_wasm_smart(assembly_instance, &QueryMsg::Config {}) + .unwrap(); + + assert_eq!(res.xastro_token_addr, xastro_token_addr); + assert_eq!(res.builder_unlock_addr, builder_unlock_addr); + assert_eq!(res.proposal_voting_period, PROPOSAL_VOTING_PERIOD); + assert_eq!(res.proposal_effective_delay, PROPOSAL_EFFECTIVE_DELAY); + assert_eq!(res.proposal_expiration_period, PROPOSAL_EXPIRATION_PERIOD); + assert_eq!( + res.proposal_required_deposit, + Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) + ); + assert_eq!( + res.proposal_required_quorum, + Decimal::from_str(PROPOSAL_REQUIRED_QUORUM).unwrap() + ); + assert_eq!( + res.proposal_required_threshold, + Decimal::from_str(PROPOSAL_REQUIRED_THRESHOLD).unwrap() + ); + assert_eq!( + res.whitelisted_links, + vec!["https://some.link/".to_string(),] + ); +} + +#[test] +fn test_proposal_submitting() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + let user = Addr::unchecked("user1"); + + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, false, false); + + let proposals: ProposalListResponse = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposals { + start: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposals.proposal_count, Uint64::from(0u32)); + assert_eq!(proposals.proposal_list, vec![]); + + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &user, + PROPOSAL_REQUIRED_DEPOSIT, + ); + + check_token_balance(&mut app, &xastro_addr, &user, PROPOSAL_REQUIRED_DEPOSIT); + + // Try to create proposal with insufficient token deposit + let submit_proposal_msg = Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("Title"), + description: String::from("Description"), + link: Some(String::from("https://some.link")), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT - 1), + }; + + let err = app + .execute_contract(user.clone(), xastro_addr.clone(), &submit_proposal_msg, &[]) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Insufficient token deposit!"); + + // Try to create a proposal with wrong title + let err = app + .execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("X"), + description: String::from("Description"), + link: Some(String::from("https://some.link/")), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Title too short!" + ); + + let err = app + .execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from_utf8(vec![b'X'; 65]).unwrap(), + description: String::from("Description"), + link: Some(String::from("https://some.link/")), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Title too long!" + ); + + // Try to create a proposal with wrong description + let err = app + .execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("Title"), + description: String::from("X"), + link: Some(String::from("https://some.link/")), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Description too short!" + ); + + let err = app + .execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("Title"), + description: String::from_utf8(vec![b'X'; 1025]).unwrap(), + link: Some(String::from("https://some.link/")), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Description too long!" + ); + + // Try to create a proposal with wrong link + let err = app + .execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("Title"), + description: String::from("Description"), + link: Some(String::from("X")), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Link too short!" + ); + + let err = app + .execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("Title"), + description: String::from("Description"), + link: Some(String::from_utf8(vec![b'X'; 129]).unwrap()), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Link too long!" + ); + + let err = app + .execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("Title"), + description: String::from("Description"), + link: Some(String::from("https://some1.link")), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Link is not whitelisted!" + ); + + let err = app + .execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("Title"), + description: String::from("Description"), + link: Some(String::from( + "https://some.link/", + )), + messages: None, + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Link is not properly formatted or contains unsafe characters!" + ); + + // Valid proposal submission + app.execute_contract( + user.clone(), + xastro_addr.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly_addr.to_string(), + msg: to_binary(&Cw20HookMsg::SubmitProposal { + title: String::from("Title"), + description: String::from("Description"), + link: Some(String::from("https://some.link/q/")), + messages: Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(750), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: None, + whitelist_remove: None, + }))) + .unwrap(), + funds: vec![], + })]), + ibc_channel: None, + }) + .unwrap(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + }, + &[], + ) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.proposal_id, Uint64::from(1u64)); + assert_eq!(proposal.submitter, user); + assert_eq!(proposal.status, ProposalStatus::Active); + assert_eq!(proposal.for_power, Uint128::zero()); + assert_eq!(proposal.against_power, Uint128::zero()); + assert_eq!(proposal.start_block, 12_345); + assert_eq!(proposal.end_block, 12_345 + PROPOSAL_VOTING_PERIOD); + assert_eq!(proposal.title, String::from("Title")); + assert_eq!(proposal.description, String::from("Description")); + assert_eq!(proposal.link, Some(String::from("https://some.link/q/"))); + assert_eq!( + proposal.messages, + Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(750), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: None, + whitelist_remove: None, + }))) + .unwrap(), + funds: vec![], + })]) + ); + assert_eq!( + proposal.deposit_amount, + Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) + ) +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_successful_proposal() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let ( + token_addr, + staking_instance, + xastro_addr, + vxastro_addr, + builder_unlock_addr, + assembly_addr, + _, + ) = instantiate_contracts(&mut app, owner, false, false, false); + + // Init voting power for users + let balances: Vec<(&str, u128, u128)> = vec![ + ("user0", PROPOSAL_REQUIRED_DEPOSIT, 0), // proposal submitter + ("user1", 20, 80), + ("user2", 100, 100), + ("user3", 300, 100), + ("user4", 200, 50), + ("user5", 0, 90), + ("user6", 100, 200), + ("user7", 30, 0), + ("user8", 80, 100), + ("user9", 50, 0), + ("user10", 0, 90), + ("user11", 500, 0), + ("user12", 10000_000000, 0), + ]; + + let default_allocation_params = AllocationParams { + amount: Uint128::zero(), + unlock_schedule: Schedule { + start_time: 12_345, + cliff: 5, + duration: 500, + }, + proposed_receiver: None, + }; + + let locked_balances = vec![ + ( + "user1".to_string(), + AllocationParams { + amount: Uint128::from(80u32), + ..default_allocation_params.clone() + }, + ), + ( + "user4".to_string(), + AllocationParams { + amount: Uint128::from(50u32), + ..default_allocation_params.clone() + }, + ), + ( + "user7".to_string(), + AllocationParams { + amount: Uint128::from(100u32), + ..default_allocation_params.clone() + }, + ), + ( + "user10".to_string(), + AllocationParams { + amount: Uint128::from(30u32), + ..default_allocation_params + }, + ), + ]; + + for (addr, xastro, vxastro) in balances { + if xastro > 0 { + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked(addr), + xastro, + ); + } + + if vxastro > 0 { + mint_vxastro( + &mut app, + &staking_instance, + xastro_addr.clone(), + &vxastro_addr, + Addr::unchecked(addr), + vxastro, + ); + } + } + + create_allocations(&mut app, token_addr, builder_unlock_addr, locked_balances); + + // Skip period + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create default proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(PROPOSAL_VOTING_PERIOD + 1000), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: Some(vec![ + "https://some1.link/".to_string(), + "https://some2.link/".to_string(), + ]), + whitelist_remove: Some(vec!["https://some.link/".to_string()]), + }))) + .unwrap(), + funds: vec![], + })]), + ); + + let votes: Vec<(&str, ProposalVoteOption, u128)> = vec![ + ("user1", ProposalVoteOption::For, 280u128), + ("user2", ProposalVoteOption::For, 350u128), + ("user3", ProposalVoteOption::For, 550u128), + ("user4", ProposalVoteOption::For, 350u128), + ("user5", ProposalVoteOption::For, 240u128), + ("user6", ProposalVoteOption::For, 600u128), + ("user7", ProposalVoteOption::For, 130u128), + ("user8", ProposalVoteOption::Against, 330u128), + ("user9", ProposalVoteOption::Against, 50u128), + ("user10", ProposalVoteOption::Against, 270u128), + ("user11", ProposalVoteOption::Against, 500u128), + ("user12", ProposalVoteOption::For, 10000_000000u128), + ]; + + check_total_vp(&mut app, &assembly_addr, 1, 20000003650); + + for (addr, option, expected_vp) in votes { + let sender = Addr::unchecked(addr); + + check_user_vp(&mut app, &assembly_addr, &sender, 1, expected_vp); + + cast_vote(&mut app, assembly_addr.clone(), 1, sender, option).unwrap(); + } + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + let proposal_votes: ProposalVotesResponse = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::ProposalVotes { proposal_id: 1 }, + ) + .unwrap(); + + let proposal_for_voters: Vec = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::ProposalVoters { + proposal_id: 1, + vote_option: ProposalVoteOption::For, + start: None, + limit: None, + }, + ) + .unwrap(); + + let proposal_against_voters: Vec = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::ProposalVoters { + proposal_id: 1, + vote_option: ProposalVoteOption::Against, + start: None, + limit: None, + }, + ) + .unwrap(); + + // Check proposal votes + assert_eq!(proposal.for_power, Uint128::from(10000002500u128)); + assert_eq!(proposal.against_power, Uint128::from(1150u32)); + + assert_eq!(proposal_votes.for_power, Uint128::from(10000002500u128)); + assert_eq!(proposal_votes.against_power, Uint128::from(1150u32)); + + assert_eq!( + proposal_for_voters, + vec![ + Addr::unchecked("user1"), + Addr::unchecked("user2"), + Addr::unchecked("user3"), + Addr::unchecked("user4"), + Addr::unchecked("user5"), + Addr::unchecked("user6"), + Addr::unchecked("user7"), + Addr::unchecked("user12"), + ] + ); + assert_eq!( + proposal_against_voters, + vec![ + Addr::unchecked("user8"), + Addr::unchecked("user9"), + Addr::unchecked("user10"), + Addr::unchecked("user11") + ] + ); + + // Skip voting period + app.update_block(|bi| { + bi.height += PROPOSAL_VOTING_PERIOD + 1; + bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); + }); + + // Try to vote after voting period + let err = cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user11"), + ProposalVoteOption::Against, + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Voting period ended!"); + + // Try to execute the proposal before end_proposal + let err = app + .execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, + &[], + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Proposal not passed!"); + + // Check the successful completion of the proposal + check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); + + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::EndProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + check_token_balance( + &mut app, + &xastro_addr, + &Addr::unchecked("user0"), + PROPOSAL_REQUIRED_DEPOSIT, + ); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.status, ProposalStatus::Passed); + + // Try to end proposal again + let err = app + .execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::EndProposal { proposal_id: 1 }, + &[], + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Proposal not active!"); + + // Try to execute the proposal before the delay + let err = app + .execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, + &[], + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Proposal delay not ended!"); + + // Skip blocks + app.update_block(|bi| { + bi.height += PROPOSAL_EFFECTIVE_DELAY + 1; + bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EFFECTIVE_DELAY + 1)); + }); + + // Try to execute the proposal after the delay + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let config: Config = app + .wrap() + .query_wasm_smart(assembly_addr.to_string(), &QueryMsg::Config {}) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.to_string(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + // Check execution result + assert_eq!(config.proposal_voting_period, PROPOSAL_VOTING_PERIOD + 1000); + assert_eq!( + config.whitelisted_links, + vec![ + "https://some1.link/".to_string(), + "https://some2.link/".to_string(), + ] + ); + assert_eq!(proposal.status, ProposalStatus::Executed); + + // Try to remove proposal before expiration period + let err = app + .execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, + &[], + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Proposal not completed!"); + + // Remove expired proposal + app.update_block(|bi| { + bi.height += PROPOSAL_EXPIRATION_PERIOD + 1; + bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + 1)); + }); + + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let res: ProposalListResponse = app + .wrap() + .query_wasm_smart( + assembly_addr.to_string(), + &QueryMsg::Proposals { + start: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(res.proposal_list, vec![]); + // proposal_count should not be changed after removing a proposal + assert_eq!(res.proposal_count, Uint64::from(1u32)); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_successful_emissions_proposal() { + use cosmwasm_std::{coins, BankMsg}; + + let mut app = mock_app(); + let owner = Addr::unchecked("generator_controller"); + + let (_, _, _, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, true, false); + + // Provide some funds to the Assembly contract to use in the proposal messages + app.init_modules(|router, _, storage| { + router.bank.init_balance( + storage, + &Addr::unchecked(assembly_addr.clone()), + coins(1000, "uluna"), + ) + }) + .unwrap(); + + let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { + title: "Emissions Test title".to_string(), + description: "Emissions Test description".to_string(), + // Sample message to use as we don't have IBC or the Generator to set emissions on + messages: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: "generator_controller".into(), + amount: coins(1, "uluna"), + })], + ibc_channel: None, + }; + + app.execute_contract( + Addr::unchecked("generator_controller"), + assembly_addr.clone(), + &emissions_proposal_msg, + &[], + ) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart(assembly_addr, &QueryMsg::Proposal { proposal_id: 1 }) + .unwrap(); + + assert_eq!(proposal.status, ProposalStatus::Executed); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_no_generator_controller_emissions_proposal() { + let mut app = mock_app(); + let owner = Addr::unchecked("generator_controller"); + + let (_, _, _, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, false, false); + let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { + title: "Emissions Test title!".to_string(), + description: "Emissions Test description!".to_string(), + messages: vec![], + ibc_channel: None, + }; + + let err = app + .execute_contract( + Addr::unchecked("generator_controller"), + assembly_addr, + &emissions_proposal_msg, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Sender is not the Generator controller installed in the assembly" + ); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_empty_messages_emissions_proposal() { + let mut app = mock_app(); + let owner = Addr::unchecked("generator_controller"); + + let (_, _, _, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, true, false); + let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { + title: "Emissions Test title!".to_string(), + description: "Emissions Test description!".to_string(), + messages: vec![], + ibc_channel: None, + }; + + let err = app + .execute_contract( + Addr::unchecked("generator_controller"), + assembly_addr, + &emissions_proposal_msg, + &[], + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "The proposal has no messages to execute" + ); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_unauthorised_emissions_proposal() { + use cosmwasm_std::BankMsg; + + let mut app = mock_app(); + let owner = Addr::unchecked("generator_controller"); + + let (_, _, _, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, true, false); + let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { + title: "Emissions Test title!".to_string(), + description: "Emissions Test description!".to_string(), + // Sample message to use as we don't have IBC or the Generator to set emissions on + messages: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: "generator_controller".into(), + amount: coins(1, "uluna"), + })], + ibc_channel: None, + }; + + let err = app + .execute_contract( + Addr::unchecked("not_generator_controller"), + assembly_addr, + &emissions_proposal_msg, + &[], + ) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Unauthorized"); +} + +#[test] +fn test_voting_power_changes() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, false, false); + + // Mint tokens for submitting proposal + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user0"), + PROPOSAL_REQUIRED_DEPOSIT, + ); + + // Mint tokens for casting votes at start block + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user1"), + 40000_000000, + ); + + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(750), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: None, + whitelist_remove: None, + }))) + .unwrap(), + funds: vec![], + })]), + ); + // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user2"), + 5000_000000, + ); + + app.update_block(next_block); + + // user1 can vote as he had voting power before the proposal submitting. + cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user1"), + ProposalVoteOption::For, + ) + .unwrap(); + // Should panic, because user2 doesn't have any voting power. + let err = cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user2"), + ProposalVoteOption::Against, + ) + .unwrap_err(); + + // user2 doesn't have voting power and doesn't affect on total voting power(total supply at) + // total supply = 5000 + assert_eq!( + err.root_cause().to_string(), + "You don't have any voting power!" + ); + + app.update_block(next_block); + + // Skip voting period and delay + app.update_block(|bi| { + bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; + bi.time = bi + .time + .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); + }); + + // End proposal + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::EndProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + // Check proposal votes + assert_eq!(proposal.for_power, Uint128::from(40000_000000u128)); + assert_eq!(proposal.against_power, Uint128::zero()); + // Should be passed, as total_voting_power=5000, for_votes=40000. + // So user2 didn't affect the result. Because he had to have xASTRO before the vote was submitted. + assert_eq!(proposal.status, ProposalStatus::Passed); +} + +#[test] +fn test_fail_outpost_vote_without_hub() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, false, false); + + // Mint tokens for submitting proposal + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user0"), + PROPOSAL_REQUIRED_DEPOSIT, + ); + + // Mint tokens for casting votes at start block + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user1"), + 40000_000000, + ); + + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(750), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: None, + whitelist_remove: None, + }))) + .unwrap(), + funds: vec![], + })]), + ); + // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user2"), + 5000_000000, + ); + + app.update_block(next_block); + + // user1 can not vote from an Outpost due to no Hub contract set + let err = cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("invalid_contract"), + Addr::unchecked("user1"), + ProposalVoteOption::For, + Uint128::from(100u64), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Sender is not the Hub installed in the assembly" + ); +} + +#[test] +fn test_outpost_vote() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner.clone(), false, false, true); + + let user1_voting_power = 10_000_000_000; + let user2_voting_power = 5_000_000_000; + let remote_user1_voting_power = 80_000_000_000u128; + let remote_user2_voting_power = 3_000_000_000u128; + + // Mint tokens for submitting proposal + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user0"), + PROPOSAL_REQUIRED_DEPOSIT, + ); + + // Mint tokens for casting votes at start block + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user1"), + user1_voting_power, + ); + + // Mint tokens for casting votes against vote at start block + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user2"), + user2_voting_power, + ); + + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(750), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: None, + whitelist_remove: None, + }))) + .unwrap(), + funds: vec![], + })]), + ); + + app.update_block(next_block); + + // Outpost votes won't be accepted from other addresses + let err = cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("other_contract"), + Addr::unchecked("remote1"), + ProposalVoteOption::For, + Uint128::from(remote_user1_voting_power), + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + owner.clone(), + Addr::unchecked("remote1"), + ProposalVoteOption::For, + Uint128::from(remote_user1_voting_power), + ) + .unwrap(); + + cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + owner.clone(), + Addr::unchecked("remote2"), + ProposalVoteOption::Against, + Uint128::from(remote_user2_voting_power), + ) + .unwrap(); + + let err = cast_outpost_vote( + &mut app, + assembly_addr.clone(), + 1, + owner, + Addr::unchecked("remote1"), + ProposalVoteOption::For, + Uint128::from(remote_user2_voting_power), + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "User already voted!"); + + // user1 can vote as he had voting power before the proposal submitting. + cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user1"), + ProposalVoteOption::For, + ) + .unwrap(); + + let err = cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user1"), + ProposalVoteOption::For, + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "User already voted!"); + + // user2 can vote as he had voting power before the proposal submitting. + cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked("user2"), + ProposalVoteOption::Against, + ) + .unwrap(); + + app.update_block(next_block); + + // Skip voting period and delay + app.update_block(|bi| { + bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; + bi.time = bi + .time + .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); + }); + + // End proposal + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::EndProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + // Check proposal votes, Outpost and Hub votes should be counted + let total_for_voting_power = user1_voting_power + remote_user1_voting_power; + let total_against_voting_power = user2_voting_power + remote_user2_voting_power; + assert_eq!(proposal.for_power, Uint128::from(total_for_voting_power)); + assert_eq!( + proposal.against_power, + Uint128::from(total_against_voting_power) + ); + // Should be passed + assert_eq!(proposal.status, ProposalStatus::Passed); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_block_height_selection() { + // Block height is 12345 after app initialization + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + let user1 = Addr::unchecked("user1"); + let user2 = Addr::unchecked("user2"); + let user3 = Addr::unchecked("user3"); + + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, false, false); + + // Mint tokens for submitting proposal + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user0"), + PROPOSAL_REQUIRED_DEPOSIT, + ); + + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &user1, + 6000_000001, + ); + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &user2, + 4000_000000, + ); + + // Skip to the next period + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + None, + ); + + cast_vote( + &mut app, + assembly_addr.clone(), + 1, + user1, + ProposalVoteOption::For, + ) + .unwrap(); + + // Mint huge amount of xASTRO. These tokens cannot affect on total supply in proposal 1 because + // they were minted after proposal.start_block - 1 + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &user3, + 100000_000000, + ); + // Mint more xASTRO to user2, who will vote against the proposal, what is enough to make proposal unsuccessful. + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &user2, + 3000_000000, + ); + // Total voting power should be 20k xASTRO (proposal minimum deposit 10k + 4k + 6k users VP) + check_total_vp(&mut app, &assembly_addr, 1, 20000_000001); + + cast_vote( + &mut app, + assembly_addr.clone(), + 1, + user2, + ProposalVoteOption::Against, + ) + .unwrap(); + + // Skip voting period + app.update_block(|bi| { + bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; + bi.time = bi + .time + .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); + }); + + // End proposal + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::EndProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.for_power, Uint128::new(6000_000001)); + // Against power is 4000, as user2's balance was increased after proposal.start_block - 1 + // at which everyone's voting power are considered. + assert_eq!(proposal.against_power, Uint128::new(4000_000000)); + // Proposal is passed, as the total supply was increased after proposal.start_block - 1. + assert_eq!(proposal.status, ProposalStatus::Passed); +} + +#[cfg(not(feature = "testnet"))] +#[test] +fn test_unsuccessful_proposal() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, false, false); + + // Init voting power for users + let xastro_balances: Vec<(&str, u128)> = vec![ + ("user0", PROPOSAL_REQUIRED_DEPOSIT), // proposal submitter + ("user1", 100), + ("user2", 200), + ("user3", 400), + ("user4", 250), + ("user5", 90), + ("user6", 300), + ("user7", 30), + ("user8", 180), + ("user9", 50), + ("user10", 90), + ("user11", 500), + ]; + + for (addr, xastro) in xastro_balances { + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked(addr), + xastro, + ); + } + + // Skip period + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + None, + ); + + let expected_voting_power: Vec<(&str, ProposalVoteOption)> = vec![ + ("user1", ProposalVoteOption::For), + ("user2", ProposalVoteOption::For), + ("user3", ProposalVoteOption::For), + ("user4", ProposalVoteOption::Against), + ("user5", ProposalVoteOption::Against), + ("user6", ProposalVoteOption::Against), + ("user7", ProposalVoteOption::Against), + ("user8", ProposalVoteOption::Against), + ("user9", ProposalVoteOption::Against), + ("user10", ProposalVoteOption::Against), + ]; + + for (addr, option) in expected_voting_power { + cast_vote( + &mut app, + assembly_addr.clone(), + 1, + Addr::unchecked(addr), + option, + ) + .unwrap(); + } + + // Skip voting period + app.update_block(|bi| { + bi.height += PROPOSAL_VOTING_PERIOD + 1; + bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); + }); + + // Check balance of submitter before and after proposal completion + check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); + + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::EndProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + check_token_balance( + &mut app, + &xastro_addr, + &Addr::unchecked("user0"), + 10000_000000, + ); + + // Check proposal status + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.status, ProposalStatus::Rejected); + + // Remove expired proposal + app.update_block(|bi| { + bi.height += PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; + bi.time = bi + .time + .plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); + }); + + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let res: ProposalListResponse = app + .wrap() + .query_wasm_smart( + assembly_addr.to_string(), + &QueryMsg::Proposals { + start: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(res.proposal_list, vec![]); + // proposal_count should not be changed after removing + assert_eq!(res.proposal_count, Uint64::from(1u32)); +} + +#[test] +fn test_check_messages() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let (_, _, _, vxastro_addr, _, assembly_addr, _) = + instantiate_contracts(&mut app, owner, false, false, false); + + change_owner(&mut app, &vxastro_addr, &assembly_addr); + let user = Addr::unchecked("user"); + let into_check_msg = |msgs: Vec<(String, Binary)>| { + let messages = msgs + .into_iter() + .map(|(contract_addr, msg)| { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds: vec![], + }) + }) + .collect(); + ExecuteMsg::CheckMessages { messages } + }; + + let config_before: astroport_governance::voting_escrow::ConfigResponse = app + .wrap() + .query_wasm_smart( + &vxastro_addr, + &astroport_governance::voting_escrow::QueryMsg::Config {}, + ) + .unwrap(); + + let vxastro_blacklist_msg = vec![( + vxastro_addr.to_string(), + to_binary( + &astroport_governance::voting_escrow::ExecuteMsg::UpdateConfig { new_guardian: None }, + ) + .unwrap(), + )]; + let err = app + .execute_contract( + user, + assembly_addr.clone(), + &into_check_msg(vxastro_blacklist_msg), + &[], + ) + .unwrap_err(); + assert_eq!( + &err.root_cause().to_string(), + "Messages check passed. Nothing was committed to the blockchain" + ); + + let config_after: astroport_governance::voting_escrow::ConfigResponse = app + .wrap() + .query_wasm_smart( + &vxastro_addr, + &astroport_governance::voting_escrow::QueryMsg::Config {}, + ) + .unwrap(); + assert_eq!(config_before, config_after); +} + +#[test] +fn test_delegated_vp() { + let mut app = mock_app(); + + let owner = Addr::unchecked("owner"); + + let (_, staking_instance, xastro_addr, vxastro_addr, _, assembly_addr, delegator) = + instantiate_contracts(&mut app, owner, true, false, false); + let delegator = delegator.unwrap(); + + let users = vec![ + ( + "user1", + 103_000_000_000u128, + 1000u16, + "user4", + 177_278_846_150u128, + ), + ( + "user2", + 612_000_000_000u128, + 2000u16, + "user5", + 1_053_346_153_800u128, + ), + ( + "user3", + 205_000_000_000u128, + 3000u16, + "user6", + 352_836_538_450u128, + ), + ]; + + // Mint tokens for submitting proposal + mint_tokens( + &mut app, + &staking_instance, + &xastro_addr, + &Addr::unchecked("user0"), + PROPOSAL_REQUIRED_DEPOSIT, + ); + + // Mint vxASTRO and delegate it to the other users + for (from, amount, bps, to, exp_vp) in users { + mint_vxastro( + &mut app, + &staking_instance, + xastro_addr.clone(), + &vxastro_addr, + Addr::unchecked(from), + amount, + ); + delegate_vxastro( + &mut app, + delegator.clone(), + Addr::unchecked(from), + Addr::unchecked(to), + bps, + ); + + let from_amount: Uint128 = app + .wrap() + .query_wasm_smart( + &delegator, + &DelegatorQueryMsg::AdjustedBalance { + account: from.to_string(), + timestamp: None, + }, + ) + .unwrap(); + + let to_amount: Uint128 = app + .wrap() + .query_wasm_smart( + &delegator, + &DelegatorQueryMsg::AdjustedBalance { + account: to.to_string(), + timestamp: None, + }, + ) + .unwrap(); + + assert_eq!(from_amount + to_amount, Uint128::from(exp_vp)); + } + + app.update_block(|mut block| { + block.time = block.time.plus_seconds(WEEK); + block.height += WEEK / 5; + }); + + // Create proposal + create_proposal( + &mut app, + &xastro_addr, + &assembly_addr, + Addr::unchecked("user0"), + Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: assembly_addr.to_string(), + msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_token_addr: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: Some(750), + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_add: None, + whitelist_remove: None, + }))) + .unwrap(), + funds: vec![], + })]), + ); + + let votes: Vec<(&str, ProposalVoteOption)> = vec![ + ("user1", ProposalVoteOption::Against), + ("user2", ProposalVoteOption::For), + ("user3", ProposalVoteOption::Against), + ("user4", ProposalVoteOption::For), + ("user5", ProposalVoteOption::Against), + ("user6", ProposalVoteOption::For), + ]; + + for (user, vote) in votes { + cast_vote( + &mut app, + assembly_addr.clone(), + 1u64, + Addr::unchecked(user), + vote, + ) + .unwrap(); + } + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.for_power, Uint128::from(1_578_255_769_188u128)); + assert_eq!(proposal.against_power, Uint128::from(925_205_769_212u128)); + + // Skip voting period + app.update_block(|bi| { + bi.height += PROPOSAL_VOTING_PERIOD + 1; + bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); + }); + + app.execute_contract( + Addr::unchecked("user0"), + assembly_addr.clone(), + &ExecuteMsg::EndProposal { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: Proposal = app + .wrap() + .query_wasm_smart( + assembly_addr.clone(), + &QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.status, ProposalStatus::Passed); +} + +fn mock_app() -> App { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(EPOCH_START); + let api = MockApi::default(); + let bank = BankKeeper::new(); + let storage = MockStorage::new(); + + AppBuilder::new() + .with_api(api) + .with_block(env.block) + .with_bank(bank) + .with_storage(storage) + .build(|_, _, _| {}) +} + +fn instantiate_contracts( + router: &mut App, + owner: Addr, + with_delegator: bool, + with_generator_controller: bool, + with_hub: bool, +) -> (Addr, Addr, Addr, Addr, Addr, Addr, Option) { + let token_addr = instantiate_astro_token(router, &owner); + let (staking_addr, xastro_token_addr) = instantiate_xastro_token(router, &owner, &token_addr); + let vxastro_token_addr = instantiate_vxastro_token(router, &owner, &xastro_token_addr); + let builder_unlock_addr = instantiate_builder_unlock_contract(router, &owner, &token_addr); + + let mut delegator_addr = None; + + if with_delegator { + delegator_addr = Some(instantiate_delegator_contract( + router, + &owner, + &vxastro_token_addr, + )); + } + + // If we want to test immediate proposals we need to set the address + // for the generator controller. Deploying the generator controller in this + // test would require deploying factory, tokens and pools. That test is + // better suited in the generator controller itself. Thus, we use the owner + // address as the generator controller address to test immediate proposals. + let mut generator_controller_addr = None; + + if with_generator_controller { + generator_controller_addr = Some(owner.to_string()); + } + + let mut hub_addr = None; + + if with_hub { + hub_addr = Some(owner.to_string()); + } + + let assembly_addr = instantiate_assembly_contract( + router, + &owner, + &xastro_token_addr, + &vxastro_token_addr, + &builder_unlock_addr, + delegator_addr.clone().map(String::from), + generator_controller_addr, + hub_addr, + ); + + ( + token_addr, + staking_addr, + xastro_token_addr, + vxastro_token_addr, + builder_unlock_addr, + assembly_addr, + delegator_addr, + ) +} + +fn instantiate_astro_token(router: &mut App, owner: &Addr) -> Addr { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + let astro_token_code_id = router.store_code(astro_token_contract); + + let msg = TokenInstantiateMsg { + name: String::from("Astro token"), + symbol: String::from("ASTRO"), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + router + .instantiate_contract( + astro_token_code_id, + owner.clone(), + &msg, + &[], + String::from("ASTRO"), + None, + ) + .unwrap() +} + +fn instantiate_xastro_token(router: &mut App, owner: &Addr, astro_token: &Addr) -> (Addr, Addr) { + let xastro_contract = Box::new(ContractWrapper::new_with_empty( + astroport_xastro_token::contract::execute, + astroport_xastro_token::contract::instantiate, + astroport_xastro_token::contract::query, + )); + + let xastro_code_id = router.store_code(xastro_contract); + + let staking_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_staking::contract::execute, + astroport_staking::contract::instantiate, + astroport_staking::contract::query, + ) + .with_reply_empty(astroport_staking::contract::reply), + ); + + let staking_code_id = router.store_code(staking_contract); + + let msg = astroport::staking::InstantiateMsg { + owner: owner.to_string(), + token_code_id: xastro_code_id, + deposit_token_addr: astro_token.to_string(), + marketing: None, + }; + let staking_instance = router + .instantiate_contract( + staking_code_id, + owner.clone(), + &msg, + &[], + String::from("xASTRO"), + None, + ) + .unwrap(); + + let res = router + .wrap() + .query::(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: staking_instance.to_string(), + msg: to_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), + })) + .unwrap(); + + (staking_instance, res.share_token_addr) +} + +fn instantiate_vxastro_token(router: &mut App, owner: &Addr, xastro: &Addr) -> Addr { + let vxastro_token_contract = Box::new(ContractWrapper::new_with_empty( + voting_escrow::contract::execute, + voting_escrow::contract::instantiate, + voting_escrow::contract::query, + )); + + let vxastro_token_code_id = router.store_code(vxastro_token_contract); + + let msg = VXAstroInstantiateMsg { + owner: owner.to_string(), + guardian_addr: Some(owner.to_string()), + deposit_token_addr: xastro.to_string(), + marketing: None, + logo_urls_whitelist: vec![], + }; + + router + .instantiate_contract( + vxastro_token_code_id, + owner.clone(), + &msg, + &[], + String::from("vxASTRO"), + None, + ) + .unwrap() +} + +fn instantiate_builder_unlock_contract(router: &mut App, owner: &Addr, astro_token: &Addr) -> Addr { + let builder_unlock_contract = Box::new(ContractWrapper::new_with_empty( + builder_unlock::contract::execute, + builder_unlock::contract::instantiate, + builder_unlock::contract::query, + )); + + let builder_unlock_code_id = router.store_code(builder_unlock_contract); + + let msg = BuilderUnlockInstantiateMsg { + owner: owner.to_string(), + astro_token: astro_token.to_string(), + max_allocations_amount: Uint128::new(300_000_000_000_000u128), + }; + + router + .instantiate_contract( + builder_unlock_code_id, + owner.clone(), + &msg, + &[], + "Builder Unlock contract".to_string(), + Some(owner.to_string()), + ) + .unwrap() +} + +#[allow(clippy::too_many_arguments)] +fn instantiate_assembly_contract( + router: &mut App, + owner: &Addr, + xastro: &Addr, + vxastro: &Addr, + builder: &Addr, + delegator: Option, + generator_controller_addr: Option, + hub_addr: Option, +) -> Addr { + let assembly_contract = Box::new(ContractWrapper::new_with_empty( + astro_assembly::contract::execute, + astro_assembly::contract::instantiate, + astro_assembly::contract::query, + )); + + let assembly_code = router.store_code(assembly_contract); + + let msg = InstantiateMsg { + xastro_token_addr: xastro.to_string(), + vxastro_token_addr: Some(vxastro.to_string()), + voting_escrow_delegator_addr: delegator, + ibc_controller: None, + generator_controller_addr, + hub_addr, + builder_unlock_addr: builder.to_string(), + proposal_voting_period: PROPOSAL_VOTING_PERIOD, + proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, + proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, + proposal_required_deposit: Uint128::new(PROPOSAL_REQUIRED_DEPOSIT), + proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), + proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), + whitelisted_links: vec!["https://some.link/".to_string()], + }; + + router + .instantiate_contract( + assembly_code, + owner.clone(), + &msg, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap() +} + +fn instantiate_delegator_contract(router: &mut App, owner: &Addr, vxastro: &Addr) -> Addr { + let nft_contract = Box::new(ContractWrapper::new_with_empty( + astroport_nft::contract::execute, + astroport_nft::contract::instantiate, + astroport_nft::contract::query, + )); + + let nft_code_id = router.store_code(nft_contract); + + let delegator_contract = Box::new( + ContractWrapper::new_with_empty( + voting_escrow_delegation::contract::execute, + voting_escrow_delegation::contract::instantiate, + voting_escrow_delegation::contract::query, + ) + .with_reply_empty(voting_escrow_delegation::contract::reply), + ); + + let delegator_code_id = router.store_code(delegator_contract); + + let msg = DelegatorInstantiateMsg { + owner: owner.to_string(), + nft_code_id, + voting_escrow_addr: vxastro.to_string(), + }; + + router + .instantiate_contract( + delegator_code_id, + owner.clone(), + &msg, + &[], + "Voting Escrow Delegator", + Some(owner.to_string()), + ) + .unwrap() +} + +fn mint_tokens(app: &mut App, minter: &Addr, token: &Addr, recipient: &Addr, amount: u128) { + let msg = Cw20ExecuteMsg::Mint { + recipient: recipient.to_string(), + amount: Uint128::from(amount), + }; + + app.execute_contract(minter.clone(), token.to_owned(), &msg, &[]) + .unwrap(); +} + +fn mint_vxastro( + app: &mut App, + staking_instance: &Addr, + xastro: Addr, + vxastro: &Addr, + recipient: Addr, + amount: u128, +) { + mint_tokens(app, staking_instance, &xastro, &recipient, amount); + + let msg = Cw20ExecuteMsg::Send { + contract: vxastro.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), + }; + + app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); +} + +fn delegate_vxastro(app: &mut App, delegator_addr: Addr, from: Addr, to: Addr, bps: u16) { + let msg = DelegatorExecuteMsg::CreateDelegation { + bps, + expire_time: 2 * 7 * 86400, + token_id: format!("{}-{}-{}", from, to, bps), + recipient: to.to_string(), + }; + + app.execute_contract(from, delegator_addr, &msg, &[]) + .unwrap(); +} + +fn create_allocations( + app: &mut App, + token: Addr, + builder_unlock_contract_addr: Addr, + allocations: Vec<(String, AllocationParams)>, +) { + let amount = allocations + .iter() + .map(|params| params.1.amount.u128()) + .sum(); + + mint_tokens( + app, + &Addr::unchecked("owner"), + &token, + &Addr::unchecked("owner"), + amount, + ); + + app.execute_contract( + Addr::unchecked("owner"), + Addr::unchecked(token.to_string()), + &Cw20ExecuteMsg::Send { + contract: builder_unlock_contract_addr.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }).unwrap(), + }, + &[], + ) + .unwrap(); +} + +fn create_proposal( + app: &mut App, + token: &Addr, + assembly: &Addr, + submitter: Addr, + msgs: Option>, +) { + let submit_proposal_msg = Cw20HookMsg::SubmitProposal { + title: "Test title!".to_string(), + description: "Test description!".to_string(), + link: None, + messages: msgs, + ibc_channel: None, + }; + + app.execute_contract( + submitter, + token.clone(), + &Cw20ExecuteMsg::Send { + contract: assembly.to_string(), + amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + msg: to_binary(&submit_proposal_msg).unwrap(), + }, + &[], + ) + .unwrap(); +} + +fn check_token_balance(app: &mut App, token: &Addr, address: &Addr, expected: u128) { + let msg = XAstroQueryMsg::Balance { + address: address.to_string(), + }; + let res: StdResult = app.wrap().query_wasm_smart(token, &msg); + assert_eq!(res.unwrap().balance, Uint128::from(expected)); +} + +fn check_user_vp(app: &mut App, assembly: &Addr, address: &Addr, proposal_id: u64, expected: u128) { + let res: Uint128 = app + .wrap() + .query_wasm_smart( + assembly.to_string(), + &QueryMsg::UserVotingPower { + user: address.to_string(), + proposal_id, + }, + ) + .unwrap(); + + assert_eq!(res.u128(), expected); +} + +fn check_total_vp(app: &mut App, assembly: &Addr, proposal_id: u64, expected: u128) { + let res: Uint128 = app + .wrap() + .query_wasm_smart( + assembly.to_string(), + &QueryMsg::TotalVotingPower { proposal_id }, + ) + .unwrap(); + + assert_eq!(res.u128(), expected); +} + +fn cast_vote( + app: &mut App, + assembly: Addr, + proposal_id: u64, + sender: Addr, + option: ProposalVoteOption, +) -> anyhow::Result { + app.execute_contract( + sender, + assembly, + &ExecuteMsg::CastVote { + proposal_id, + vote: option, + }, + &[], + ) +} + +fn cast_outpost_vote( + app: &mut App, + assembly: Addr, + proposal_id: u64, + sender: Addr, + voter: Addr, + option: ProposalVoteOption, + voting_power: Uint128, +) -> anyhow::Result { + app.execute_contract( + sender, + assembly, + &ExecuteMsg::CastOutpostVote { + proposal_id, + voter: voter.to_string(), + vote: option, + voting_power, + }, + &[], + ) +} + +fn change_owner(app: &mut App, contract: &Addr, assembly: &Addr) { + let msg = astroport_governance::voting_escrow::ExecuteMsg::ProposeNewOwner { + new_owner: assembly.to_string(), + expires_in: 100, + }; + app.execute_contract(Addr::unchecked("owner"), contract.clone(), &msg, &[]) + .unwrap(); + + app.execute_contract( + assembly.clone(), + contract.clone(), + &astroport_governance::voting_escrow::ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); +} diff --git a/contracts/generator_controller_lite/.cargo/config b/contracts/generator_controller_lite/.cargo/config new file mode 100644 index 00000000..8d4bc738 --- /dev/null +++ b/contracts/generator_controller_lite/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/generator_controller_lite/Cargo.toml b/contracts/generator_controller_lite/Cargo.toml new file mode 100644 index 00000000..e95f2e33 --- /dev/null +++ b/contracts/generator_controller_lite/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "generator-controller-lite" +version = "1.0.0" +authors = ["Astroport"] +edition = "2021" +repository = "https://github.com/astroport-fi/astroport-governance" +homepage = "https://astroport.fi" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cw2 = "0.15" +cw20 = "0.15" +cosmwasm-std = "1.1" +cw-storage-plus = "0.15" +thiserror = { version = "1.0" } +itertools = "0.10" +astroport-governance = { path = "../../packages/astroport-governance" } +cosmwasm-schema = "1.1" + +[dev-dependencies] +cw-multi-test = "0.16" +astroport-tests-lite = { path = "../../packages/astroport-tests-lite" } + +astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +cw20 = "0.15" +voting-escrow = { path = "../voting_escrow" } +anyhow = "1" +proptest = "1.0" diff --git a/contracts/generator_controller_lite/README.md b/contracts/generator_controller_lite/README.md new file mode 100644 index 00000000..95be0ce1 --- /dev/null +++ b/contracts/generator_controller_lite/README.md @@ -0,0 +1,310 @@ +# Generator Controller + +The Generator Controller allows vxASTRO holders to vote on changing `alloc_point`s in the Generator contract every 2 weeks. Note that the Controller contract uses the word "pool" when referring to LP tokens (generators) available in the Generator contract. + +## InstantiateMsg + +Initialize the contract with the initial owner, the addresses of the xvASTRO, the Generator and the Factory contracts +and the max amount of pools that can receive ASTRO emissions at the same time. + +```json +{ + "owner": "wasm...", + "escrow_addr": "wasm...", + "generator_addr": "wasm...", + "factory_addr": "wasm...", + "pools_limit": 5 +} +``` + +## ExecuteMsg + +### `kick_blacklisted_voters` + +Remove votes of voters that are blacklisted. + +```json +{ + "kick_blacklisted_voters": { + "blacklisted_voters": ["wasm...", "wasm..."] + } +} +``` + +### `kick_unlocked_voters` + +Remove votes of voters that have unlocked their vxASTRO. + +```json +{ + "kick_unlocked_voters": { + "unlocked_voters": ["wasm...", "wasm..."] + } +} +``` + +### `update_config` + +Sets various configuration parameters. Any of them can be omitted. + +```json +{ + "update_config": { + "blacklisted_voters_limit": 22, + "main_pool": "wasm...", + "main_pool_min_alloc": "0.3" + } +} +``` + +### `vote` + +Vote on pools that will start to get an ASTRO distribution in the current period. For example, assume an address has voting +power `100`. Then, following the example below, pools will receive voting power 10, 50, 40 respectively. Note that all values are scaled so they sum to 10,000. + +```json +{ + "vote": { + "votes": [ + [ + "wasm...", + 1000 + ], + [ + "wasm...", + 5000 + ], + [ + "wasm...", + 4000 + ] + ] + } +} +``` + +### `tune_pools` + +Calculate voting power for all pools and apply new allocation points in generator contract. + +```json +{ + "tune_pools": {} +} +``` + +### `change_pool_limit` + +Only contract owner can call this function. Change max number of pools that can receive an ASTRO allocation. + +```json +{ + "change_pool_limit": { + "limit": 6 + } +} +``` + +### `propose_new_owner` + +Create a request to change contract ownership. The validity period of the offer is set by the `expires_in` variable. +Only the current contract owner can execute this method. + +```json +{ + "propose_new_owner": { + "owner": "wasm...", + "expires_in": 1234567 + } +} +``` + +### `drop_ownership_proposal` + +Delete the contract ownership transfer proposal. Only the current contract owner can execute this method. + +```json +{ + "drop_ownership_proposal": {} +} +``` + +### `claim_ownership` + +Used to claim contract ownership. Only the newly proposed contract owner can execute this method. + +```json +{ + "claim_ownership": {} +} +``` + +### `update_whitelist` + +Adds or removes lp tokens which are eligible to receive votes. + +```json +{ + "update_whitelist": { + "add": [ + "wasm...", + "wasm..." + ], + "remove": [ + "wasm...", + "wasm..." + ] + } +} +``` + +### `update_networks` + +Adds or removes network mappings for tuning pools on remote chains. + +```json +{ + "update_networks": { + "add": [ + { + "address_prefix": "wasm", + "generator_address": "wasm124tapgv8wsn5t3rv2cvywhxxxxxxxxx", + "ibc_channel": "channel-1" + } + ], + "remove": [ + "wasm", + ] + } +} +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `user_info` + +Request: + +```json +{ + "user_info": { + "user": "wasm..." + } +} +``` + +Returns last user's voting parameters. + +```json +{ + "user_info_response": { + "vote_ts": 1234567, + "voting_power": 100, + "slope": 0, + "lock_end": 0, + "votes": [ + [ + "wasm...", + 1000 + ], + [ + "wasm...", + 5000 + ], + [ + "wasm...", + 4000 + ] + ] + } +} +``` + +### `tune_info` + +Returns last tune information. + +```json +{ + "tune_info_response": { + "tune_ts": 1234567, + "pool_alloc_points": [ + [ + "wasm...", + 4000 + ], + [ + "wasm...", + 6000 + ] + ] + } +} +``` + +### `pool_info` + +Returns pool voting parameters at the current block period. + +Request: + +```json +{ + "pool_info": { + "pool_addr": "wasm..." + } +} +``` + +Response: + +```json +{ + "voted_pool_info_response": { + "vxastro_amount": 1000, + "slope": 0 + } +} +``` + +### `pool_info_at_period` + +Returns pool voting parameters at specified period. + +Request: + +```json +{ + "pool_info_at_period": { + "pool_addr": "wasm...", + "period": 10 + } +} +``` + +Response: + +```json +{ + "voted_pool_info_response": { + "vxastro_amount": 1000, + "slope": 0 + } +} +``` + +### `config` + +Returns the contract's config. + +```json +{ + "owner": "wasm...", + "escrow_addr": "wasm...", + "generator_addr": "wasm...", + "factory_addr": "wasm...", + "pools_limit": 5 +} +``` diff --git a/contracts/generator_controller_lite/src/bps.rs b/contracts/generator_controller_lite/src/bps.rs new file mode 100644 index 00000000..63799710 --- /dev/null +++ b/contracts/generator_controller_lite/src/bps.rs @@ -0,0 +1,89 @@ +use crate::error::ContractError; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Decimal, Fraction, StdError, Uint128}; +use std::convert::{TryFrom, TryInto}; +use std::ops::Mul; + +/// BasicPoints struct implementation. BasicPoints value is within [0, 10000] interval. +/// Technically BasicPoints is wrapper over [`u16`] with additional limit checks and +/// several implementations of math functions so BasicPoints object +/// can be used in formulas along with [`Uint128`] and [`Decimal`]. +#[cw_serde] +#[derive(Default, Copy)] +pub struct BasicPoints(u16); + +impl BasicPoints { + pub const MAX: u16 = 10000; + + pub fn checked_add(self, rhs: Self) -> Result { + let next_value = self.0 + rhs.0; + if next_value > Self::MAX { + Err(ContractError::BPSLimitError {}) + } else { + Ok(Self(next_value)) + } + } + + pub fn from_ratio(numerator: Uint128, denominator: Uint128) -> Result { + numerator + .checked_multiply_ratio(Self::MAX, denominator) + .map_err(|_| StdError::generic_err("Checked multiply ratio error!"))? + .u128() + .try_into() + } +} + +impl TryFrom for BasicPoints { + type Error = ContractError; + + fn try_from(value: u16) -> Result { + if value <= Self::MAX { + Ok(Self(value)) + } else { + Err(ContractError::BPSConverstionError(value as u128)) + } + } +} + +impl TryFrom for BasicPoints { + type Error = ContractError; + + fn try_from(value: u128) -> Result { + if value <= Self::MAX as u128 { + Ok(Self(value as u16)) + } else { + Err(ContractError::BPSConverstionError(value)) + } + } +} + +impl From for u16 { + fn from(value: BasicPoints) -> Self { + value.0 + } +} + +impl From for Uint128 { + fn from(value: BasicPoints) -> Self { + Uint128::from(u16::from(value)) + } +} + +impl Mul for BasicPoints { + type Output = Uint128; + + fn mul(self, rhs: Uint128) -> Self::Output { + rhs.multiply_ratio(self.0, Self::MAX) + } +} + +impl Mul for BasicPoints { + type Output = Decimal; + + fn mul(self, rhs: Decimal) -> Self::Output { + Decimal::from_ratio( + rhs.numerator() * Uint128::from(self.0), + rhs.denominator() * Uint128::from(Self::MAX), + ) + } +} diff --git a/contracts/generator_controller_lite/src/contract.rs b/contracts/generator_controller_lite/src/contract.rs new file mode 100644 index 00000000..d9fdc9e8 --- /dev/null +++ b/contracts/generator_controller_lite/src/contract.rs @@ -0,0 +1,937 @@ +use std::collections::HashSet; +use std::convert::TryInto; + +use crate::astroport; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use astroport_governance::assembly::{ + Config as AssemblyConfig, ExecuteMsg::ExecuteEmissionsProposal, +}; +use astroport_governance::astroport::asset::addr_opt_validate; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, Fraction, MessageInfo, Order, + Response, StdError, StdResult, Uint128, WasmMsg, +}; +use cw2::set_contract_version; +use itertools::Itertools; + +use astroport_governance::generator_controller_lite::{ + ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, NetworkInfo, QueryMsg, + UserInfoResponse, VOTERS_MAX_LIMIT, +}; +use astroport_governance::utils::{check_controller_supports_channel, get_lite_period}; +use astroport_governance::voting_escrow_lite::QueryMsg::CheckVotersAreBlacklisted; +use astroport_governance::voting_escrow_lite::{ + get_emissions_voting_power, get_lock_info, BlacklistedVotersResponse, +}; + +use crate::bps::BasicPoints; +use crate::error::ContractError; +use crate::state::{ + Config, TuneInfo, UserInfo, VotedPoolInfo, CONFIG, OWNERSHIP_PROPOSAL, POOLS, TUNE_INFO, + USER_INFO, +}; + +use crate::utils::{ + cancel_user_changes, check_duplicated, determine_address_prefix, filter_pools, get_pool_info, + group_pools_by_network, update_pool_info, validate_pool, validate_pools_limit, vote_for_pool, +}; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "generator-controller-lite"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +type ExecuteResult = Result; + +/// Creates a new contract with the specified parameters in the [`InstantiateMsg`]. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> ExecuteResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + CONFIG.save( + deps.storage, + &Config { + owner: deps.api.addr_validate(&msg.owner)?, + escrow_addr: deps.api.addr_validate(&msg.escrow_addr)?, + generator_addr: deps.api.addr_validate(&msg.generator_addr)?, + factory_addr: deps.api.addr_validate(&msg.factory_addr)?, + assembly_addr: deps.api.addr_validate(&msg.assembly_addr)?, + hub_addr: addr_opt_validate(deps.api, &msg.hub_addr)?, + pools_limit: validate_pools_limit(msg.pools_limit)?, + kick_voters_limit: None, + main_pool: None, + main_pool_min_alloc: Decimal::zero(), + whitelisted_pools: vec![], + // Set the current network as allowed by default + whitelisted_networks: vec![NetworkInfo { + address_prefix: determine_address_prefix(&msg.generator_addr)?, + generator_address: deps.api.addr_validate(&msg.generator_addr)?, + ibc_channel: None, + }], + }, + )?; + + // Set tune_ts just for safety so the first tuning could happen in 2 weeks + TUNE_INFO.save( + deps.storage, + &TuneInfo { + tune_period: get_lite_period(env.block.time.seconds())?, + pool_alloc_points: vec![], + }, + )?; + + Ok(Response::default()) +} + +/// Exposes all the execute functions available in the contract. +/// +/// ## Execute messages +/// * **ExecuteMsg::KickBlacklistedVoters { blacklisted_voters }** Removes all votes applied by +/// blacklisted voters +/// +/// * **ExecuteMsg::KickUnlockedVoters { blacklisted_voters }** Removes all votes applied by +/// voters that started unlocking +/// +/// * **ExecuteMsg::KickUnlockedOutpostVoter { blacklisted_voters }** Removes all votes applied by +/// voters that started unlocking on an Outpost +/// +/// * **ExecuteMsg::Vote { votes }** Casts votes for pools +/// +/// * **ExecuteMsg::OutpostVote { voter, votes, voting_power }** Casts votes for pools from an Outpost +/// +/// * **ExecuteMsg::TunePools** Launches pool tuning +/// +/// * **ExecuteMsg::ChangePoolsLimit { limit }** Changes the number of pools which are eligible +/// to receive allocation points +/// +/// * **ExecuteMsg::UpdateConfig { blacklisted_voters_limit }** Changes the number of blacklisted +/// voters that can be kicked at once +/// +/// * **ExecuteMsg::UpdateWhitelist { add, remove }** Adds or removes lp tokens which are eligible +/// to receive votes. +/// +/// * **ExecuteMsg::UpdateNetworks { add, remove }** Adds or removes networks mappings for tuning +/// pools on remote chains via a special governance proposal +/// +/// * **ExecuteMsg::ProposeNewOwner { owner, expires_in }** Creates a new request to change +/// contract ownership. +/// +/// * **ExecuteMsg::DropOwnershipProposal {}** Removes a request to change contract ownership. +/// +/// * **ExecuteMsg::ClaimOwnership {}** Claims contract ownership. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> ExecuteResult { + match msg { + ExecuteMsg::KickBlacklistedVoters { blacklisted_voters } => { + kick_blacklisted_voters(deps, env, blacklisted_voters) + } + ExecuteMsg::KickUnlockedVoters { unlocked_voters } => { + kick_unlocked_voters(deps, env, unlocked_voters) + } + ExecuteMsg::KickUnlockedOutpostVoter { unlocked_voter } => { + kick_unlocked_outpost_voter(deps, env, info, unlocked_voter) + } + ExecuteMsg::Vote { votes } => handle_vote(deps, env, info, votes), + ExecuteMsg::OutpostVote { + voter, + votes, + voting_power, + } => handle_outpost_vote(deps, env, info, voter, votes, voting_power), + ExecuteMsg::TunePools {} => tune_pools(deps, env), + ExecuteMsg::ChangePoolsLimit { limit } => change_pools_limit(deps, info, limit), + ExecuteMsg::UpdateConfig { + assembly_addr, + kick_voters_limit, + main_pool, + main_pool_min_alloc, + remove_main_pool, + hub_addr, + } => update_config( + deps, + info, + assembly_addr, + kick_voters_limit, + main_pool, + main_pool_min_alloc, + remove_main_pool, + hub_addr, + ), + ExecuteMsg::UpdateWhitelist { add, remove } => update_whitelist(deps, info, add, remove), + ExecuteMsg::UpdateNetworks { add, remove } => update_networks(deps, info, add, remove), + ExecuteMsg::ProposeNewOwner { + new_owner, + expires_in, + } => { + let config: Config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + new_owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config: Config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + } +} + +/// Adds or removes lp tokens which are eligible to receive votes. +/// Returns a [`ContractError`] on failure. +fn update_whitelist( + deps: DepsMut, + info: MessageInfo, + add: Option>, + remove: Option>, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + // Remove old LP tokens + if let Some(remove_lp_tokens) = remove { + config + .whitelisted_pools + .retain(|pool| !remove_lp_tokens.contains(&pool.to_string())); + } + + // Add new lp tokens + if let Some(add_lp_tokens) = add { + config.whitelisted_pools.append( + &mut add_lp_tokens + .into_iter() + .map(|lp_token| { + validate_pool(&config, &lp_token)?; + Ok(lp_token) + }) + .collect::, ContractError>>()?, + ); + check_duplicated(&config.whitelisted_pools).map_err(|_| + ContractError::Std(StdError::generic_err("The resulting whitelist contains duplicated pools. It's either provided 'add' list contains duplicated pools or some of the added pools are already whitelisted.")))?; + } + + CONFIG.save(deps.storage, &config)?; + Ok(Response::default().add_attribute("action", "update_whitelist")) +} + +/// Adds or removes networks mappings for tuning +/// pools on remote chains via a special governance proposal +/// Returns a [`ContractError`] on failure. +fn update_networks( + deps: DepsMut, + info: MessageInfo, + add: Option>, + remove: Option>, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Permission check + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + // Handle removals + // The network added in instantiate, ie. the network of the contract itself, cannot be removed + if let Some(remove_prefixes) = remove { + let native_prefix = determine_address_prefix(config.generator_addr.as_ref())?; + + if remove_prefixes.contains(&native_prefix) { + return Err(ContractError::Std(StdError::generic_err(format!( + "Cannot remove the native network with prefix {}", + native_prefix + )))); + } + + config + .whitelisted_networks + .retain(|network| !remove_prefixes.contains(&network.address_prefix)); + } + + let mut response = Response::default().add_attribute("action", "update_networks"); + if let Some(add_prefix) = add { + // Get the assembly contract to check if the controller supports a specific channel + let assembly_config: AssemblyConfig = deps + .querier + .query_wasm_smart(config.assembly_addr.clone(), &QueryMsg::Config {})?; + + config.whitelisted_networks.append( + &mut add_prefix + .into_iter() + .map(|mut network_info| { + // If the IBC channel is set, check if the controller supports it + if let Some(ibc_channel) = network_info.ibc_channel.clone() { + match &assembly_config.ibc_controller { + Some(ibc_controller) => { + check_controller_supports_channel( + deps.querier, + ibc_controller, + &ibc_channel, + )?; + } + None => { + return Err(ContractError::Std(StdError::generic_err( + "The Assembly does not have an IBC controller set", + ))) + } + } + } + // Determine the prefix based on the generator address + network_info.address_prefix = + determine_address_prefix(network_info.generator_address.as_ref())?; + Ok(network_info) + }) + .collect::, ContractError>>()?, + ); + let prefixes: Vec = config + .whitelisted_networks + .iter() + .map(|info| info.address_prefix.clone()) + .collect(); + check_duplicated(&prefixes).map_err(|_| + ContractError::Std(StdError::generic_err("The resulting whitelist contains duplicated prefixes. It's either provided 'add' list contains duplicated prefixes or some of the added prefixes are already whitelisted.")))?; + // Emit added prefixes + response = response.add_attribute("added", prefixes.join(",")); + } + + CONFIG.save(deps.storage, &config)?; + Ok(response) +} + +/// This function removes all votes applied by blacklisted voters. +/// +/// * **holders** list with blacklisted holders whose votes will be removed. +fn kick_blacklisted_voters(deps: DepsMut, env: Env, voters: Vec) -> ExecuteResult { + let block_period = get_lite_period(env.block.time.seconds())?; + let config = CONFIG.load(deps.storage)?; + + if voters.len() > config.kick_voters_limit.unwrap_or(VOTERS_MAX_LIMIT) as usize { + return Err(ContractError::KickVotersLimitExceeded {}); + } + + // Check duplicated voters + let addrs_set = voters.iter().collect::>(); + if voters.len() != addrs_set.len() { + return Err(ContractError::DuplicatedVoters {}); + } + + // Check if voters are blacklisted + let res: BlacklistedVotersResponse = deps.querier.query_wasm_smart( + config.escrow_addr, + &CheckVotersAreBlacklisted { + voters: voters.clone(), + }, + )?; + + if !res.eq(&BlacklistedVotersResponse::VotersBlacklisted {}) { + return Err(ContractError::Std(StdError::generic_err(res.to_string()))); + } + + for voter in voters { + if let Some(user_info) = USER_INFO.may_load(deps.storage, &voter)? { + // Cancel changes applied by previous votes immediately + user_info.votes.iter().try_for_each(|(pool_addr, bps)| { + cancel_user_changes( + deps.storage, + block_period, + pool_addr, + *bps, + user_info.voting_power, + ) + })?; + + let user_info = UserInfo { + vote_period: Some(block_period), + ..Default::default() + }; + + USER_INFO.save(deps.storage, &voter, &user_info)?; + } + } + + Ok(Response::new().add_attribute("action", "kick_blocklisted_holders")) +} + +/// This function removes all votes applied by unlocked voters. +/// +/// * **holders** list with unlocked holders whose votes will be removed. +fn kick_unlocked_voters(deps: DepsMut, env: Env, voters: Vec) -> ExecuteResult { + let block_period = get_lite_period(env.block.time.seconds())?; + let config = CONFIG.load(deps.storage)?; + + if voters.len() > config.kick_voters_limit.unwrap_or(VOTERS_MAX_LIMIT) as usize { + return Err(ContractError::KickVotersLimitExceeded {}); + } + + // Check duplicated voters + let addrs_set = voters.iter().collect::>(); + if voters.len() != addrs_set.len() { + return Err(ContractError::DuplicatedVoters {}); + } + + for voter in voters { + let lock_info = get_lock_info(&deps.querier, config.escrow_addr.clone(), voter.clone())?; + if lock_info.end.is_none() { + // This voter has not unlocked + return Err(ContractError::AddressIsLocked(voter)); + } + + if let Some(user_info) = USER_INFO.may_load(deps.storage, &voter)? { + // Cancel changes applied by previous votes immediately + user_info.votes.iter().try_for_each(|(pool_addr, bps)| { + cancel_user_changes( + deps.storage, + block_period, + pool_addr, + *bps, + user_info.voting_power, + ) + })?; + + let user_info = UserInfo { + vote_period: Some(block_period), + ..Default::default() + }; + + USER_INFO.save(deps.storage, &voter, &user_info)?; + } + } + + Ok(Response::new().add_attribute("action", "kick_holders")) +} + +/// This function removes all votes applied by an unlocked voters from an Outpost. +/// +/// * **voter** the unlocked holder whose votes will be removed. +fn kick_unlocked_outpost_voter( + deps: DepsMut, + env: Env, + info: MessageInfo, + voter: String, +) -> ExecuteResult { + let config = CONFIG.load(deps.storage)?; + + // We only allow the Hub to kick a voter from an Outpost + let hub = match config.hub_addr { + Some(hub) => hub, + None => return Err(ContractError::InvalidHub {}), + }; + + if info.sender != hub { + return Err(ContractError::Unauthorized {}); + } + + let block_period = get_lite_period(env.block.time.seconds())?; + if let Some(user_info) = USER_INFO.may_load(deps.storage, &voter)? { + // Cancel changes applied by previous votes immediately + user_info.votes.iter().try_for_each(|(pool_addr, bps)| { + cancel_user_changes( + deps.storage, + block_period, + pool_addr, + *bps, + user_info.voting_power, + ) + })?; + + let user_info = UserInfo { + vote_period: Some(block_period), + ..Default::default() + }; + + USER_INFO.save(deps.storage, &voter, &user_info)?; + } + + Ok(Response::new().add_attribute("action", "kick_outpost_holders")) +} + +/// Handles a vote on the current chain. +/// +/// * **votes** is a vector of pairs ([`String`], [`u16`]). +/// Tuple consists of pool address and percentage of user's voting power for a given pool. +/// Percentage should be in BPS form. +fn handle_vote( + deps: DepsMut, + env: Env, + info: MessageInfo, + votes: Vec<(String, u16)>, +) -> ExecuteResult { + let user = info.sender.to_string(); + let config = CONFIG.load(deps.storage)?; + let user_vp = get_emissions_voting_power(&deps.querier, &config.escrow_addr, &user)?; + + apply_vote(deps, env, user, user_vp, config, votes)?; + + Ok(Response::new().add_attribute("action", "vote")) +} + +/// Handles a vote from an Outpost. +/// +/// * **voter** is the address of the voter from the Outpost. +/// +/// * **votes** is a vector of pairs ([`String`], [`u16`]). +/// Tuple consists of pool address and percentage of user's voting power for a given pool. +/// Percentage should be in BPS form. +/// +/// * **voting_power** is voting power of the voter from the Outpost as validated by the Hub. +fn handle_outpost_vote( + deps: DepsMut, + env: Env, + info: MessageInfo, + voter: String, + votes: Vec<(String, u16)>, + voting_power: Uint128, +) -> ExecuteResult { + let config = CONFIG.load(deps.storage)?; + + // We only allow the Hub to submit emission votes on behalf of Outpost user + // The Hub is responsible for validating the Hub vote with the Outpost + let hub = match config.hub_addr.clone() { + Some(hub) => hub, + None => return Err(ContractError::InvalidHub {}), + }; + + if info.sender != hub { + return Err(ContractError::Unauthorized {}); + } + + apply_vote(deps, env, voter, voting_power, config, votes)?; + + Ok(Response::new().add_attribute("action", "outpost_vote")) +} + +/// Apply the votes for the given user +/// +/// The function checks that: +/// * the user voting power is > 0, +/// * user didn't vote in this period, +/// * 'votes' vector doesn't contain duplicated pool addresses, +/// * sum of all BPS values <= 10000. +/// +/// The function cancels changes applied by previous votes and apply new votes for the this period. +/// New vote parameters are saved in [`USER_INFO`]. +fn apply_vote( + deps: DepsMut, + env: Env, + voter: String, + voting_power: Uint128, + config: ConfigResponse, + votes: Vec<(String, u16)>, +) -> Result<(), ContractError> { + if voting_power.is_zero() { + return Err(ContractError::ZeroVotingPower {}); + } + + if config.whitelisted_pools.is_empty() { + return Err(ContractError::WhitelistEmpty {}); + } + + let user_info = USER_INFO + .may_load(deps.storage, &voter)? + .unwrap_or_default(); + + let block_period = get_lite_period(env.block.time.seconds())?; + if let Some(vote_period) = user_info.vote_period { + if vote_period == block_period { + return Err(ContractError::CooldownError {}); + } + } + + // Has the user voted in this period? + check_duplicated( + &votes + .iter() + .map(|vote| { + let (lp_token, _) = vote; + lp_token + }) + .collect::>(), + )?; + + // Validating addrs and bps + let votes = votes + .into_iter() + .map(|(addr, bps)| { + // Voting for the main pool is prohibited + if let Some(main_pool) = &config.main_pool { + if addr == *main_pool { + return Err(ContractError::MainPoolVoteOrWhitelistingProhibited( + main_pool.to_string(), + )); + } + } + if !config.whitelisted_pools.contains(&addr) { + return Err(ContractError::PoolIsNotWhitelisted(addr)); + } + + validate_pool(&config, &addr)?; + + let bps: BasicPoints = bps.try_into()?; + Ok((addr, bps)) + }) + .collect::, ContractError>>()?; + + // Check the bps sum is within the limit + votes + .iter() + .try_fold(BasicPoints::default(), |acc, (_, bps)| { + acc.checked_add(*bps) + })?; + + // Cancel changes applied by previous votes + user_info.votes.iter().try_for_each(|(pool_addr, bps)| { + cancel_user_changes( + deps.storage, + block_period, + pool_addr, + *bps, + user_info.voting_power, + ) + })?; + + // Votes are applied to current period + // In vxASTRO lite, voting power is removed immediately + // when a user unlocks + votes.iter().try_for_each(|(pool_addr, bps)| { + vote_for_pool( + deps.storage, + block_period, + pool_addr.as_str(), + *bps, + voting_power, + ) + })?; + + let user_info = UserInfo { + vote_period: Some(block_period), + voting_power, + votes, + }; + + Ok(USER_INFO.save(deps.storage, &voter, &user_info)?) +} + +/// The function checks that the last pools tuning happened >= 14 days ago. +/// Then it calculates voting power for each pool at the current period, filters all pools which +/// are not eligible to receive allocation points, +/// takes top X pools by voting power, where X is 'config.pools_limit', calculates allocation points +/// for these pools and applies allocation points in generator contract. +/// +/// For pools on the same network (e.g. Terra), the allocation points are set +/// directly on the generator. For pools on different networks (e.g. Injective), +/// we create a special governance proposal to set the allocation points on the +/// remote generator. +/// +/// We determine the network of a pool by looking at the address prefix. +fn tune_pools(deps: DepsMut, env: Env) -> ExecuteResult { + let mut tune_info = TUNE_INFO.load(deps.storage)?; + let config = CONFIG.load(deps.storage)?; + let block_period = get_lite_period(env.block.time.seconds())?; + + if tune_info.tune_period == block_period { + return Err(ContractError::CooldownError {}); + } + + // We're tuning pools based on the previous voting period + let tune_period = block_period - 1; + let pool_votes: Vec<_> = POOLS + .keys(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>() + .into_iter() + .map(|pool_addr| { + let pool_addr = pool_addr?; + + let pool_info = update_pool_info(deps.storage, tune_period, &pool_addr, None)?; + // Remove pools with zero voting power so we won't iterate over them in future + if pool_info.vxastro_amount.is_zero() { + POOLS.remove(deps.storage, &pool_addr) + } + Ok((pool_addr, pool_info.vxastro_amount)) + }) + .collect::>>()? + .into_iter() + .filter(|(_, vxastro_amount)| !vxastro_amount.is_zero()) + .sorted_by(|(_, a), (_, b)| b.cmp(a)) // Sort in descending order + .collect(); + + // Filter pools which are not eligible to receive allocation points + // Pools might be on a different chain and thus not much can be done in + // terms of validation. That will be handled via governance proposals and + // the whitelist + tune_info.pool_alloc_points = filter_pools( + pool_votes, + config.pools_limit + 1, // +1 additional pool if we will need to remove the main pool + )?; + + // Set allocation points for the main pool + match config.main_pool { + Some(main_pool) if !config.main_pool_min_alloc.is_zero() => { + // Main pool may appear in the pool list thus we need to eliminate its contribution in the total VP. + tune_info + .pool_alloc_points + .retain(|(pool, _)| pool != &main_pool.to_string()); + // If there is no main pool in the filtered list then we need to remove additional pool + tune_info.pool_alloc_points = tune_info + .pool_alloc_points + .iter() + .take(config.pools_limit as usize) + .cloned() + .collect(); + + let total_vp: Uint128 = tune_info + .pool_alloc_points + .iter() + .fold(Uint128::zero(), |acc, (_, vp)| acc + vp); + // Calculate main pool contribution. + // Example (30% for the main pool): VP + x = y, x = 0.3y => y = VP/0.7 => x = 0.3 * VP / 0.7, + // where VP - total VP, x - main pool's contribution, y - new total VP. + // x = 0.3 * VP * (1-0.3)^(-1) + let main_pool_contribution = config.main_pool_min_alloc + * total_vp + * (Decimal::one() - config.main_pool_min_alloc).inv().unwrap(); + tune_info + .pool_alloc_points + .push((main_pool.to_string(), main_pool_contribution)) + } + _ => { + // there is no main pool or min alloc is 0% + tune_info.pool_alloc_points = tune_info + .pool_alloc_points + .iter() + .take(config.pools_limit as usize) + .cloned() + .collect(); + } + } + + if tune_info.pool_alloc_points.is_empty() { + return Err(ContractError::TuneNoPools {}); + } + + // Tuning can only happen once per period. As we're tuning for the previous + // period, we set this to the current period + tune_info.tune_period = block_period; + TUNE_INFO.save(deps.storage, &tune_info)?; + + // Split pools by network and send separate messages for each network + let grouped_pools = group_pools_by_network(&config.whitelisted_networks, &tune_info); + + let mut response = Response::new().add_attribute("action", "tune_pools"); + for (network_info, pool_alloc_points) in &grouped_pools { + // The message to set the allocation points on the generator, either + // directly or via a governance proposal for Outposts + let setup_pools_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: network_info.generator_address.to_string(), + msg: to_binary(&astroport::generator::ExecuteMsg::SetupPools { + pools: pool_alloc_points.to_vec(), + })?, + funds: vec![], + }); + + match &network_info.ibc_channel { + // If the channel is empty, then this is setting up pools on the network + // we are deployed on and we can continue as normal + None => { + response = response + .add_attribute("tune", network_info.address_prefix.to_string()) + .add_attribute("pool_count", pool_alloc_points.len().to_string()) + .add_message(setup_pools_msg); + } + // If the channel is not empty, then this is setting up pools on an + // Outpost + Some(ibc_channel) => { + // We need to submit the setup pools message to the + // Assembly as a proposal to execute on the remote chain + let proposal_msg = to_binary(&ExecuteEmissionsProposal { + title: format!( + // Sample title: "Update emissions on the inj outpost", "Update emissions on the neutron outpost" + "Update emissions on the {} outpost", + network_info.address_prefix + ), + description: format!( + // Sample title: "This proposal aims to update emissions on the inj outpost using IBC channel-2" + "This proposal aims to update emissions on the {} outpost using IBC {}", + network_info.address_prefix, ibc_channel + ), + messages: vec![setup_pools_msg], + ibc_channel: Some(ibc_channel.to_string()), + })?; + + let setup_pools_assembly_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.assembly_addr.to_string(), + msg: proposal_msg, + funds: vec![], + }); + + response = response + .add_attribute("tune", network_info.address_prefix.to_string()) + .add_attribute("channel", ibc_channel) + .add_attribute("pool_count", pool_alloc_points.len().to_string()) + .add_message(setup_pools_assembly_msg); + } + } + } + Ok(response) +} + +/// Only contract owner can call this function. +/// The function sets a new limit of blacklisted voters that can be kicked at once. +/// +/// * **assembly_addr** is a new address of the Assembly contract +/// +/// * **kick_voters_limit** is a new limit of blacklisted or unlocked voters which can be kicked at once +/// +/// * **main_pool** is a main pool address +/// +/// * **main_pool_min_alloc** is a minimum percentage of ASTRO emissions that this pool should get every block +/// +/// * **remove_main_pool** should the main pool be removed or not +#[allow(clippy::too_many_arguments)] +fn update_config( + deps: DepsMut, + info: MessageInfo, + assembly_addr: Option, + kick_voters_limit: Option, + main_pool: Option, + main_pool_min_alloc: Option, + remove_main_pool: Option, + hub_addr: Option, +) -> ExecuteResult { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + if let Some(assembly_addr) = assembly_addr { + config.assembly_addr = deps.api.addr_validate(&assembly_addr)?; + } + + if let Some(kick_voters_limit) = kick_voters_limit { + config.kick_voters_limit = Some(kick_voters_limit); + } + + if let Some(main_pool_min_alloc) = main_pool_min_alloc { + if main_pool_min_alloc == Decimal::zero() || main_pool_min_alloc >= Decimal::one() { + return Err(ContractError::MainPoolMinAllocFailed {}); + } + config.main_pool_min_alloc = main_pool_min_alloc; + } + + if let Some(main_pool) = main_pool { + if config.main_pool_min_alloc.is_zero() { + return Err(StdError::generic_err("Main pool min alloc can not be zero").into()); + } + config.main_pool = Some(deps.api.addr_validate(&main_pool)?); + } + + if let Some(remove_main_pool) = remove_main_pool { + if remove_main_pool { + config.main_pool = None; + } + } + + if let Some(hub_addr) = hub_addr { + config.hub_addr = Some(deps.api.addr_validate(&hub_addr)?); + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default().add_attribute("action", "update_config")) +} + +/// Only contract owner can call this function. +/// The function sets new limit of pools which are eligible to receive allocation points. +/// +/// * **limit** is a new limit of pools which are eligible to receive allocation points. +fn change_pools_limit(deps: DepsMut, info: MessageInfo, limit: u64) -> ExecuteResult { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + config.pools_limit = validate_pools_limit(limit)?; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default().add_attribute("action", "change_pools_limit")) +} + +/// Expose available contract queries. +/// +/// ## Queries +/// * **QueryMsg::UserInfo { user }** Fetch user information +/// +/// * **QueryMsg::TuneInfo** Fetch last tuning information +/// +/// * **QueryMsg::Config** Fetch contract config +/// +/// * **QueryMsg::PoolInfo { pool_addr }** Fetch pool's voting information at the current period. +/// +/// * **QueryMsg::PoolInfoAtPeriod { pool_addr, period }** Fetch pool's voting information at a specified period. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::UserInfo { user } => to_binary(&user_info(deps, user)?), + QueryMsg::TuneInfo {} => to_binary(&TUNE_INFO.load(deps.storage)?), + QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), + QueryMsg::PoolInfo { pool_addr } => to_binary(&pool_info(deps, env, pool_addr, None)?), + QueryMsg::PoolInfoAtPeriod { pool_addr, period } => { + to_binary(&pool_info(deps, env, pool_addr, Some(period))?) + } + } +} + +/// Returns user information. +fn user_info(deps: Deps, user: String) -> StdResult { + USER_INFO + .may_load(deps.storage, &user)? + .map(UserInfo::into_response) + .ok_or_else(|| StdError::generic_err("User not found")) +} + +/// Returns pool's voting information at a specified period. +fn pool_info( + deps: Deps, + env: Env, + pool_addr: String, + period: Option, +) -> StdResult { + let block_period = get_lite_period(env.block.time.seconds())?; + let period = period.unwrap_or(block_period); + get_pool_info(deps.storage, period, &pool_addr) +} + +/// Manages contract migration +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Err(ContractError::MigrationError {}) +} diff --git a/contracts/generator_controller_lite/src/error.rs b/contracts/generator_controller_lite/src/error.rs new file mode 100644 index 00000000..2c3368b3 --- /dev/null +++ b/contracts/generator_controller_lite/src/error.rs @@ -0,0 +1,69 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +/// This enum describes contract errors +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Basic points conversion error. {0} > 10000")] + BPSConverstionError(u128), + + #[error("Basic points sum exceeds limit")] + BPSLimitError {}, + + #[error("You can't vote with zero voting power")] + ZeroVotingPower {}, + + #[error("{0} is the main pool. Voting or whitelisting the main pool is prohibited.")] + MainPoolVoteOrWhitelistingProhibited(String), + + #[error("main_pool_min_alloc should be more than 0 and less than 1")] + MainPoolMinAllocFailed {}, + + #[error("You can only run this action once in a voting period")] + CooldownError {}, + + #[error("Invalid lp token address: {0}")] + InvalidLPTokenAddress(String), + + #[error("Votes contain duplicated pool addresses")] + DuplicatedPools {}, + + #[error("There are no pools to tune")] + TuneNoPools {}, + + #[error("Invalid pool number: {0}. Must be within [2, 100] range")] + InvalidPoolNumber(u64), + + #[error("The vector contains duplicated addresses")] + DuplicatedVoters {}, + + #[error("Exceeded voters limit for kick blacklisted/unlocked voters operation!")] + KickVotersLimitExceeded {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Whitelist cannot be empty!")] + WhitelistEmpty {}, + + #[error("The pair aren't registered: {0}-{1}")] + PairNotRegistered(String, String), + + #[error("Pool is already whitelisted: {0}")] + PoolIsWhitelisted(String), + + #[error("Pool is not whitelisted: {0}")] + PoolIsNotWhitelisted(String), + + #[error("Address is still locked: {0}")] + AddressIsLocked(String), + + #[error("Sender is not the Hub installed")] + InvalidHub {}, +} diff --git a/contracts/generator_controller_lite/src/lib.rs b/contracts/generator_controller_lite/src/lib.rs new file mode 100644 index 00000000..6764e51f --- /dev/null +++ b/contracts/generator_controller_lite/src/lib.rs @@ -0,0 +1,10 @@ +pub mod bps; +pub mod contract; +pub mod state; + +// During development this import could be replaced with another astroport version. +// However, in production, the astroport version should be the same for all contracts. +pub use astroport_governance::astroport; + +mod error; +mod utils; diff --git a/contracts/generator_controller_lite/src/state.rs b/contracts/generator_controller_lite/src/state.rs new file mode 100644 index 00000000..25879046 --- /dev/null +++ b/contracts/generator_controller_lite/src/state.rs @@ -0,0 +1,67 @@ +use crate::astroport::common::OwnershipProposal; +use crate::bps::BasicPoints; + +use astroport_governance::generator_controller_lite::{ + ConfigResponse, GaugeInfoResponse, UserInfoResponse, VotedPoolInfoResponse, +}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint128; +use cw_storage_plus::{Item, Map}; + +/// This structure describes the main control config of generator controller contract. +pub type Config = ConfigResponse; +/// This structure describes voting parameters for a specific pool. +pub type VotedPoolInfo = VotedPoolInfoResponse; +/// This structure describes last tuning parameters. +pub type TuneInfo = GaugeInfoResponse; + +/// The struct describes last user's votes parameters. +#[cw_serde] +#[derive(Default)] +pub struct UserInfo { + /// The period when the user voted last time, None if they've never voted + pub vote_period: Option, + /// The user's vxASTRO voting power + pub voting_power: Uint128, + /// The vote distribution for all the generators/pools the staker picked + pub votes: Vec<(String, BasicPoints)>, +} + +impl UserInfo { + /// The function converts [`UserInfo`] object into [`UserInfoResponse`]. + pub(crate) fn into_response(self) -> UserInfoResponse { + let votes = self + .votes + .iter() + .map(|(pool_addr, bps)| (pool_addr.clone(), u16::from(*bps))) + .collect(); + + UserInfoResponse { + vote_period: self.vote_period, + voting_power: self.voting_power, + votes, + } + } +} + +/// Stores config at the given key. +pub const CONFIG: Item = Item::new("config"); + +/// Stores voting parameters per pool at a specific period by key ( period -> pool_addr ). +pub const POOL_VOTES: Map<(u64, &str), VotedPoolInfo> = Map::new("pool_votes"); + +/// HashSet based on [`Map`]. It contains all pool addresses whose voting power > 0. +pub const POOLS: Map<&str, ()> = Map::new("pools"); + +/// Hashset based on [`Map`]. It stores null object by key ( pool_addr -> period ). +/// This hashset contains all periods which have saved result in [`POOL_VOTES`] for a specific pool address. +pub const POOL_PERIODS: Map<(&str, u64), ()> = Map::new("pool_periods"); + +/// User's voting information. +pub const USER_INFO: Map<&str, UserInfo> = Map::new("user_info"); + +/// Last tuning information. +pub const TUNE_INFO: Item = Item::new("tune_info"); + +/// Contains a proposal to change contract ownership +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); diff --git a/contracts/generator_controller_lite/src/utils.rs b/contracts/generator_controller_lite/src/utils.rs new file mode 100644 index 00000000..a7313648 --- /dev/null +++ b/contracts/generator_controller_lite/src/utils.rs @@ -0,0 +1,295 @@ +use std::collections::{HashMap, HashSet}; +use std::hash::Hash; +use std::ops::RangeInclusive; + +use astroport_governance::generator_controller_lite::{ + ConfigResponse, GaugeInfoResponse, NetworkInfo, +}; +use cosmwasm_std::{Order, StdError, StdResult, Storage, Uint128}; +use cw_storage_plus::Bound; + +use crate::bps::BasicPoints; +use crate::error::ContractError; +use crate::state::{VotedPoolInfo, POOLS, POOL_PERIODS, POOL_VOTES}; + +/// Pools limit should be within the range `[2, 100]` +const POOL_NUMBER_LIMIT: RangeInclusive = 2..=100; + +/// The enum defines math operations with voting power and slope. +#[derive(Debug)] +pub(crate) enum Operation { + Add, + Sub, +} + +impl Operation { + pub fn calc_voting_power(&self, cur_vp: Uint128, vp: Uint128, bps: BasicPoints) -> Uint128 { + match self { + Operation::Add => cur_vp + bps * vp, + Operation::Sub => cur_vp.saturating_sub(bps * vp), + } + } +} + +/// Enum wraps [`VotedPoolInfo`] so the contract can leverage storage operations efficiently. +#[derive(Debug)] +pub(crate) enum VotedPoolInfoResult { + Unchanged(VotedPoolInfo), + New(VotedPoolInfo), +} + +/// Filters pairs (LP token address, voting parameters) by only taking up to +/// pool_limit +/// We can no longer validate the pools as they might be on a different chain +pub(crate) fn filter_pools( + pools: Vec<(String, Uint128)>, + pools_limit: u64, +) -> StdResult> { + let pools = pools + .into_iter() + .map(|(pool_addr, vxastro_amount)| (pool_addr, vxastro_amount)) + .take(pools_limit as usize) + .collect(); + Ok(pools) +} + +/// Cancels user changes using old voting parameters for a given pool. +/// Firstly, it removes slope change scheduled for previous lockup end period. +/// Secondly, it updates voting parameters for the given period, but without user's vote. +pub(crate) fn cancel_user_changes( + storage: &mut dyn Storage, + period: u64, + pool_addr: &str, + old_bps: BasicPoints, + old_vp: Uint128, +) -> StdResult<()> { + update_pool_info( + storage, + period, + pool_addr, + Some((old_bps, old_vp, Operation::Sub)), + ) + .map(|_| ()) +} + +/// Applies user's vote for a given pool. +/// It updates voting parameters with applied user's vote. +pub(crate) fn vote_for_pool( + storage: &mut dyn Storage, + period: u64, + pool_addr: &str, + bps: BasicPoints, + vp: Uint128, +) -> StdResult<()> { + update_pool_info(storage, period, pool_addr, Some((bps, vp, Operation::Add))).map(|_| ()) +} + +/// Fetches voting parameters for a given pool at specific period, applies new changes, saves it in storage +/// and returns new voting parameters in [`VotedPoolInfo`] object. +/// If there are no changes in 'changes' parameter +/// and voting parameters were already calculated before the function just returns [`VotedPoolInfo`]. +pub(crate) fn update_pool_info( + storage: &mut dyn Storage, + period: u64, + pool_addr: &str, + changes: Option<(BasicPoints, Uint128, Operation)>, +) -> StdResult { + if POOLS.may_load(storage, pool_addr)?.is_none() { + POOLS.save(storage, pool_addr, &())? + } + let period_key = period; + let pool_info = match get_pool_info_mut(storage, period, pool_addr)? { + VotedPoolInfoResult::Unchanged(mut pool_info) | VotedPoolInfoResult::New(mut pool_info) + if changes.is_some() => + { + if let Some((bps, vp, op)) = changes { + pool_info.vxastro_amount = op.calc_voting_power(pool_info.vxastro_amount, vp, bps); + } + POOL_PERIODS.save(storage, (pool_addr, period_key), &())?; + POOL_VOTES.save(storage, (period_key, pool_addr), &pool_info)?; + pool_info + } + VotedPoolInfoResult::New(pool_info) => { + POOL_PERIODS.save(storage, (pool_addr, period_key), &())?; + POOL_VOTES.save(storage, (period_key, pool_addr), &pool_info)?; + pool_info + } + VotedPoolInfoResult::Unchanged(pool_info) => pool_info, + }; + + Ok(pool_info) +} + +/// Returns pool info at specified period +pub(crate) fn get_pool_info_mut( + storage: &mut dyn Storage, + period: u64, + pool_addr: &str, +) -> StdResult { + let pool_info_result = + if let Some(pool_info) = POOL_VOTES.may_load(storage, (period, pool_addr))? { + VotedPoolInfoResult::Unchanged(pool_info) + } else { + let pool_info_result = + if let Some(prev_period) = fetch_last_pool_period(storage, period, pool_addr)? { + let pool_info = POOL_VOTES.load(storage, (prev_period, pool_addr))?; + VotedPoolInfo { + vxastro_amount: pool_info.vxastro_amount, + ..pool_info + } + } else { + VotedPoolInfo::default() + }; + + VotedPoolInfoResult::New(pool_info_result) + }; + + Ok(pool_info_result) +} + +/// Returns pool info at specified period. +pub(crate) fn get_pool_info( + storage: &dyn Storage, + period: u64, + pool_addr: &str, +) -> StdResult { + let pool_info = if let Some(pool_info) = POOL_VOTES.may_load(storage, (period, pool_addr))? { + pool_info + } else if let Some(prev_period) = fetch_last_pool_period(storage, period, pool_addr)? { + let pool_info = POOL_VOTES.load(storage, (prev_period, pool_addr))?; + VotedPoolInfo { + vxastro_amount: pool_info.vxastro_amount, + ..pool_info + } + } else { + VotedPoolInfo::default() + }; + + Ok(pool_info) +} + +/// Fetches last period for specified pool which has saved result in [`POOL_PERIODS`]. +pub(crate) fn fetch_last_pool_period( + storage: &dyn Storage, + period: u64, + pool_addr: &str, +) -> StdResult> { + let period_opt = POOL_PERIODS + .prefix(pool_addr) + .range( + storage, + None, + Some(Bound::exclusive(period)), + Order::Descending, + ) + .next() + .transpose()? + .map(|(period, _)| period); + Ok(period_opt) +} + +/// Input validation for pools limit. +pub(crate) fn validate_pools_limit(number: u64) -> Result { + if !POOL_NUMBER_LIMIT.contains(&number) { + Err(ContractError::InvalidPoolNumber(number)) + } else { + Ok(number) + } +} + +/// Check if a pool isn't the main pool. Check if a pool is an LP token. +/// In the lite version this no longer validates if a pool is an LP token +/// or that it is registered in the factory. That is because in the lite +/// version we are dealing with multi chain addresses +pub fn validate_pool(config: &ConfigResponse, pool: &str) -> Result<(), ContractError> { + // Voting for the main pool or updating it is prohibited + if let Some(main_pool) = &config.main_pool { + if pool == *main_pool { + return Err(ContractError::MainPoolVoteOrWhitelistingProhibited( + main_pool.to_string(), + )); + } + } + Ok(()) +} + +/// Checks for duplicate items in a slice +pub fn check_duplicated(items: &[T]) -> Result<(), ContractError> { + let mut uniq = HashSet::new(); + if !items.iter().all(|item| uniq.insert(item)) { + return Err(ContractError::DuplicatedPools {}); + } + + Ok(()) +} + +/// Filters pools by network prefixes to enable sending the message to the +/// correct contracts +pub fn group_pools_by_network<'a>( + networks: &'a [NetworkInfo], + gauge_info: &GaugeInfoResponse, +) -> HashMap<&'a NetworkInfo, Vec<(String, Uint128)>> { + networks + .iter() + .map(|network_info| { + let matching_pools: Vec<_> = gauge_info + .pool_alloc_points + .iter() + .filter(|(address, _)| address.starts_with(network_info.address_prefix.as_str())) + .cloned() + .collect(); + + (network_info, matching_pools) + }) + .collect() +} + +/// Finds the prefix by returning all the characters before the first instance +/// of the first instance of "1" as Cosmos addresses are all based on prefix1restofaddress +/// If the prefix could not be determined, an error is returned +pub fn determine_address_prefix(s: &str) -> Result { + let prefix: String = s.chars().take_while(|&c| c != '1').collect(); + if prefix.is_empty() { + Err(ContractError::Std(StdError::GenericErr { + msg: "Invalid prefix".to_string(), + })) + } else { + Ok(prefix) + } +} + +#[test] +fn test_determine_address_prefix() { + // Test that the prefix is determined correctly, format is + // (expected_prefix, address) + let test_addresses = vec![ + ("inj", "inj19aenkaj6qhymmt746av8ck4r8euthq3zmxr2r6"), + ("inj", "inj1z354nkau8f0dukgwctq9mladvdwu6zcj8k4928"), + ( + "neutron", + "neutron1eeyntmsq448c68ez06jsy6h2mtjke5tpuplnwtjfwcdznqmw72kswnlmm0", + ), + ( + "neutron", + "neutron1unc0549k2f0d7mjjyfm94fuz2x53wrx3px0pr55va27grdgmspcqgzfr8p", + ), + ( + "sei", + "sei1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsgshtdj", + ), + ( + "terra", + "terra15hlvnufpk8a3gcex09djzkhkz3jg9dpqvv6fvgd0ynudtu2z0qlq2fyfaq", + ), + ("terra", "terra174gu7kg8ekk5gsxdma5jlfcedm653tyg6ayppw"), + ("contract", "contract"), + ("contract", "contract1"), + ("contract", "contract1abc"), + ("wasm", "wasm12345"), + ]; + + for (expected_prefix, address) in test_addresses { + let prefix = determine_address_prefix(address).unwrap(); + assert_eq!(expected_prefix, prefix); + } +} diff --git a/contracts/generator_controller_lite/tests/integration.rs b/contracts/generator_controller_lite/tests/integration.rs new file mode 100644 index 00000000..2104a7ba --- /dev/null +++ b/contracts/generator_controller_lite/tests/integration.rs @@ -0,0 +1,1621 @@ +use astroport::asset::AssetInfo; +use astroport::generator::PoolInfoResponse; +use cosmwasm_std::{attr, Addr, Decimal, StdResult, Uint128}; +use cw_multi_test::{App, ContractWrapper, Executor}; +use generator_controller_lite::astroport; +use std::str::FromStr; + +use crate::astroport::asset::PairInfo; +use astroport_governance::generator_controller_lite::{ + ConfigResponse, ExecuteMsg, NetworkInfo, QueryMsg, VOTERS_MAX_LIMIT, +}; +use astroport_governance::utils::{get_lite_period, LITE_VOTING_PERIOD, MAX_LOCK_TIME, WEEK}; +use astroport_tests_lite::{ + controller_helper::ControllerHelper, escrow_helper::MULTIPLIER, mock_app, TerraAppExtension, +}; +use generator_controller_lite::state::TuneInfo; + +#[test] +fn update_configs() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, None); + + let config = helper.query_config(&mut router).unwrap(); + assert_eq!(config.kick_voters_limit, None); + + // check if user2 cannot update config + let err = helper + .update_blacklisted_limit(&mut router, "user2", Some(4u32)) + .unwrap_err(); + assert_eq!("Unauthorized", err.root_cause().to_string()); + + // successful update config by owner + helper + .update_blacklisted_limit(&mut router, "owner", Some(4u32)) + .unwrap(); + + let config = helper.query_config(&mut router).unwrap(); + assert_eq!(config.kick_voters_limit, Some(4u32)); +} + +#[test] +fn check_kick_holders_works() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, None); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + ]; + + let err = helper + .vote(&mut router, "user1", vec![(pools[0].as_str(), 1000)]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "You can't vote with zero voting power" + ); + + helper.escrow_helper.mint_xastro(&mut router, "owner", 100); + helper.escrow_helper.mint_xastro(&mut router, "user1", 100); + // Create short lock + helper + .escrow_helper + .create_lock(&mut router, "user1", WEEK, 100f32) + .unwrap(); + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + // Votes from user1 + helper + .vote(&mut router, "user1", vec![(pools[0].as_str(), 1000)]) + .unwrap(); + + helper.escrow_helper.mint_xastro(&mut router, "user2", 100); + helper + .escrow_helper + .create_lock(&mut router, "user2", 10 * WEEK, 100f32) + .unwrap(); + + // Votes from user2 + helper + .vote( + &mut router, + "user2", + vec![(pools[0].as_str(), 3000), (pools[1].as_str(), 7000)], + ) + .unwrap(); + + let ve_power = helper + .escrow_helper + .query_user_emissions_vp(&mut router, "user2") + .unwrap(); + let user_info = helper.query_user_info(&mut router, "user2").unwrap(); + assert_eq!( + get_lite_period(router.block_info().time.seconds()).unwrap(), + user_info.vote_period.unwrap() + ); + assert_eq!( + ve_power, + user_info.voting_power.u128() as f32 / MULTIPLIER as f32 + ); + let resp_votes = user_info + .votes + .into_iter() + .map(|(addr, bps)| (addr, bps.into())) + .collect::>(); + assert_eq!( + vec![(pools[0].to_string(), 3000), (pools[1].to_string(), 7000)], + resp_votes + ); + + // Add user2 to the blacklist + let res = helper + .escrow_helper + .update_blacklist(&mut router, Some(vec!["user2".to_string()]), None) + .unwrap(); + assert_eq!( + res.events[1].attributes[1], + attr("action", "update_blacklist") + ); + + // Let's take the period for which the vote was applied. + let current_period = router.block_period() + 1u64; + + // Get pools info before kick holder + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[0].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(40_000_000), res.vxastro_amount); + + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[1].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(70_000_000), res.vxastro_amount); + + // check if blacklisted voters limit exceeded for kick operation + let err = helper + .kick_holders( + &mut router, + "user1", + vec!["user2".to_string(); (VOTERS_MAX_LIMIT + 1) as usize], + ) + .unwrap_err(); + assert_eq!( + "Exceeded voters limit for kick blacklisted/unlocked voters operation!", + err.root_cause().to_string() + ); + + // Removes votes for user2 + helper + .kick_holders(&mut router, "user1", vec!["user2".to_string()]) + .unwrap(); + + let ve_power = helper + .escrow_helper + .query_user_vp(&mut router, "user2") + .unwrap(); + + let user_info = helper.query_user_info(&mut router, "user2").unwrap(); + assert_eq!( + get_lite_period(router.block_info().time.seconds()).unwrap(), + user_info.vote_period.unwrap() + ); + assert_eq!( + ve_power, + user_info.voting_power.u128() as f32 / MULTIPLIER as f32 + ); + assert_eq!(user_info.votes, vec![]); + + // Get pool info after kick holder + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[0].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(10_000_000), res.vxastro_amount); + + let res1 = helper + .query_voted_pool_info_at_period(&mut router, pools[1].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res1.slope); + assert_eq!(Uint128::new(0), res1.vxastro_amount); +} + +#[test] +fn check_kick_unlocked_holders_works() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, None); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + ]; + + let err = helper + .vote(&mut router, "user1", vec![(pools[0].as_str(), 1000)]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "You can't vote with zero voting power" + ); + + helper.escrow_helper.mint_xastro(&mut router, "owner", 100); + helper.escrow_helper.mint_xastro(&mut router, "user1", 100); + // Create short lock + helper + .escrow_helper + .create_lock(&mut router, "user1", WEEK, 100f32) + .unwrap(); + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + // Votes from user1 + helper + .vote(&mut router, "user1", vec![(pools[0].as_str(), 1000)]) + .unwrap(); + + helper.escrow_helper.mint_xastro(&mut router, "user2", 100); + helper + .escrow_helper + .create_lock(&mut router, "user2", 10 * WEEK, 100f32) + .unwrap(); + + // Votes from user2 + helper + .vote( + &mut router, + "user2", + vec![(pools[0].as_str(), 3000), (pools[1].as_str(), 7000)], + ) + .unwrap(); + + let ve_power = helper + .escrow_helper + .query_user_emissions_vp(&mut router, "user2") + .unwrap(); + + let user_info = helper.query_user_info(&mut router, "user2").unwrap(); + assert_eq!( + get_lite_period(router.block_info().time.seconds()).unwrap(), + user_info.vote_period.unwrap() + ); + assert_eq!( + ve_power, + user_info.voting_power.u128() as f32 / MULTIPLIER as f32 + ); + + let resp_votes = user_info + .votes + .into_iter() + .map(|(addr, bps)| (addr, bps.into())) + .collect::>(); + assert_eq!( + vec![(pools[0].to_string(), 3000), (pools[1].to_string(), 7000)], + resp_votes + ); + + // Let's take the period for which the vote was applied. + let current_period = router.block_period() + 1u64; + // Get pools info before kick holder + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[0].as_str(), current_period) + .unwrap(); + + // We should see 40_000_000 as the vxASTRO amount here because: + // User1 voted with 10% of the 100_000_000 total voting power + // User2 voted with 30% of the 100_000_000 total voting power + // Total voting power is 40_000_000 + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(40_000_000), res.vxastro_amount); + + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[1].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(70_000_000), res.vxastro_amount); + + // Unlock user2, which results in an immediate kick + helper.escrow_helper.unlock(&mut router, "user2").unwrap(); + + // check if blacklisted voters limit exceeded for kick operation + let err = helper + .kick_unlocked_holders( + &mut router, + "user1", + vec!["user2".to_string(); (VOTERS_MAX_LIMIT + 1) as usize], + ) + .unwrap_err(); + assert_eq!( + "Exceeded voters limit for kick blacklisted/unlocked voters operation!", + err.root_cause().to_string() + ); + + // Removes votes for user2 + // Not strictly needed as the user is kicked immediately when unlock starts + helper + .kick_unlocked_holders(&mut router, "user1", vec!["user2".to_string()]) + .unwrap(); + + let ve_power = helper + .escrow_helper + .query_user_vp(&mut router, "user2") + .unwrap(); + + let user_info = helper.query_user_info(&mut router, "user2").unwrap(); + assert_eq!( + get_lite_period(router.block_info().time.seconds()).unwrap(), + user_info.vote_period.unwrap() + ); + assert_eq!( + ve_power, + user_info.voting_power.u128() as f32 / MULTIPLIER as f32 + ); + // All votes should be removed for this user + assert_eq!(user_info.votes, vec![]); + + // Get pool info after kick holder + // We should see 10_000_000 as the vxASTRO amount here because: + // User1 voted with 10% of the 100_000_000 total voting power + // User2 voted with 30% of the 100_000_000 total voting power + // User2 was kicked removing the 30% of the 100_000_000 total voting power + // Total voting power is now 10_000_000 + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[0].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(10_000_000), res.vxastro_amount); + + let res1 = helper + .query_voted_pool_info_at_period(&mut router, pools[1].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res1.slope); + assert_eq!(Uint128::new(0), res1.vxastro_amount); +} + +#[test] +fn check_kick_unlocked_outpost_holders_works() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, Some("hub".to_string())); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + ]; + + let voter1 = "outpost_voter1".to_string(); + let voter1_power = Uint128::from(50_000_000u64); + let voter2 = "outpost_voter2".to_string(); + let voter2_power = Uint128::from(100_000_000u64); + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 8000), (pools[1].as_str(), 1000)], + ) + .unwrap(); + + // Votes from user2 + helper + .outpost_vote( + &mut router, + "hub", + voter2, + voter2_power, + vec![(pools[0].as_str(), 2000)], + ) + .unwrap(); + + let user_info = helper + .query_user_info(&mut router, voter1.as_ref()) + .unwrap(); + assert_eq!( + get_lite_period(router.block_info().time.seconds()).unwrap(), + user_info.vote_period.unwrap() + ); + + let resp_votes = user_info + .votes + .into_iter() + .map(|(addr, bps)| (addr, bps.into())) + .collect::>(); + assert_eq!( + vec![(pools[0].to_string(), 8000), (pools[1].to_string(), 1000)], + resp_votes + ); + + // Let's take the period for which the vote was applied. + let current_period = router.block_period() + 1u64; + + // Get pools info before kick holder + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[0].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(60_000_000), res.vxastro_amount); + + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[1].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(5_000_000), res.vxastro_amount); + + // Check that only Hub can call this + let err = helper + .kick_unlocked_outpost_holders(&mut router, "not_hub", voter1.to_string()) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + helper + .kick_unlocked_outpost_holders(&mut router, "hub", voter1.to_string()) + .unwrap(); + + let user_info = helper + .query_user_info(&mut router, voter1.as_ref()) + .unwrap(); + assert_eq!( + get_lite_period(router.block_info().time.seconds()).unwrap(), + user_info.vote_period.unwrap() + ); + + // Get pool info after kick holder + let res = helper + .query_voted_pool_info_at_period(&mut router, pools[0].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res.slope); + assert_eq!(Uint128::new(20_000_000), res.vxastro_amount); + + // Since Outpost user 1 was kicked, their voting power should be removed for pools[1] + let res1 = helper + .query_voted_pool_info_at_period(&mut router, pools[1].as_str(), current_period) + .unwrap(); + assert_eq!(Uint128::new(0), res1.slope); + assert_eq!(Uint128::new(0), res1.vxastro_amount); +} + +#[test] +fn check_kick_unlocked_outpost_holders_unauthorized() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, Some("hub".to_string())); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + ]; + + let voter1 = "outpost_voter1".to_string(); + let voter1_power = Uint128::from(50_000_000u64); + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 8000), (pools[1].as_str(), 1000)], + ) + .unwrap(); + + // Check that only Hub can call this + let err = helper + .kick_unlocked_outpost_holders(&mut router, "not_hub", voter1) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); +} + +#[test] +fn check_vote_works() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, None); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + ]; + + let err = helper + .vote(&mut router, "user1", vec![(pools[0].as_str(), 1000)]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "You can't vote with zero voting power" + ); + + helper.escrow_helper.mint_xastro(&mut router, "owner", 100); + helper.escrow_helper.mint_xastro(&mut router, "user1", 100); + // Create short lock + helper + .escrow_helper + .create_lock(&mut router, "user1", WEEK, 100f32) + .unwrap(); + let err = helper + .vote(&mut router, "user1", vec![(pools[0].as_str(), 1000)]) + .unwrap_err(); + assert_eq!("Whitelist cannot be empty!", err.root_cause().to_string()); + + let err = helper + .update_whitelist(&mut router, "user1", Some(vec![pools[0].to_string()]), None) + .unwrap_err(); + assert_eq!("Unauthorized", err.root_cause().to_string()); + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + helper + .vote(&mut router, "user1", vec![(pools[0].as_str(), 1000)]) + .unwrap(); + + helper.escrow_helper.mint_xastro(&mut router, "user2", 100); + helper + .escrow_helper + .create_lock(&mut router, "user2", 10 * WEEK, 100f32) + .unwrap(); + + // Bps is > 10000 + let err = helper + .vote(&mut router, "user2", vec![(pools[1].as_str(), 10001)]) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Basic points conversion error. 10001 > 10000" + ); + + // Bps sum is > 10000 + let err = helper + .vote( + &mut router, + "user2", + vec![(pools[0].as_str(), 3000), (pools[1].as_str(), 8000)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Basic points sum exceeds limit" + ); + + // Duplicated pools + let err = helper + .vote( + &mut router, + "user2", + vec![(pools[0].as_str(), 3000), (pools[0].as_str(), 7000)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Votes contain duplicated pool addresses" + ); + + // Valid votes + helper + .vote( + &mut router, + "user2", + vec![(pools[0].as_str(), 3000), (pools[1].as_str(), 7000)], + ) + .unwrap(); + + let err = helper + .vote( + &mut router, + "user2", + vec![(pools[0].as_str(), 7000), (pools[1].as_str(), 3000)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "You can only run this action once in a voting period" + ); + + let ve_power = helper + .escrow_helper + .query_user_emissions_vp(&mut router, "user2") + .unwrap(); + let user_info = helper.query_user_info(&mut router, "user2").unwrap(); + assert_eq!( + get_lite_period(router.block_info().time.seconds()).unwrap(), + user_info.vote_period.unwrap() + ); + assert_eq!( + ve_power, + user_info.voting_power.u128() as f32 / MULTIPLIER as f32 + ); + let resp_votes = user_info + .votes + .into_iter() + .map(|(addr, bps)| (addr, bps.into())) + .collect::>(); + assert_eq!( + vec![(pools[0].to_string(), 3000), (pools[1].to_string(), 7000)], + resp_votes + ); + + router.next_block(LITE_VOTING_PERIOD); + // In the next period the user will be able to vote again + helper + .vote( + &mut router, + "user2", + vec![(pools[0].as_str(), 500), (pools[1].as_str(), 9500)], + ) + .unwrap(); +} + +#[test] +fn check_outpost_vote_no_hub() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, None); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + ]; + + let voter1 = "voter1".to_string(); + let voter1_power = Uint128::from(100_000u64); + + let err = helper + .outpost_vote( + &mut router, + "not_hub", + voter1, + voter1_power, + vec![(pools[0].as_str(), 1000)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Sender is not the Hub installed" + ); +} + +#[test] +fn check_outpost_vote_unauthorised() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, Some("hub".to_string())); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + ]; + + let voter1 = "voter1".to_string(); + let voter1_power = Uint128::from(100_000u64); + + let err = helper + .outpost_vote( + &mut router, + "not_hub", + voter1, + voter1_power, + vec![(pools[0].as_str(), 1000)], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); +} + +#[test] +fn check_outpost_vote_works() { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner, Some("hub".to_string())); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + ]; + + let voter1 = "voter1".to_string(); + let voter1_power = Uint128::from(100_000u64); + + let err = helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + Uint128::zero(), + vec![(pools[0].as_str(), 1000)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "You can't vote with zero voting power" + ); + + let err = helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 1000)], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Whitelist cannot be empty!"); + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + // Bps is > 10000 + let err = helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 10001)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Basic points conversion error. 10001 > 10000" + ); + + let err = helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 3000), (pools[1].as_str(), 8000)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Basic points sum exceeds limit" + ); + + let err = helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 3000), (pools[0].as_str(), 7000)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Votes contain duplicated pool addresses" + ); + + // Valid votes + helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 3000), (pools[1].as_str(), 7000)], + ) + .unwrap(); + + let err = helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 3000), (pools[1].as_str(), 7000)], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "You can only run this action once in a voting period" + ); + + let user_info = helper + .query_user_info(&mut router, voter1.as_ref()) + .unwrap(); + assert_eq!( + get_lite_period(router.block_info().time.seconds()).unwrap(), + user_info.vote_period.unwrap() + ); + + let resp_votes = user_info + .votes + .into_iter() + .map(|(addr, bps)| (addr, bps.into())) + .collect::>(); + assert_eq!( + vec![(pools[0].to_string(), 3000), (pools[1].to_string(), 7000)], + resp_votes + ); + + router.next_block(LITE_VOTING_PERIOD); + // In the next period the user will be able to vote again + helper + .outpost_vote( + &mut router, + "hub", + voter1.clone(), + voter1_power, + vec![(pools[0].as_str(), 3000), (pools[1].as_str(), 7000)], + ) + .unwrap(); +} + +fn create_unregistered_pool( + router: &mut App, + helper: &mut ControllerHelper, +) -> StdResult { + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_pair::contract::execute, + astroport_pair::contract::instantiate, + astroport_pair::contract::query, + ) + .with_reply_empty(astroport_pair::contract::reply), + ); + + let pair_code_id = router.store_code(pair_contract); + + let test_token1 = helper.init_cw20_token(router, "TST").unwrap(); + let test_token2 = helper.init_cw20_token(router, "TSB").unwrap(); + + let pair_addr = router + .instantiate_contract( + pair_code_id, + Addr::unchecked("owner"), + &astroport::pair::InstantiateMsg { + asset_infos: vec![ + AssetInfo::Token { + contract_addr: test_token1, + }, + AssetInfo::Token { + contract_addr: test_token2, + }, + ], + token_code_id: 1, + factory_addr: helper.factory.to_string(), + init_params: None, + }, + &[], + "Unregistered pair".to_string(), + None, + ) + .unwrap(); + + let res: PairInfo = router + .wrap() + .query_wasm_smart(pair_addr, &astroport::pair::QueryMsg::Pair {})?; + + Ok(res) +} + +#[test] +fn check_tuning() { + let mut router = mock_app(); + let owner = "owner"; + let owner_addr = Addr::unchecked(owner); + let mut helper = ControllerHelper::init(&mut router, &owner_addr, None); + let user1 = "user1"; + let user2 = "user2"; + let user3 = "user3"; + let ve_locks = vec![(user1, 10), (user2, 5), (user3, 50)]; + + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "FOO", "ADN") + .unwrap(), + ]; + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + let err = helper + .update_whitelist(&mut router, "owner", Some(vec![pools[0].to_string()]), None) + .unwrap_err(); + assert_eq!("Generic error: The resulting whitelist contains duplicated pools. It's either provided 'add' list contains duplicated pools or some of the added pools are already whitelisted.", err.root_cause().to_string()); + + let config_resp = helper.query_config(&mut router).unwrap(); + assert_eq!(config_resp.whitelisted_pools, pools); + + for (user, duration) in ve_locks { + helper.escrow_helper.mint_xastro(&mut router, user, 1000); + helper + .escrow_helper + .create_lock(&mut router, user, duration * WEEK, 100f32) + .unwrap(); + } + + let res = create_unregistered_pool(&mut router, &mut helper).unwrap(); + let err = helper + .vote( + &mut router, + user1, + vec![ + (pools[0].as_str(), 5000), + (pools[1].as_str(), 4000), + (res.liquidity_token.as_str(), 1000), + ], + ) + .unwrap_err(); + assert_eq!( + "Pool is not whitelisted: wasm1contract25", + err.root_cause().to_string() + ); + + let err = helper + .vote( + &mut router, + user1, + vec![ + (pools[0].as_str(), 5000), + (pools[1].as_str(), 2000), + (pools[1].as_str(), 2000), + ], + ) + .unwrap_err(); + assert_eq!( + "Votes contain duplicated pool addresses", + err.root_cause().to_string() + ); + + helper + .vote( + &mut router, + user1, + vec![(pools[0].as_str(), 5000), (pools[1].as_str(), 5000)], + ) + .unwrap(); + + helper + .vote( + &mut router, + user2, + vec![ + (pools[0].as_str(), 5000), + (pools[1].as_str(), 2000), + (pools[2].as_str(), 3000), + ], + ) + .unwrap(); + helper + .vote( + &mut router, + user3, + vec![ + (pools[0].as_str(), 2000), + (pools[1].as_str(), 3000), + (pools[2].as_str(), 5000), + ], + ) + .unwrap(); + + // The contract was just created so we need to wait for the next period + let err = helper.tune(&mut router).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "You can only run this action once in a voting period" + ); + + // Periods are two weeks, so this should fail as well + router.next_block(WEEK); + let err = helper.tune(&mut router).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "You can only run this action once in a voting period" + ); + + // This should now be the next period + router.next_block(WEEK); + helper.tune(&mut router).unwrap(); + + let resp: TuneInfo = router + .wrap() + .query_wasm_smart(helper.controller.clone(), &QueryMsg::TuneInfo {}) + .unwrap(); + assert_eq!(resp.tune_period, router.block_period()); + assert_eq!(resp.pool_alloc_points.len(), pools.len()); + let total_apoints: u128 = resp + .pool_alloc_points + .iter() + .cloned() + .map(|(_, apoints)| apoints.u128()) + .sum(); + assert_eq!(total_apoints, 300_000_000); + + router.next_block(2 * WEEK); + // Reduce pools limit 5 -> 2 (5 is initial limit in integration tests) + let limit = 2u64; + let err = router + .execute_contract( + Addr::unchecked("somebody"), + helper.controller.clone(), + &ExecuteMsg::ChangePoolsLimit { limit }, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Unauthorized"); + + router + .execute_contract( + owner_addr.clone(), + helper.controller.clone(), + &ExecuteMsg::ChangePoolsLimit { limit }, + &[], + ) + .unwrap(); + + let err = router + .execute_contract( + owner_addr.clone(), + helper.controller.clone(), + &ExecuteMsg::ChangePoolsLimit { limit: 101 }, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Invalid pool number: 101. Must be within [2, 100] range" + ); + + helper.tune(&mut router).unwrap(); + + let resp: TuneInfo = router + .wrap() + .query_wasm_smart(helper.controller.clone(), &QueryMsg::TuneInfo {}) + .unwrap(); + assert_eq!(resp.tune_period, router.block_period()); + assert_eq!(resp.pool_alloc_points.len(), limit as usize); + let total_apoints: u128 = resp + .pool_alloc_points + .iter() + .cloned() + .map(|(_, apoints)| apoints.u128()) + .sum(); + assert_eq!(total_apoints, 220_000_000); + + // Check alloc points are properly set in generator + for (pool_addr, apoints) in resp.pool_alloc_points { + let resp: PoolInfoResponse = router + .wrap() + .query_wasm_smart( + helper.generator.clone(), + &astroport::generator::QueryMsg::PoolInfo { + lp_token: pool_addr.to_string(), + }, + ) + .unwrap(); + assert_eq!(apoints, resp.alloc_point) + } + + // Check the last pool did not receive alloc points + let generator_resp: PoolInfoResponse = router + .wrap() + .query_wasm_smart( + helper.generator.clone(), + &astroport::generator::QueryMsg::PoolInfo { + lp_token: pools[2].to_string(), + }, + ) + .unwrap(); + assert_eq!(generator_resp.alloc_point.u128(), 0) +} + +#[test] +fn check_bad_pools_filtering() { + let mut router = mock_app(); + let owner = "owner"; + let owner_addr = Addr::unchecked(owner); + let helper = ControllerHelper::init(&mut router, &owner_addr, None); + let user = "user1"; + + let foo_token = helper.init_cw20_token(&mut router, "FOO").unwrap(); + let bar_token = helper.init_cw20_token(&mut router, "BAR").unwrap(); + let adn_token = helper.init_cw20_token(&mut router, "ADN").unwrap(); + let pools = vec![ + helper + .create_pool(&mut router, &foo_token, &bar_token) + .unwrap(), + helper + .create_pool(&mut router, &foo_token, &adn_token) + .unwrap(), + helper + .create_pool(&mut router, &bar_token, &adn_token) + .unwrap(), + ]; + + helper.escrow_helper.mint_xastro(&mut router, user, 1000); + helper + .escrow_helper + .create_lock(&mut router, user, 10 * WEEK, 100f32) + .unwrap(); + + // We must be able to add any pool to the whitelist as we can't validate + // pools on other chains + let result = helper.update_whitelist( + &mut router, + "owner", + Some(vec![("random_pool".to_string())]), + None, + ); + assert!(result.is_ok()); + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + helper + .vote(&mut router, user, vec![(pools[0].as_str(), 5000)]) + .unwrap(); + + router.next_block(2 * WEEK); + + helper.tune(&mut router).unwrap(); + let resp: TuneInfo = router + .wrap() + .query_wasm_smart(helper.controller.clone(), &QueryMsg::TuneInfo {}) + .unwrap(); + // There was only one valid pool + assert_eq!(resp.pool_alloc_points.len(), 1); + + router.next_block(2 * WEEK); + + // Deregister first pair + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: foo_token.clone(), + }, + AssetInfo::Token { + contract_addr: bar_token.clone(), + }, + ]; + router + .execute_contract( + owner_addr.clone(), + helper.factory.clone(), + &astroport::factory::ExecuteMsg::Deregister { + asset_infos: asset_infos.to_vec(), + }, + &[], + ) + .unwrap(); + + // We can vote for deregistered pool as we can't validate the information + // from other chains + let result = helper.vote(&mut router, user, vec![(pools[0].as_str(), 10000)]); + assert!(result.is_ok()); + + // Tune should fail as the pair is not registered in the generator + let err = helper.tune(&mut router).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: The pair is not registered: wasm1contract10-wasm1contract11" + ); + + router.next_block(2 * WEEK); + + // Blocking FOO token so pair[0] and pair[1] become blocked as well + let foo_asset_info = AssetInfo::Token { + contract_addr: foo_token.clone(), + }; + router + .execute_contract( + owner_addr.clone(), + helper.generator.clone(), + &astroport::generator::ExecuteMsg::UpdateBlockedTokenslist { + add: Some(vec![foo_asset_info]), + remove: None, + }, + &[], + ) + .unwrap(); + + // Voting for 2 valid pools + helper + .vote( + &mut router, + user, + vec![(pools[1].as_str(), 1000), (pools[2].as_str(), 8000)], + ) + .unwrap(); + + router.next_block(WEEK); + // Tune should fail as we have a token blocked in the generator + let err = helper.tune(&mut router).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Token wasm1contract10 is blocked!" + ); + + let resp: TuneInfo = router + .wrap() + .query_wasm_smart(helper.controller.clone(), &QueryMsg::TuneInfo {}) + .unwrap(); + // Only one pool is eligible to receive alloc points + assert_eq!(resp.pool_alloc_points.len(), 1); + let total_apoints: u128 = resp + .pool_alloc_points + .iter() + .cloned() + .map(|(_, apoints)| apoints.u128()) + .sum(); + assert_eq!(total_apoints, 50_000_000) +} + +#[test] +fn check_update_owner() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut app, &owner, None); + + let new_owner = String::from("new_owner"); + + // New owner + let msg = ExecuteMsg::ProposeNewOwner { + new_owner: new_owner.clone(), + expires_in: 100, // seconds + }; + + // Unauthed check + let err = app + .execute_contract( + Addr::unchecked("not_owner"), + helper.controller.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.controller.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner + app.execute_contract( + Addr::unchecked("owner"), + helper.controller.clone(), + &msg, + &[], + ) + .unwrap(); + + // Claim from invalid addr + let err = app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.controller.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim ownership + app.execute_contract( + Addr::unchecked(new_owner.clone()), + helper.controller.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); + + // Let's query the contract state + let msg = QueryMsg::Config {}; + let res: ConfigResponse = app + .wrap() + .query_wasm_smart(&helper.controller, &msg) + .unwrap(); + + assert_eq!(res.owner, new_owner) +} + +#[test] +fn check_main_pool() { + let mut router = mock_app(); + let owner_addr = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner_addr, None); + let pools = vec![ + helper + .create_pool_with_tokens(&mut router, "FOO", "BAR") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "BAR", "ADN") + .unwrap(), + helper + .create_pool_with_tokens(&mut router, "FOO", "ADN") + .unwrap(), + ]; + + helper.escrow_helper.mint_xastro(&mut router, "owner", 100); + + for user in ["user1", "user2"] { + helper.escrow_helper.mint_xastro(&mut router, user, 100); + helper + .escrow_helper + .create_lock(&mut router, user, MAX_LOCK_TIME, 100f32) + .unwrap(); + } + + helper + .update_whitelist( + &mut router, + "owner", + Some(pools.iter().map(|el| el.to_string()).collect()), + None, + ) + .unwrap(); + + helper + .vote( + &mut router, + "user1", + vec![ + (pools[0].as_str(), 1000), + (pools[1].as_str(), 5000), + (pools[2].as_str(), 4000), + ], + ) + .unwrap(); + let block_period = router.block_period(); + let main_pool_info = helper + .query_voted_pool_info_at_period(&mut router, pools[0].as_str(), block_period + 2) + .unwrap(); + assert_eq!(main_pool_info.vxastro_amount.u128(), 10_000_000); + + let err = helper + .update_main_pool( + &mut router, + "owner", + Some(&pools[0]), + Some(Decimal::zero()), + false, + ) + .unwrap_err(); + assert_eq!( + &err.root_cause().to_string(), + "main_pool_min_alloc should be more than 0 and less than 1" + ); + let err = helper + .update_main_pool( + &mut router, + "owner", + Some(&pools[0]), + Some(Decimal::one()), + false, + ) + .unwrap_err(); + assert_eq!( + &err.root_cause().to_string(), + "main_pool_min_alloc should be more than 0 and less than 1" + ); + helper + .update_main_pool( + &mut router, + "owner", + Some(&pools[0]), + Decimal::from_str("0.3").ok(), + false, + ) + .unwrap(); + + // From now users can't vote for the main pool + let err = helper + .vote( + &mut router, + "user2", + vec![(pools[0].as_str(), 1000), (pools[1].as_str(), 9000)], + ) + .unwrap_err(); + assert_eq!( + &err.root_cause().to_string(), + "wasm1contract13 is the main pool. Voting or whitelisting the main pool is prohibited." + ); + + router + .execute_contract( + owner_addr.clone(), + helper.controller.clone(), + &ExecuteMsg::ChangePoolsLimit { limit: 2 }, + &[], + ) + .unwrap(); + + router.next_block(2 * WEEK); + helper.tune(&mut router).unwrap(); + + let resp: TuneInfo = router + .wrap() + .query_wasm_smart(helper.controller.clone(), &QueryMsg::TuneInfo {}) + .unwrap(); + // 2 (limit) + 1 (main pool) + assert_eq!(resp.pool_alloc_points.len(), 3_usize); + let total_apoints: Uint128 = resp + .pool_alloc_points + .iter() + .map(|(_, apoints)| apoints) + .sum(); + assert_eq!(total_apoints.u128(), 128571428); + let main_pool_contribution = resp + .pool_alloc_points + .iter() + .find(|(pool, _)| pool == &pools[0]); + assert_eq!( + main_pool_contribution.unwrap().1, + (total_apoints * Decimal::from_str("0.3").unwrap()) + ); + + // Remove the main pool + helper + .update_main_pool(&mut router, "owner", None, None, true) + .unwrap(); + + router.next_block(2 * WEEK); + helper.tune(&mut router).unwrap(); + + let resp: TuneInfo = router + .wrap() + .query_wasm_smart(helper.controller.clone(), &QueryMsg::TuneInfo {}) + .unwrap(); + // The main pool was removed + assert_eq!(resp.pool_alloc_points.len(), 2_usize); +} + +#[test] +fn check_add_network() { + let mut router = mock_app(); + let owner_addr = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner_addr, None); + + // Attempt to duplicate the native/home network + let add_network = NetworkInfo { + address_prefix: "unknown".to_string(), + generator_address: Addr::unchecked("wasm1contract"), + ibc_channel: None, + }; + + // Test success + let result = helper.update_networks(&mut router, "owner", Some(vec![add_network]), None); + assert!(result.is_err()); + + let add_network = NetworkInfo { + address_prefix: "unknown".to_string(), + generator_address: Addr::unchecked("wasmx1contract"), + ibc_channel: None, + }; + + // Test success + let result = helper.update_networks(&mut router, "owner", Some(vec![add_network]), None); + assert!(result.is_ok()); + + let add_network = NetworkInfo { + address_prefix: "unknown".to_string(), + generator_address: Addr::unchecked("wasm1contract"), + ibc_channel: None, + }; + + // Test for duplicate + let err = helper + .update_networks(&mut router, "owner", Some(vec![add_network]), None) + .unwrap_err(); + assert_eq!( + "Generic error: The resulting whitelist contains duplicated prefixes. It's either provided 'add' list contains duplicated prefixes or some of the added prefixes are already whitelisted.", + err.root_cause().to_string() + ); +} + +#[test] +fn check_remove_network() { + let mut router = mock_app(); + let owner_addr = Addr::unchecked("owner"); + let helper = ControllerHelper::init(&mut router, &owner_addr, None); + + let add_network = NetworkInfo { + address_prefix: "unknown".to_string(), + generator_address: Addr::unchecked("wasmx1contract"), + ibc_channel: None, + }; + + // Add network + helper + .update_networks(&mut router, "owner", Some(vec![add_network]), None) + .unwrap(); + + // Test remove invalid network + helper + .update_networks(&mut router, "owner", None, Some(vec!["testx".to_string()])) + .unwrap(); + + // We'll still have the default and the added network + let config = helper.query_config(&mut router).unwrap(); + let prefixes: Vec = config + .whitelisted_networks + .into_iter() + .map(|network_info| network_info.address_prefix) + .collect(); + assert_eq!(prefixes, vec!["wasm".to_string(), "wasmx".to_string()]); + + // Test remove native/home network, this should not succeed + let err = helper + .update_networks(&mut router, "owner", None, Some(vec!["wasm".to_string()])) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: Cannot remove the native network with prefix wasm".to_string() + ); + + // Attempt to remove the network we added, should pass + helper + .update_networks(&mut router, "owner", None, Some(vec!["wasmx".to_string()])) + .unwrap(); + + // We'll still have the default and the added network + let config = helper.query_config(&mut router).unwrap(); + let prefixes: Vec = config + .whitelisted_networks + .into_iter() + .map(|network_info| network_info.address_prefix) + .collect(); + assert_eq!(prefixes, vec!["wasm".to_string()]); +} diff --git a/contracts/generator_controller_lite/tests/math_test.rs b/contracts/generator_controller_lite/tests/math_test.rs new file mode 100644 index 00000000..73187e5b --- /dev/null +++ b/contracts/generator_controller_lite/tests/math_test.rs @@ -0,0 +1,394 @@ +use std::cmp::Ordering; +use std::collections::HashMap; + +use anyhow::Result; +use cosmwasm_std::{Addr, Uint128}; +use cw_multi_test::{App, AppResponse, Executor}; +use itertools::Itertools; +use proptest::prelude::*; + +use astroport_governance::generator_controller::ExecuteMsg; +use astroport_governance::utils::{MAX_LOCK_TIME, WEEK}; +use generator_controller_lite::bps::BasicPoints; +use Event::*; +use VeEvent::*; + +use astroport_tests_lite::{ + controller_helper::ControllerHelper, escrow_helper::MULTIPLIER, mock_app, TerraAppExtension, +}; + +#[derive(Clone, Debug)] +enum Event { + Vote(Vec<((String, String), u16)>), + TunePools, + ChangePoolLimit(u64), +} + +#[derive(Clone, Debug)] +enum VeEvent { + CreateLock(f64, u64), + ExtendLock(f64), + Withdraw, +} + +struct Simulator { + user_votes: HashMap>, + locks: HashMap, + helper: ControllerHelper, + router: App, + owner: Addr, + limit: u64, + pairs: HashMap<(String, String), Addr>, +} + +impl Simulator { + pub fn init>(users: &[T]) -> Self { + let mut router = mock_app(); + let owner = Addr::unchecked("owner"); + Self { + helper: ControllerHelper::init(&mut router, &owner, None), + user_votes: users + .iter() + .cloned() + .map(|user| (user.into(), HashMap::new())) + .collect(), + locks: HashMap::new(), + limit: 5, + pairs: HashMap::new(), + router, + owner, + } + } + + fn escrow_events_router(&mut self, user: &str, event: VeEvent) { + // We don't check voting escrow errors + let _ = match event { + CreateLock(amount, interval) => { + self.helper + .escrow_helper + .mint_xastro(&mut self.router, user, amount as u64); + self.helper.escrow_helper.create_lock( + &mut self.router, + user, + interval, + amount as f32, + ) + } + ExtendLock(amount) => { + self.helper + .escrow_helper + .mint_xastro(&mut self.router, user, amount as u64); + self.helper + .escrow_helper + .extend_lock_amount(&mut self.router, user, amount as f32) + } + Withdraw => self.helper.escrow_helper.withdraw(&mut self.router, user), + }; + } + + fn vote(&mut self, user: &str, votes: Vec<((String, String), u16)>) -> Result { + let votes: Vec<_> = votes + .iter() + .map(|(tokens, bps)| { + let addr = self + .pairs + .get(tokens) + .cloned() + .expect(&format!("Pair {}-{} was not found", tokens.0, tokens.1)); + (addr, *bps) + }) + .collect(); + self.helper + .vote(&mut self.router, user, votes.clone()) + .map(|response| { + let vp = self + .helper + .escrow_helper + .query_user_vp(&mut self.router, user) + .unwrap(); + self.locks + .insert(user.to_string(), (self.router.block_period(), vp)); + self.user_votes.insert(user.to_string(), HashMap::new()); + for (pool, bps) in votes { + self.user_votes + .get_mut(user) + .expect("User not found!") + .insert(pool.to_string(), bps); + } + let user_info = self.helper.query_user_info(&mut self.router, user).unwrap(); + let total_apoints: u16 = user_info + .votes + .iter() + .cloned() + .map(|pair| u16::from(pair.1)) + .sum(); + if total_apoints > 10000 { + panic!("{} > 10000", total_apoints) + } + assert_eq!( + user_info.vote_period.unwrap(), + self.router.block_info().time.seconds() + ); + response + }) + } + + fn change_pool_limit(&mut self, limit: u64) -> Result { + self.router + .execute_contract( + self.owner.clone(), + self.helper.controller.clone(), + &ExecuteMsg::ChangePoolsLimit { limit }, + &[], + ) + .map(|response| { + self.limit = limit; + response + }) + } + + pub fn event_router(&mut self, user: &str, event: Event) { + println!("User {} Event {:?}", user, event); + match event { + Vote(votes) => { + if let Err(err) = self.vote(user, votes) { + println!("{}", err); + } + } + TunePools => { + if let Err(err) = self.helper.tune(&mut self.router) { + println!("{}", err); + } + } + ChangePoolLimit(limit) => { + if let Err(err) = self.change_pool_limit(limit) { + println!("{}", err); + } + } + } + } + + pub fn register_pools(&mut self, tokens: &[String]) { + for token1 in tokens { + for token2 in tokens { + if matches!(token1.cmp(token2), Ordering::Less) { + self.pairs.insert( + (token1.to_string(), token2.to_string()), + self.helper + .create_pool_with_tokens(&mut self.router, token1, token2) + .unwrap(), + ); + } + } + } + } + + pub fn simulate_case( + &mut self, + tokens: &[String], + ve_events_tuples: &[(usize, String, VeEvent)], + events_tuples: &[(usize, String, Event)], + ) { + self.register_pools(tokens); + let pools = self + .pairs + .values() + .map(|pool_addr| pool_addr.to_string()) + .collect_vec(); + + let mut events: Vec> = vec![vec![]; MAX_PERIOD + 1]; + let mut ve_events: Vec> = vec![vec![]; MAX_PERIOD + 1]; + + for (period, user, event) in events_tuples.iter().cloned() { + events[period].push((user, event)); + } + for (period, user, event) in ve_events_tuples.iter().cloned() { + ve_events[period].push((user, event)) + } + + for period in 0..events.len() { + // vxASTRO events + if let Some(period_events) = ve_events.get(period) { + for (user, event) in period_events { + self.escrow_events_router(user, event.clone()) + } + } + // Generator controller events + if let Some(period_events) = events.get(period) { + if !period_events.is_empty() { + println!("Period {}:", period); + } + for (user, event) in period_events { + self.event_router(user, event.clone()) + } + } + + let mut voted_pools: HashMap = HashMap::new(); + + // Checking calculations + for user in self.user_votes.keys() { + let votes = self.user_votes.get(user).unwrap(); + if let Some((_, vp)) = self.locks.get(user) { + let user_vp = Uint128::from((*vp * MULTIPLIER as f32) as u128); + let user_vp = user_vp.u128() as f32 / MULTIPLIER as f32; + votes.iter().for_each(|(pool, &bps)| { + let vp = voted_pools.entry(pool.clone()).or_default(); + *vp += (bps as f32 / BasicPoints::MAX as f32) * user_vp + }) + } + } + let block_period = self.router.block_period(); + for pool_addr in &pools { + let pool_vp = self + .helper + .query_voted_pool_info_at_period(&mut self.router, pool_addr, block_period + 1) + .unwrap() + .vxastro_amount + .u128() as f32 + / MULTIPLIER as f32; + let real_vp = voted_pools.get(pool_addr).cloned().unwrap_or(0f32); + if (pool_vp - real_vp).abs() >= 10e-3 { + assert_eq!(pool_vp, real_vp, "Period: {}, pool: {}", period, pool_addr) + } + } + self.router.next_block(WEEK); + } + } +} + +const MAX_PERIOD: usize = 20; +const MAX_USERS: usize = 10; +const MAX_POOLS: usize = 5; +const MAX_EVENTS: usize = 100; + +fn escrow_events_strategy() -> impl Strategy { + prop_oneof![ + Just(VeEvent::Withdraw), + (1f64..=100f64).prop_map(VeEvent::ExtendLock), + ((1f64..=100f64), WEEK..MAX_LOCK_TIME).prop_map(|(a, b)| VeEvent::CreateLock(a, b)), + ] +} + +fn vote_strategy(tokens: Vec) -> impl Strategy { + prop::collection::vec( + (prop::sample::subsequence(tokens, 2), 1..=2500u16), + 1..MAX_POOLS, + ) + .prop_filter_map( + "Accepting only BPS sum <= 10000", + |vec: Vec<(Vec, u16)>| { + let votes = vec + .iter() + .into_grouping_map_by(|(pair, _)| { + let mut pair = pair.clone(); + pair.sort(); + (pair[0].clone(), pair[1].clone()) + }) + .aggregate(|acc, _, (_, val)| Some(acc.unwrap_or(0) + *val)) + .into_iter() + .collect_vec(); + if votes.iter().map(|(_, bps)| bps).sum::() <= 10000 { + Some(Event::Vote(votes)) + } else { + None + } + }, + ) +} + +fn controller_events_strategy(tokens: Vec) -> impl Strategy { + prop_oneof![ + Just(Event::TunePools), + (2..=MAX_POOLS as u64).prop_map(Event::ChangePoolLimit), + vote_strategy(tokens) + ] +} + +fn generate_cases() -> impl Strategy< + Value = ( + Vec, + Vec, + Vec<(usize, String, VeEvent)>, + Vec<(usize, String, Event)>, + ), +> { + let tokens_strategy = + prop::collection::hash_set("[A-Z]{3}", MAX_POOLS * MAX_POOLS / 2 - MAX_POOLS); + let users_strategy = prop::collection::vec("[a-z]{10}", 1..MAX_USERS); + (users_strategy, tokens_strategy).prop_flat_map(|(users, tokens)| { + ( + Just(users.clone()), + Just(tokens.iter().cloned().collect()), + prop::collection::vec( + ( + 1..=MAX_PERIOD, + prop::sample::select(users.clone()), + escrow_events_strategy(), + ), + 0..MAX_EVENTS, + ), + prop::collection::vec( + ( + 1..=MAX_PERIOD, + prop::sample::select(users), + controller_events_strategy(tokens.iter().cloned().collect_vec()), + ), + 0..MAX_EVENTS, + ), + ) + }) +} + +proptest! { + #[test] + fn run_simulations( + case in generate_cases() + ) { + let (users, tokens, ve_events_tuples, events_tuples) = case; + let mut simulator = Simulator::init(&users); + simulator.simulate_case(&tokens, &ve_events_tuples[..], &events_tuples[..]); + } +} + +#[test] +fn exact_simulation() { + let case = ( + ["rsgnawburh", "kxhuagnkvo"], + ["FOO", "BAR"], + [ + (4, "rsgnawburh", CreateLock(100.0, 1809600)), + (6, "kxhuagnkvo", CreateLock(100.0, 604800)), + ], + [ + ( + 4, + "rsgnawburh", + Vote(vec![(("BAR".to_string(), "FOO".to_string()), 10000)]), + ), + ( + 6, + "kxhuagnkvo", + Vote(vec![(("BAR".to_string(), "FOO".to_string()), 10000)]), + ), + ( + 6, + "rsgnawburh", + Vote(vec![(("BAR".to_string(), "FOO".to_string()), 10000)]), + ), + ], + ); + + let (users, tokens, ve_events_tuples, events_tuples) = case; + let tokens = tokens.iter().map(|item| item.to_string()).collect_vec(); + let ve_events_tuples = ve_events_tuples + .iter() + .map(|(period, user, event)| (*period, user.to_string(), event.clone())) + .collect_vec(); + let events_tuples = events_tuples + .iter() + .map(|(period, user, event)| (*period, user.to_string(), event.clone())) + .collect_vec(); + + let mut simulator = Simulator::init(&users); + simulator.simulate_case(&tokens, &ve_events_tuples[..], &events_tuples[..]); +} diff --git a/contracts/voting_escrow_lite/.cargo/config b/contracts/voting_escrow_lite/.cargo/config new file mode 100644 index 00000000..8d4bc738 --- /dev/null +++ b/contracts/voting_escrow_lite/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --example schema" diff --git a/contracts/voting_escrow_lite/Cargo.toml b/contracts/voting_escrow_lite/Cargo.toml new file mode 100644 index 00000000..92f94d28 --- /dev/null +++ b/contracts/voting_escrow_lite/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "voting-escrow-lite" +version = "1.0.0" +authors = ["Astroport"] +edition = "2021" +repository = "https://github.com/astroport-fi/astroport-governance" +homepage = "https://astroport.fi" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cw2 = "0.15" +cw20 = "0.15" +cw20-base = { version = "0.15", features = ["library"] } +cosmwasm-std = "1.1" +cw-storage-plus = "0.15" +thiserror = { version = "1.0" } +astroport-governance = { path = "../../packages/astroport-governance" } +cosmwasm-schema = "1.1" + +[dev-dependencies] +cw-multi-test = "0.15" +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-generator-controller = { path = "../../contracts/generator_controller_lite", package = "generator-controller-lite" } +anyhow = "1" +proptest = "1.0" diff --git a/contracts/voting_escrow_lite/README.md b/contracts/voting_escrow_lite/README.md new file mode 100644 index 00000000..51a6ff4c --- /dev/null +++ b/contracts/voting_escrow_lite/README.md @@ -0,0 +1,348 @@ +# Vote Escrowed Staked ASTRO Lite + +The vxASTRO lite contract allows xASTRO token holders to lock their tokens in order to gain emissions voting power that +is used in voting which pools should be receiving ASTRO emissions. + +The xASTRO is lock forever, or until a holder decides to unlock the position. Unlocking is subject to a 2 week (default) +waiting period until withdrawal is allowed. Once an unlocking period starts, the holder will lose the emissions voting power +immediately. + +## InstantiateMsg + +Initialize the contract with the initial owner and the address of the xASTRO token. + +```json +{ + "owner": "terra...", + "deposit_token_addr": "terra..." +} +``` + +## ExecuteMsg + +### `receive` + +Create new lock/vxASTRO position, deposit more xASTRO in the user's vxASTRO position or deposit on behalf of another address. + +```json +{ + "receive": { + "sender": "terra...", + "amount": "123", + "msg": "" + } +} +``` + +### `unlock` + +Unlock the whole position in vxASTRO, subject to a waiting period until `withdraw` is possible + +```json +{ + "unlock": {} +} +``` + +### `withdraw` + +Withdraw the whole amount of xASTRO if the lock for a vxASTRO position has been unlocked and the waiting period has passed. + +```json +{ + "withdraw": {} +} +``` + +### `propose_new_owner` + +Create a request to change contract ownership. The validity period of the offer is set by the `expires_in` variable. +Only the current contract owner can execute this method. + +```json +{ + "propose_new_owner": { + "owner": "terra...", + "expires_in": 1234567 + } +} +``` + +### `drop_ownership_proposal` + +Delete the contract ownership transfer proposal. Only the current contract owner can execute this method. + +```json +{ + "drop_ownership_proposal": {} +} +``` + +### `claim_ownership` + +Used to claim contract ownership. Only the newly proposed contract owner can execute this method. + +```json +{ + "claim_ownership": {} +} +``` + +### `update_blacklist` + +Updates the list of addresses that are prohibited from staking in vxASTRO or if they are already staked, from voting with their vxASTRO in the Astral Assembly. Only the contract owner can execute this method. + +```json +{ + "append_addrs": ["terra...", "terra...", "terra..."], + "remove_addrs": ["terra...", "terra..."] +} +``` + +### `update_config` + +Updates contract parameters. + +```json +{ + "new_guardian": "terra..." +} +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `total_voting_power` + +Returns the total supply of vxASTRO at the current block, for this version, will always return 0. + +```json +{ + "voting_power_response": { + "voting_power": 0 + } +} +``` + +### `user_voting_power` + +Returns a user's vxASTRO balance at the current block, for this version, will always return 0. + +Request: + +```json +{ + "user_voting_power": { + "user": "terra..." + } +} +``` + +Response: + +```json +{ + "voting_power_response": { + "voting_power": 0 + } +} +``` + +### `total_voting_power_at` + +Returns the total vxASTRO supply at a specific timestamp (in seconds), for this version, will always return 0. + +Request: + +```json +{ + "total_voting_power_at": { + "time": 1234567 + } +} +``` + +Response: + +```json +{ + "voting_power_response": { + "voting_power": 0 + } +} +``` + +### `user_voting_power_at` + +Returns the user's vxASTRO balance at a specific timestamp (in seconds), for this version, will always return 0. + +Request: + +```json +{ + "user_voting_power_at": { + "user": "terra...", + "time": 1234567 + } +} +``` + +Response: + +```json +{ + "voting_power_response": { + "voting_power": 0 + } +} +``` + +### `total_emissions_voting_power` + +Returns the total emissions voting power of vxASTRO at the current block. + +```json +{ + "voting_power_response": { + "voting_power": 0 + } +} +``` + +### `user_emissions_voting_power` + +Returns a user's emissions voting power at the current block. + +Request: + +```json +{ + "user_emissions_voting_power": { + "user": "terra..." + } +} +``` + +Response: + +```json +{ + "voting_power_response": { + "voting_power": 0 + } +} +``` + +### `total_emissions_voting_power_at` + +Returns the total emissions voting power at a specific timestamp (in seconds), for this version, will always return 0. + +Request: + +```json +{ + "total_emissions_voting_power_at": { + "time": 1234567 + } +} +``` + +Response: + +```json +{ + "voting_power_response": { + "voting_power": 0 + } +} +``` + +### `user_emissions_voting_power_at` + +Returns a user's emissions voting power at a specific timestamp (in seconds), for this version, will always return 0. + +Request: + +```json +{ + "user_emissions_voting_power_at": { + "user": "terra...", + "time": 1234567 + } +} +``` + +Response: + +```json +{ + "voting_power_response": { + "voting_power": 0 + } +} +``` +### `lock_info` + +Returns the information about a user's vxASTRO position. + +Request: + +```json +{ + "lock_info": { + "user": "terra..." + } +} +``` + +Response: + +```json +{ + "lock_info_response": { + "amount": 10, + "coefficient": 2.5, + "start": 2600, + "end": 2704 + } +} +``` + +### `config` + +Returns the contract's config. + +```json +{ + "config_response": { + "owner": "terra...", + "deposit_token_addr" : "terra..." + } +} +``` + +### `blacklisted_voters` + +Returns blacklisted voters. + +```json +{ + "blacklisted_voters": { + "start_after": "terra...", + "limit": 5 + } +} +``` + +### `check_voters_are_blacklisted` + +Checks if specified addresses are blacklisted + +```json +{ + "check_voters_are_blacklisted": { + "voters": ["terra...", "terra..."] + } +} +``` \ No newline at end of file diff --git a/contracts/voting_escrow_lite/examples/schema.rs b/contracts/voting_escrow_lite/examples/schema.rs new file mode 100644 index 00000000..3d98f61a --- /dev/null +++ b/contracts/voting_escrow_lite/examples/schema.rs @@ -0,0 +1,11 @@ +use astroport_governance::voting_escrow_lite::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/voting_escrow_lite/src/contract.rs b/contracts/voting_escrow_lite/src/contract.rs new file mode 100644 index 00000000..7df5f783 --- /dev/null +++ b/contracts/voting_escrow_lite/src/contract.rs @@ -0,0 +1,113 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, StdError, Uint128}; +use cw2::set_contract_version; +use cw20::{Logo, LogoInfo, MarketingInfoResponse}; +use cw20_base::state::{TokenInfo, LOGO, MARKETING_INFO, TOKEN_INFO}; + +use crate::astroport::asset::addr_opt_validate; +use astroport_governance::utils::DEFAULT_UNLOCK_PERIOD; +use astroport_governance::voting_escrow_lite::{Config, InstantiateMsg, MigrateMsg}; + +use crate::error::ContractError; +use crate::marketing_validation::{validate_marketing_info, validate_whitelist_links}; +use crate::state::{BLACKLIST, CONFIG, VOTING_POWER_HISTORY}; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "astro-voting-escrow-lite"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Creates a new contract with the specified parameters in [`InstantiateMsg`]. +#[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 deposit_token_addr = deps.api.addr_validate(&msg.deposit_token_addr)?; + + validate_whitelist_links(&msg.logo_urls_whitelist)?; + let guardian_addr = addr_opt_validate(deps.api, &msg.guardian_addr)?; + + // We only allow either generator controller *or* the outpost to be set + // If we're on the Hub generator controller should be set + // If we're on an outpost, then outpost should be set + if msg.generator_controller_addr.is_some() && msg.outpost_addr.is_some() { + return Err(StdError::generic_err( + "Only one of Generator Controller or Outpost can be set", + ) + .into()); + } + + let config = Config { + owner: deps.api.addr_validate(&msg.owner)?, + guardian_addr, + deposit_token_addr, + logo_urls_whitelist: msg.logo_urls_whitelist.clone(), + unlock_period: DEFAULT_UNLOCK_PERIOD, + generator_controller_addr: addr_opt_validate(deps.api, &msg.generator_controller_addr)?, + outpost_addr: addr_opt_validate(deps.api, &msg.outpost_addr)?, + }; + CONFIG.save(deps.storage, &config)?; + + VOTING_POWER_HISTORY.save( + deps.storage, + (env.contract.address, env.block.time.seconds()), + &Uint128::zero(), + )?; + BLACKLIST.save(deps.storage, &vec![])?; + + if let Some(marketing) = msg.marketing { + if msg.logo_urls_whitelist.is_empty() { + return Err(StdError::generic_err("Logo URLs whitelist can not be empty").into()); + } + + validate_marketing_info( + marketing.project.as_ref(), + marketing.description.as_ref(), + marketing.logo.as_ref(), + &config.logo_urls_whitelist, + )?; + + let logo = if let Some(logo) = marketing.logo { + LOGO.save(deps.storage, &logo)?; + + match logo { + Logo::Url(url) => Some(LogoInfo::Url(url)), + Logo::Embedded(_) => Some(LogoInfo::Embedded), + } + } else { + None + }; + + let data = MarketingInfoResponse { + project: marketing.project, + description: marketing.description, + marketing: addr_opt_validate(deps.api, &marketing.marketing)?, + logo, + }; + MARKETING_INFO.save(deps.storage, &data)?; + } + + // Store token info + let data = TokenInfo { + name: "Vote Escrowed xASTRO lite".to_string(), + symbol: "vxASTRO".to_string(), + decimals: 6, + total_supply: Uint128::zero(), + mint: None, + }; + + TOKEN_INFO.save(deps.storage, &data)?; + + Ok(Response::default()) +} + +/// Manages contract migration. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Err(ContractError::MigrationError {}) +} diff --git a/contracts/voting_escrow_lite/src/error.rs b/contracts/voting_escrow_lite/src/error.rs new file mode 100644 index 00000000..c00fb504 --- /dev/null +++ b/contracts/voting_escrow_lite/src/error.rs @@ -0,0 +1,52 @@ +use cosmwasm_std::{OverflowError, StdError}; +use cw20_base::ContractError as cw20baseError; +use thiserror::Error; + +/// This enum describes vxASTRO contract errors +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Cw20Base(#[from] cw20baseError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Lock already exists, either unlock and withdraw or extend_lock to add to the lock")] + LockAlreadyExists {}, + + #[error("Lock does not exist")] + LockDoesNotExist {}, + + #[error("Lock time must be within limits (week <= lock time < 2 years)")] + LockTimeLimitsError {}, + + #[error("The lock time has not yet expired")] + LockHasNotExpired {}, + + #[error("The lock expired. Withdraw and create new lock")] + LockExpired {}, + + #[error("The {0} address is blacklisted")] + AddressBlacklisted(String), + + #[error("Marketing info validation error: {0}")] + MarketingInfoValidationError(String), + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Already unlocking")] + Unlocking {}, + + #[error("The lock has not been unlocked, call unlock first")] + NotUnlocked, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} diff --git a/contracts/voting_escrow_lite/src/execute.rs b/contracts/voting_escrow_lite/src/execute.rs new file mode 100644 index 00000000..66487a42 --- /dev/null +++ b/contracts/voting_escrow_lite/src/execute.rs @@ -0,0 +1,596 @@ +use crate::astroport; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; + +use astroport_governance::{generator_controller_lite, outpost}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, from_binary, to_binary, Addr, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, + StdResult, Storage, Uint128, WasmMsg, +}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; +use cw20_base::contract::{execute_update_marketing, execute_upload_logo}; +use cw20_base::state::MARKETING_INFO; + +use crate::astroport::common::validate_addresses; +use astroport_governance::voting_escrow_lite::{Config, Cw20HookMsg, ExecuteMsg}; + +use crate::error::ContractError; +use crate::marketing_validation::{validate_marketing_info, validate_whitelist_links}; +use crate::state::{Lock, BLACKLIST, CONFIG, LOCKED, OWNERSHIP_PROPOSAL, VOTING_POWER_HISTORY}; +use crate::utils::{blacklist_check, fetch_last_checkpoint, xastro_token_check}; + +/// Exposes all the execute functions available in the contract. +/// +/// ## Execute messages +/// * **ExecuteMsg::Receive(msg)** Parse incoming messages coming from the xASTRO token contract. +/// +/// * **ExecuteMsg::Unlock {}** Unlock all xASTRO from a lock position, subject to a waiting period until withdrawal is possible. +/// +/// * **ExecuteMsg::Relock {}** Relock all xASTRO from an unlocking position if the Hub could not be notified +/// +/// * **ExecuteMsg::Withdraw {}** Withdraw all xASTRO from an lock position if the unlock time has expired. +/// +/// * **ExecuteMsg::ProposeNewOwner { owner, expires_in }** Creates a new request to change contract ownership. +/// +/// * **ExecuteMsg::DropOwnershipProposal {}** Removes a request to change contract ownership. +/// +/// * **ExecuteMsg::ClaimOwnership {}** Claims contract ownership. +/// +/// * **ExecuteMsg::UpdateBlacklist { append_addrs, remove_addrs }** Updates the contract's blacklist. +/// +/// * **ExecuteMsg::UpdateMarketing { project, description, marketing }** Updates the contract's marketing information. +/// +/// * **ExecuteMsg::UploadLogo { logo }** Uploads a new logo to the contract. +/// +/// * **ExecuteMsg::SetLogoUrlsWhitelist { whitelist }** Sets the contract's logo whitelist. +/// +/// * **ExecuteMsg::UpdateConfig { new_guardian }** Updates the contract's guardian. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::Unlock {} => unlock(deps, env, info), + ExecuteMsg::Relock { user } => relock(deps, env, info, user), + ExecuteMsg::Withdraw {} => withdraw(deps, env, info), + ExecuteMsg::ProposeNewOwner { + new_owner, + expires_in, + } => { + let config = CONFIG.load(deps.storage)?; + propose_new_owner( + deps, + info, + env, + new_owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config: Config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + ExecuteMsg::UpdateBlacklist { + append_addrs, + remove_addrs, + } => update_blacklist(deps, env, info, append_addrs, remove_addrs), + ExecuteMsg::UpdateMarketing { + project, + description, + marketing, + } => { + validate_marketing_info(project.as_ref(), description.as_ref(), None, &[])?; + execute_update_marketing(deps, env, info, project, description, marketing) + .map_err(Into::into) + } + ExecuteMsg::UploadLogo(logo) => { + let config = CONFIG.load(deps.storage)?; + validate_marketing_info(None, None, Some(&logo), &config.logo_urls_whitelist)?; + execute_upload_logo(deps, env, info, logo).map_err(Into::into) + } + ExecuteMsg::SetLogoUrlsWhitelist { whitelist } => { + let mut config = CONFIG.load(deps.storage)?; + let marketing_info = MARKETING_INFO.load(deps.storage)?; + if info.sender != config.owner && Some(info.sender) != marketing_info.marketing { + Err(ContractError::Unauthorized {}) + } else { + validate_whitelist_links(&whitelist)?; + config.logo_urls_whitelist = whitelist; + CONFIG.save(deps.storage, &config)?; + Ok(Response::default().add_attribute("action", "set_logo_urls_whitelist")) + } + } + ExecuteMsg::UpdateConfig { + new_guardian, + generator_controller, + outpost, + } => execute_update_config(deps, info, new_guardian, generator_controller, outpost), + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. +/// Only allows messages coming from the xASTRO token contract. +/// +/// * **cw20_msg** CW20 message to process. +fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + xastro_token_check(deps.storage, info.sender)?; + let sender = Addr::unchecked(cw20_msg.sender); + blacklist_check(deps.storage, &sender)?; + + match from_binary(&cw20_msg.msg)? { + Cw20HookMsg::CreateLock { .. } => create_lock(deps, env, sender, cw20_msg.amount), + Cw20HookMsg::ExtendLockAmount {} => deposit_for(deps, env, cw20_msg.amount, sender), + Cw20HookMsg::DepositFor { user } => { + let addr = deps.api.addr_validate(&user)?; + blacklist_check(deps.storage, &addr)?; + deposit_for(deps, env, cw20_msg.amount, addr) + } + } +} + +/// Creates a lock for the user that lasts until Unlock is called +/// Creates a lock if it doesn't exist and triggers a [`checkpoint`] for the staker. +/// If a lock already exists, then a [`ContractError`] is returned. +/// +/// * **user** staker for which we create a lock position. +/// +/// * **amount** amount of xASTRO deposited in the lock position. +fn create_lock( + deps: DepsMut, + env: Env, + user: Addr, + amount: Uint128, +) -> Result { + LOCKED.update( + deps.storage, + user.clone(), + env.block.time.seconds(), + |lock_opt| { + if lock_opt.is_some() && !lock_opt.unwrap().amount.is_zero() { + return Err(ContractError::LockAlreadyExists {}); + } + Ok(Lock { amount, end: None }) + }, + )?; + checkpoint(deps, env, user, Some(amount))?; + + Ok(Response::default().add_attribute("action", "create_lock")) +} + +/// Deposits an 'amount' of xASTRO tokens into 'user''s lock. +/// Triggers a [`checkpoint`] for the user. +/// If the user does not have a lock, then a lock is created. +/// +/// * **amount** amount of xASTRO to deposit. +/// +/// * **user** user who's lock amount will increase. +fn deposit_for( + deps: DepsMut, + env: Env, + amount: Uint128, + user: Addr, +) -> Result { + LOCKED.update( + deps.storage, + user.clone(), + env.block.time.seconds(), + |lock_opt| { + match lock_opt { + Some(mut lock) if !lock.amount.is_zero() => match lock.end { + // This lock is still locked + None => { + lock.amount += amount; + Ok(lock) + } + // This lock is expired or being unlocked, thus reject the deposit + Some(end) => { + if end <= env.block.time.seconds() { + return Err(ContractError::LockExpired {}); + } + Err(ContractError::Unlocking {}) + } + }, + // If no lock exists, create a new one + _ => Ok(Lock { amount, end: None }), + } + }, + )?; + checkpoint(deps, env, user, Some(amount))?; + + Ok(Response::default().add_attribute("action", "deposit_for")) +} + +/// Starts the unlock of the whole amount of locked xASTRO from a specific user lock. +/// If the user lock doesn't exist or if it has been unlocked, then a [`ContractError`] is returned. +/// +/// Note: When a user unlocks, they lose their emission voting power immediately +fn unlock(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + let sender = info.sender; + + // 'LockDoesNotExist' is thrown either when a lock does not exist in LOCKED or when a lock exists but lock.amount == 0 + let lock = LOCKED + .may_load(deps.storage, sender.clone())? + .filter(|lock| !lock.amount.is_zero()) + .ok_or(ContractError::LockDoesNotExist {})?; + + match lock.end { + // This lock is still locked, we can unlock + None => { + let config = CONFIG.load(deps.storage)?; + let response = Response::default().add_attribute("action", "unlock_initiated"); + + // Start the unlock for this address + start_unlock(lock, deps, env, sender.clone())?; + + // We only allow either the generator controller _or_ the Outpost to be set at any time + let kick_msg = match (&config.generator_controller_addr, &config.outpost_addr) { + (Some(generator_controller), None) => { + // On the Hub we kick the user from the Generator Controller directly + // Voting power is removed immediately after a user unlocks + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: generator_controller.to_string(), + msg: to_binary( + &generator_controller_lite::ExecuteMsg::KickUnlockedVoters { + unlocked_voters: vec![sender.to_string()], + }, + )?, + funds: vec![], + }) + } + (None, Some(outpost)) => { + // If this vxASTRO contract is deployed on an Outpost we need to + // forward the unlock to the Hub, if the notification fails + // the funds will be locked again + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: outpost.to_string(), + msg: to_binary(&outpost::ExecuteMsg::KickUnlocked { user: sender })?, + funds: vec![], + }) + } + _ => { + return Err(ContractError::Std(StdError::GenericErr { + msg: "Either Generator Controller or Outpost must be set".to_string(), + })); + } + }; + + Ok(response.add_message(kick_msg)) + } + // This lock is expired or being unlocked, can't unlock again + Some(end) => { + if end <= env.block.time.seconds() { + return Err(ContractError::LockExpired {}); + } + Err(ContractError::Unlocking {}) + } + } +} + +/// Locks the given user's xASTRO lock again if the Hub could not be notified +/// +/// When a user unlocks, the Hub needs to be notified so that the user's votes +/// can be kicked from the Generator Controller. If the notification to the Hub +/// fails, then the position must be locked again +/// If the user lock doesn't exist or if it has been completely unlocked, +/// then a [`ContractError`] is returned. +fn relock( + deps: DepsMut, + env: Env, + info: MessageInfo, + user: String, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Check that the caller is the Outpost contract + if Some(info.sender) != config.outpost_addr { + return Err(ContractError::Unauthorized {}); + } + + let sender = Addr::unchecked(user); + // 'LockDoesNotExist' is thrown either when a lock does not exist in LOCKED or when a lock exists but lock.amount == 0 + let mut lock = LOCKED + .may_load(deps.storage, sender.clone())? + .filter(|lock| !lock.amount.is_zero()) + .ok_or(ContractError::LockDoesNotExist {})?; + + // If the lock has been unlocked + if lock.end.is_some() { + lock.end = None; + LOCKED.save( + deps.storage, + sender.clone(), + &lock, + env.block.time.seconds(), + )?; + // Relock needs to add back the user's voting power + VOTING_POWER_HISTORY.save( + deps.storage, + (sender.clone(), env.block.time.seconds()), + &lock.amount, + )?; + checkpoint_total(deps.storage, env, Some(lock.amount), None)?; + } + + Ok(Response::new() + .add_attribute("action", "relock") + .add_attribute("user", sender)) +} + +/// Withdraws the whole amount of locked xASTRO from a specific user lock. +/// If the user lock doesn't exist or if it has not yet expired, then a [`ContractError`] is returned. +fn withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + let sender = info.sender; + // 'LockDoesNotExist' is thrown either when a lock does not exist in LOCKED or when a lock exists but lock.amount == 0 + let mut lock = LOCKED + .may_load(deps.storage, sender.clone())? + .filter(|lock| !lock.amount.is_zero()) + .ok_or(ContractError::LockDoesNotExist {})?; + + match lock.end { + // This lock is still locked, withdrawal not possible + None => Err(ContractError::NotUnlocked {}), + // This lock is expired or being unlocked + Some(end) => { + // Still unlocking, can't withdraw + if end > env.block.time.seconds() { + return Err(ContractError::LockHasNotExpired {}); + } + // Unlocked, withdrawal is now allowed + let config = CONFIG.load(deps.storage)?; + let transfer_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: config.deposit_token_addr.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: sender.to_string(), + amount: lock.amount, + })?, + funds: vec![], + }); + lock.amount = Uint128::zero(); + LOCKED.save(deps.storage, sender, &lock, env.block.time.seconds())?; + + Ok(Response::default() + .add_message(transfer_msg) + .add_attribute("action", "withdraw")) + } + } +} + +/// Update the staker blacklist. Whitelists addresses specified in 'remove_addrs' +/// and blacklists new addresses specified in 'append_addrs'. Nullifies staker voting power and +/// cancels their contribution in the total voting power (total vxASTRO supply). +/// +/// * **append_addrs** array of addresses to blacklist. +/// +/// * **remove_addrs** array of addresses to whitelist. +fn update_blacklist( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + append_addrs: Option>, + remove_addrs: Option>, +) -> Result { + let config = CONFIG.load(deps.storage)?; + // Permission check + if info.sender != config.owner && Some(info.sender) != config.guardian_addr { + return Err(ContractError::Unauthorized {}); + } + let append_addrs = append_addrs.unwrap_or_default(); + let remove_addrs = remove_addrs.unwrap_or_default(); + let blacklist = BLACKLIST.load(deps.storage)?; + let append: Vec<_> = validate_addresses(deps.api, &append_addrs)? + .into_iter() + .filter(|addr| !blacklist.contains(addr)) + .collect(); + let remove: Vec<_> = validate_addresses(deps.api, &remove_addrs)? + .into_iter() + .filter(|addr| blacklist.contains(addr)) + .collect(); + + if append.is_empty() && remove.is_empty() { + return Err(StdError::generic_err("Append and remove arrays are empty").into()); + } + + let timestamp = env.block.time.seconds(); + let mut reduce_total_vp = Uint128::zero(); // accumulator for decreasing total voting power + + for addr in append.iter() { + let last_checkpoint = fetch_last_checkpoint(deps.storage, addr, timestamp)?; + if let Some((_, emissions_power)) = last_checkpoint { + // We need to checkpoint with zero power and zero slope + VOTING_POWER_HISTORY.save(deps.storage, (addr.clone(), timestamp), &Uint128::zero())?; + + let cur_power = emissions_power; + // User's contribution is already zero. Skipping them + if cur_power.is_zero() { + continue; + } + + // User's contribution in the total voting power calculation + reduce_total_vp += cur_power; + } + } + + if !reduce_total_vp.is_zero() { + // Trigger a total voting power recalculation + checkpoint_total(deps.storage, env.clone(), None, Some(reduce_total_vp))?; + } + + for addr in remove.iter() { + let lock_opt = LOCKED.may_load(deps.storage, addr.clone())?; + if let Some(Lock { amount, end, .. }) = lock_opt { + match end { + // Only checkpoint the amount if the lock if still active + None => checkpoint(deps.branch(), env.clone(), addr.clone(), Some(amount))?, + // This lock is expired or being unlocked and has already been set to zero + Some(_) => checkpoint(deps.branch(), env.clone(), addr.clone(), None)?, + } + } + } + + BLACKLIST.update(deps.storage, |blacklist| -> StdResult> { + let mut updated_blacklist: Vec<_> = blacklist + .into_iter() + .filter(|addr| !remove.contains(addr)) + .collect(); + updated_blacklist.extend(append); + Ok(updated_blacklist) + })?; + + let mut attrs = vec![attr("action", "update_blacklist")]; + if !append_addrs.is_empty() { + attrs.push(attr("added_addresses", append_addrs.join(","))) + } + if !remove_addrs.is_empty() { + attrs.push(attr("removed_addresses", remove_addrs.join(","))) + } + + // TODO: Submit update blacklist immediately + + Ok(Response::default().add_attributes(attrs)) +} + +/// Updates contracts' guardian address. +fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + new_guardian: Option, + generator_controller: Option, + outpost: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + if config.owner != info.sender { + return Err(ContractError::Unauthorized {}); + } + + if let Some(new_guardian) = new_guardian { + config.guardian_addr = Some(deps.api.addr_validate(&new_guardian)?); + } + + if let Some(generator_controller) = generator_controller { + if config.outpost_addr.is_some() { + return Err(StdError::generic_err( + "Only one of Generator Controller or Outpost can be set", + ) + .into()); + } + config.generator_controller_addr = Some(deps.api.addr_validate(&generator_controller)?); + } + + if let Some(outpost) = outpost { + if config.generator_controller_addr.is_some() { + return Err(StdError::generic_err( + "Only one of Generator Controller or Outpost can be set", + ) + .into()); + } + config.outpost_addr = Some(deps.api.addr_validate(&outpost)?); + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default().add_attribute("action", "execute_update_config")) +} + +/// Start the unlock of a user's Lock +/// +/// The unlocking time is based on the current block time + configured unlock period +fn start_unlock(mut lock: Lock, deps: DepsMut, env: Env, sender: Addr) -> StdResult<()> { + let config = CONFIG.load(deps.storage)?; + let unlock_time = env.block.time.seconds() + config.unlock_period; + lock.end = Some(unlock_time); + LOCKED.save( + deps.storage, + sender.clone(), + &lock, + env.block.time.seconds(), + )?; + // Update user's voting power + VOTING_POWER_HISTORY.save( + deps.storage, + (sender, env.block.time.seconds()), + &Uint128::zero(), + )?; + // Update total voting power + checkpoint_total(deps.storage, env, None, Some(lock.amount)) +} + +/// Checkpoint a user's voting power (vxASTRO balance). +/// This function fetches the user's last available checkpoint, calculates the user's current voting power +/// and saves the new checkpoint for the current period in [`HISTORY`] (using the user's address). +/// If a user already checkpointed themselves for the current period, then this function uses the current checkpoint as the latest +/// available one. +/// +/// * **addr** staker for which we checkpoint the voting power. +/// +/// * **add_amount** amount of vxASTRO to add to the staker's balance. +fn checkpoint(deps: DepsMut, env: Env, addr: Addr, add_amount: Option) -> StdResult<()> { + let timestamp = env.block.time.seconds(); + let add_amount = add_amount.unwrap_or_default(); + + // Get the last user checkpoint + let last_checkpoint = fetch_last_checkpoint(deps.storage, &addr, timestamp)?; + let new_power = if let Some((_, emissions_power)) = last_checkpoint { + emissions_power.checked_add(add_amount)? + } else { + add_amount + }; + + VOTING_POWER_HISTORY.save(deps.storage, (addr, timestamp), &new_power)?; + checkpoint_total(deps.storage, env, Some(add_amount), None) +} + +/// Checkpoint the total voting power (total supply of vxASTRO). +/// This function fetches the last available vxASTRO checkpoint +/// saves all recalculated periods in [`HISTORY`]. +/// +/// * **add_voting_power** amount of vxASTRO to add to the total. +/// +/// * **reduce_power** amount of vxASTRO to subtract from the total. +fn checkpoint_total( + storage: &mut dyn Storage, + env: Env, + add_voting_power: Option, + reduce_power: Option, +) -> StdResult<()> { + let timestamp = env.block.time.seconds(); + let contract_addr = env.contract.address; + let add_voting_power = add_voting_power.unwrap_or_default(); + + // Get last checkpoint + let last_checkpoint = fetch_last_checkpoint(storage, &contract_addr, timestamp)?; + let new_point = if let Some((_, emissions_power)) = last_checkpoint { + let mut new_power = emissions_power.saturating_add(add_voting_power); + new_power = new_power.saturating_sub(reduce_power.unwrap_or_default()); + new_power + } else { + add_voting_power + }; + VOTING_POWER_HISTORY.save(storage, (contract_addr, timestamp), &new_point) +} diff --git a/contracts/voting_escrow_lite/src/lib.rs b/contracts/voting_escrow_lite/src/lib.rs new file mode 100644 index 00000000..183e6145 --- /dev/null +++ b/contracts/voting_escrow_lite/src/lib.rs @@ -0,0 +1,12 @@ +pub mod contract; +pub mod state; + +// During development this import could be replaced with another astroport version. +// However, in production, the astroport version should be the same for all contracts. +pub use astroport_governance::astroport; + +pub mod error; +pub mod execute; +mod marketing_validation; +pub mod query; +mod utils; diff --git a/contracts/voting_escrow_lite/src/marketing_validation.rs b/contracts/voting_escrow_lite/src/marketing_validation.rs new file mode 100644 index 00000000..aea35004 --- /dev/null +++ b/contracts/voting_escrow_lite/src/marketing_validation.rs @@ -0,0 +1,75 @@ +use crate::error::ContractError; +use crate::error::ContractError::MarketingInfoValidationError; + +use cosmwasm_std::StdError; +use cw20::Logo; + +const SAFE_TEXT_CHARS: &str = "!&?#()*+'-.,/\""; +const SAFE_LINK_CHARS: &str = "-_:/?#@!$&()*+,;=.~[]'%"; + +fn validate_text(text: &str, name: &str) -> Result<(), ContractError> { + if text.chars().any(|c| { + !c.is_ascii_alphanumeric() && !c.is_ascii_whitespace() && !SAFE_TEXT_CHARS.contains(c) + }) { + Err(MarketingInfoValidationError(format!( + "{name} contains invalid characters: {text}" + ))) + } else { + Ok(()) + } +} + +pub fn validate_whitelist_links(links: &[String]) -> Result<(), ContractError> { + links.iter().try_for_each(|link| { + if !link.ends_with('/') { + return Err(MarketingInfoValidationError(format!( + "Whitelist link should end with '/': {link}" + ))); + } + validate_link(link) + }) +} + +pub fn validate_link(link: &String) -> Result<(), ContractError> { + if link + .chars() + .any(|c| !c.is_ascii_alphanumeric() && !SAFE_LINK_CHARS.contains(c)) + { + Err(StdError::generic_err(format!("Link contains invalid characters: {link}")).into()) + } else { + Ok(()) + } +} + +fn check_link(link: &String, whitelisted_links: &[String]) -> Result<(), ContractError> { + if validate_link(link).is_err() { + Err(MarketingInfoValidationError(format!( + "Logo link is invalid: {link}" + ))) + } else if !whitelisted_links.iter().any(|wl| link.starts_with(wl)) { + Err(MarketingInfoValidationError(format!( + "Logo link is not whitelisted: {link}" + ))) + } else { + Ok(()) + } +} + +pub(crate) fn validate_marketing_info( + project: Option<&String>, + description: Option<&String>, + logo: Option<&Logo>, + whitelisted_links: &[String], +) -> Result<(), ContractError> { + if let Some(description) = description { + validate_text(description, "description")?; + } + if let Some(project) = project { + validate_text(project, "project")?; + } + if let Some(Logo::Url(url)) = logo { + check_link(url, whitelisted_links)?; + } + + Ok(()) +} diff --git a/contracts/voting_escrow_lite/src/query.rs b/contracts/voting_escrow_lite/src/query.rs new file mode 100644 index 00000000..45690ae3 --- /dev/null +++ b/contracts/voting_escrow_lite/src/query.rs @@ -0,0 +1,273 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_binary, Addr, Binary, Deps, Env, StdError, StdResult, Uint128, Uint64}; + +use cw20::{BalanceResponse, TokenInfoResponse}; +use cw20_base::contract::{query_download_logo, query_marketing_info}; +use cw20_base::state::TOKEN_INFO; + +use astroport_governance::voting_escrow_lite::{ + BlacklistedVotersResponse, LockInfoResponse, QueryMsg, VotingPowerResponse, DEFAULT_LIMIT, + MAX_LIMIT, +}; + +use crate::state::{BLACKLIST, CONFIG, LOCKED}; +use crate::utils::fetch_last_checkpoint; + +/// Expose available contract queries. +/// +/// ## Queries +/// * **QueryMsg::CheckVotersAreBlacklisted { voters }** Check if the provided voters are blacklisted. +/// +/// * **QueryMsg::BlacklistedVoters { start_after, limit }** Fetch all blacklisted voters. +/// +/// * **QueryMsg::TotalVotingPower {}** Fetch the total voting power (vxASTRO supply) at the current block. Always returns 0 in this version. +/// +/// * **QueryMsg::TotalVotingPowerAt { .. }** Fetch the total voting power (vxASTRO supply) at a specified timestamp. Always returns 0 in this version. +/// +/// * **QueryMsg::TotalVotingPowerAtPeriod { .. }** Fetch the total voting power (vxASTRO supply) at a specified period. Always returns 0 in this version. +/// +/// * **QueryMsg::UserVotingPower{ .. }** Fetch the user's voting power (vxASTRO balance) at the current block. Always returns 0 in this version. +/// +/// * **QueryMsg::UserVotingPowerAt { .. }** Fetch the user's voting power (vxASTRO balance) at a specified timestamp. Always returns 0 in this version. +/// +/// * **QueryMsg::UserVotingPowerAtPeriod { .. }** Fetch the user's voting power (vxASTRO balance) at a specified period. Always returns 0 in this version. +/// +/// * **QueryMsg::TotalEmissionsVotingPower {}** Fetch the total emissions voting power at the current block. +/// +/// * **QueryMsg::TotalEmissionsVotingPowerAt { time }** Fetch the total emissions voting power at a specified timestamp. +/// +/// * **QueryMsg::UserEmissionsVotingPower { user }** Fetch a user's emissions voting power at the current block. +/// +/// * **QueryMsg::UserEmissionsVotingPowerAt { user, time }** Fetch a user's emissions voting power at a specified timestamp. +/// +/// * **QueryMsg::LockInfo { user }** Fetch a user's lock information. +/// +/// * **QueryMsg::UserDepositAt { user, timestamp }** Fetch a user's deposit at a specified timestamp. +/// +/// * **QueryMsg::Config {}** Fetch the contract's config. +/// +/// * **QueryMsg::Balance { address: _ }** Fetch the user's balance. Always returns 0 in this version. +/// +/// * **QueryMsg::TokenInfo {}** Fetch the token's information. +/// +/// * **QueryMsg::MarketingInfo {}** Fetch the token's marketing information. +/// +/// * **QueryMsg::DownloadLogo {}** Fetch the token's logo. +/// +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::CheckVotersAreBlacklisted { voters } => { + to_binary(&check_voters_are_blacklisted(deps, voters)?) + } + QueryMsg::BlacklistedVoters { start_after, limit } => { + to_binary(&get_blacklisted_voters(deps, start_after, limit)?) + } + QueryMsg::TotalVotingPower {} => to_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::TotalVotingPowerAt { .. } => to_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::TotalVotingPowerAtPeriod { .. } => to_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::UserVotingPower { .. } => to_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::UserVotingPowerAt { .. } => to_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::UserVotingPowerAtPeriod { .. } => to_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::TotalEmissionsVotingPower {} => { + to_binary(&get_total_emissions_voting_power(deps, env, None)?) + } + QueryMsg::TotalEmissionsVotingPowerAt { time } => { + to_binary(&get_total_emissions_voting_power(deps, env, Some(time))?) + } + QueryMsg::UserEmissionsVotingPower { user } => { + to_binary(&get_user_emissions_voting_power(deps, env, user, None)?) + } + QueryMsg::UserEmissionsVotingPowerAt { user, time } => to_binary( + &get_user_emissions_voting_power(deps, env, user, Some(time))?, + ), + QueryMsg::LockInfo { user } => to_binary(&get_user_lock_info(deps, env, user)?), + QueryMsg::UserDepositAt { user, timestamp } => { + to_binary(&get_user_deposit_at_time(deps, user, timestamp)?) + } + QueryMsg::Config {} => { + let config = CONFIG.load(deps.storage)?; + to_binary(&config) + } + QueryMsg::Balance { address } => to_binary(&get_user_balance(deps, env, address)?), + QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps, env)?), + QueryMsg::MarketingInfo {} => to_binary(&query_marketing_info(deps)?), + QueryMsg::DownloadLogo {} => to_binary(&query_download_logo(deps)?), + } +} + +/// Checks if specified addresses are blacklisted. +/// +/// * **voters** addresses to check if they are blacklisted. +pub fn check_voters_are_blacklisted( + deps: Deps, + voters: Vec, +) -> StdResult { + let black_list = BLACKLIST.load(deps.storage)?; + + for voter in voters { + let voter_addr = deps.api.addr_validate(voter.as_str())?; + if !black_list.contains(&voter_addr) { + return Ok(BlacklistedVotersResponse::VotersNotBlacklisted { voter }); + } + } + + Ok(BlacklistedVotersResponse::VotersBlacklisted {}) +} + +/// Returns a list of blacklisted voters. +/// +/// * **start_after** is an optional field that specifies whether the function should return +/// a list of voters starting from a specific address onward. +/// +/// * **limit** max amount of voters addresses to return. +pub fn get_blacklisted_voters( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let mut black_list = BLACKLIST.load(deps.storage)?; + + if black_list.is_empty() { + return Ok(vec![]); + } + + black_list.sort(); + + let mut start_index = Default::default(); + if let Some(start_after) = start_after { + let start_addr = deps.api.addr_validate(start_after.as_str())?; + start_index = black_list + .iter() + .position(|addr| *addr == start_addr) + .ok_or_else(|| { + StdError::generic_err(format!( + "The {} address is not blacklisted", + start_addr.as_str() + )) + })? + + 1; // start from the next element of the slice + } + + // validate end index of the slice + let end_index = (start_index + limit).min(black_list.len()); + + Ok(black_list[start_index..end_index].to_vec()) +} + +/// Return a user's lock information. +/// +/// * **user** user for which we return lock information. +fn get_user_lock_info(deps: Deps, _env: Env, user: String) -> StdResult { + let addr = deps.api.addr_validate(&user)?; + if let Some(lock) = LOCKED.may_load(deps.storage, addr)? { + let resp = LockInfoResponse { + amount: lock.amount, + end: lock.end, + }; + Ok(resp) + } else { + Err(StdError::generic_err("User is not found")) + } +} + +/// Fetches a user's emissions voting power at the current block and uses that +/// as the balance +/// +/// * **user** user/staker for which we fetch the current voting power (vxASTRO balance). +fn get_user_balance(deps: Deps, env: Env, user: String) -> StdResult { + let vp_response = get_user_emissions_voting_power(deps, env, user, None)?; + Ok(BalanceResponse { + balance: vp_response.voting_power, + }) +} + +/// Return a user's staked xASTRO amount at a given timestamp. +/// +/// * **user** user for which we return lock information. +/// +/// * **timestamp** timestamp at which we return the staked xASTRO amount. +fn get_user_deposit_at_time(deps: Deps, user: String, timestamp: Uint64) -> StdResult { + let addr = deps.api.addr_validate(&user)?; + let locked_opt = LOCKED.may_load_at_height(deps.storage, addr, timestamp.u64())?; + if let Some(lock) = locked_opt { + Ok(lock.amount) + } else { + Ok(Uint128::zero()) + } +} + +/// Fetch a user's emissions voting power at the current block if no time +/// is specified, else uses the given time. If a user is blacklisted, this will +/// return 0 +/// +/// * **user** user/staker for which we fetch the current emissions voting power. +/// +/// * **time** optional time at which to fetch the user's emissions voting power. +fn get_user_emissions_voting_power( + deps: Deps, + env: Env, + user: String, + time: Option, +) -> StdResult { + let user = deps.api.addr_validate(&user)?; + let timestamp = time.unwrap_or_else(|| env.block.time.seconds()); + let last_checkpoint = fetch_last_checkpoint(deps.storage, &user, timestamp)?; + + if let Some(emissions_power) = last_checkpoint.map(|(_, emissions_power)| emissions_power) { + // The voting power point at the specified `time` was found + Ok(VotingPowerResponse { + voting_power: emissions_power, + }) + } else { + // User not found + Ok(VotingPowerResponse { + voting_power: Uint128::zero(), + }) + } +} + +/// Fetch the total emissions voting power at the current block if no time +/// is specified, else uses the given time. +/// +/// * **time** optional time at which to fetch the user's emissions voting power. +fn get_total_emissions_voting_power( + deps: Deps, + env: Env, + time: Option, +) -> StdResult { + let timestamp = time.unwrap_or_else(|| env.block.time.seconds()); + let last_checkpoint = fetch_last_checkpoint(deps.storage, &env.contract.address, timestamp)?; + + let emissions_power = + last_checkpoint.map_or(Uint128::zero(), |(_, emissions_power)| emissions_power); + Ok(VotingPowerResponse { + voting_power: emissions_power, + }) +} + +/// Fetch the vxASTRO token information, such as the token name, symbol, decimals and total supply (total voting power). +fn query_token_info(deps: Deps, _env: Env) -> StdResult { + let info = TOKEN_INFO.load(deps.storage)?; + let res = TokenInfoResponse { + name: info.name, + symbol: info.symbol, + decimals: info.decimals, + total_supply: Uint128::zero(), + }; + Ok(res) +} diff --git a/contracts/voting_escrow_lite/src/state.rs b/contracts/voting_escrow_lite/src/state.rs new file mode 100644 index 00000000..87dc4aad --- /dev/null +++ b/contracts/voting_escrow_lite/src/state.rs @@ -0,0 +1,35 @@ +use crate::astroport::common::OwnershipProposal; +use astroport_governance::voting_escrow_lite::Config; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map, SnapshotMap, Strategy}; + +/// This structure stores data about the lockup position for a specific vxASTRO staker. +#[cw_serde] +pub struct Lock { + /// The total amount of xASTRO tokens that were deposited in the vxASTRO position + pub amount: Uint128, + /// The timestamp when a lock will be unlocked. None for positions in Locked state + pub end: Option, +} + +/// Stores the contract config at the given key +pub const CONFIG: Item = Item::new("config"); + +/// Stores all user lock history by timestamp +pub const LOCKED: SnapshotMap = SnapshotMap::new( + "locked_timestamp", + "locked_timestamp__checkpoints", + "locked_timestamp__changelog", + Strategy::EveryBlock, +); + +/// Stores the voting power history for every staker (addr => timestamp) +/// Total voting power checkpoints are stored using a (contract_addr => timestamp) key +pub const VOTING_POWER_HISTORY: Map<(Addr, u64), Uint128> = Map::new("voting_power_history"); + +/// Contains a proposal to change contract ownership +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); + +/// Contains blacklisted staker addresses +pub const BLACKLIST: Item> = Item::new("blacklist"); diff --git a/contracts/voting_escrow_lite/src/utils.rs b/contracts/voting_escrow_lite/src/utils.rs new file mode 100644 index 00000000..31c0827a --- /dev/null +++ b/contracts/voting_escrow_lite/src/utils.rs @@ -0,0 +1,44 @@ +use crate::{error::ContractError, state::VOTING_POWER_HISTORY}; + +use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; +use cw_storage_plus::Bound; + +use crate::state::{BLACKLIST, CONFIG}; + +/// Checks that the sender is the xASTRO token. +pub(crate) fn xastro_token_check(storage: &dyn Storage, sender: Addr) -> Result<(), ContractError> { + let config = CONFIG.load(storage)?; + if sender != config.deposit_token_addr { + Err(ContractError::Unauthorized {}) + } else { + Ok(()) + } +} + +/// Checks if the blacklist contains a specific address. +pub(crate) fn blacklist_check(storage: &dyn Storage, addr: &Addr) -> Result<(), ContractError> { + let blacklist = BLACKLIST.load(storage)?; + if blacklist.contains(addr) { + Err(ContractError::AddressBlacklisted(addr.to_string())) + } else { + Ok(()) + } +} + +/// Fetches the last known voting power in [`VOTING_POWER_HISTORY`] for the given address. +pub(crate) fn fetch_last_checkpoint( + storage: &dyn Storage, + addr: &Addr, + timestamp: u64, +) -> StdResult> { + VOTING_POWER_HISTORY + .prefix(addr.clone()) + .range( + storage, + None, + Some(Bound::inclusive(timestamp)), + Order::Descending, + ) + .next() + .transpose() +} diff --git a/contracts/voting_escrow_lite/tests/integration.rs b/contracts/voting_escrow_lite/tests/integration.rs new file mode 100644 index 00000000..1f31431d --- /dev/null +++ b/contracts/voting_escrow_lite/tests/integration.rs @@ -0,0 +1,1164 @@ +use astroport::token as astro; +use cosmwasm_std::{attr, to_binary, Addr, StdError, Uint128, Uint64}; +use cw20::{Cw20ExecuteMsg, Logo, LogoInfo, MarketingInfoResponse, MinterResponse}; +use cw_multi_test::{next_block, ContractWrapper, Executor}; +use voting_escrow_lite::astroport; + +use astroport_governance::utils::{get_lite_period, WEEK}; +use astroport_governance::voting_escrow_lite::{ + Config, Cw20HookMsg, ExecuteMsg, LockInfoResponse, QueryMsg, +}; + +use crate::test_utils::{mock_app, Helper, MULTIPLIER}; + +mod test_utils; + +#[test] +fn lock_unlock_logic() { + let mut router = mock_app(); + let router_ref = &mut router; + let owner = Addr::unchecked("owner"); + let helper = Helper::init(router_ref, owner); + + helper.mint_xastro(router_ref, "owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro(router_ref, "user", 100); + helper.check_xastro_balance(router_ref, "user", 100); + + // Try to withdraw from a non-existent lock + let err = helper.withdraw(router_ref, "user").unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Lock does not exist"); + + // Try to deposit more xASTRO in a position that does not already exist + // This should create a new lock + helper.extend_lock_amount(router_ref, "user", 1f32).unwrap(); + helper.check_xastro_balance(router_ref, "user", 99); + helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 1); + + // Current total voting power is 0 + let vp = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(vp, 1.0); + + // Try to create another voting escrow lock + let err = helper + .create_lock(router_ref, "user", WEEK * 2, 90f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Lock already exists, either unlock and withdraw or extend_lock to add to the lock" + ); + + // Check that 90 xASTRO were not debited + helper.check_xastro_balance(router_ref, "user", 99); + helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 1); + + // Add more xASTRO to the existing position + helper.extend_lock_amount(router_ref, "user", 9f32).unwrap(); + helper.check_xastro_balance(router_ref, "user", 90); + helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 10); + + // Try to withdraw from a non-unlocked lock + let err = helper.withdraw(router_ref, "user").unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The lock has not been unlocked, call unlock first" + ); + + helper.unlock(router_ref, "user").unwrap(); + + // Go in the future + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK)); + + // The lock has not yet expired since unlocking has a 2 week waiting time + let err = helper.withdraw(router_ref, "user").unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The lock time has not yet expired" + ); + + // Go to the future again + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK)); + + // Try to add more xASTRO to an expired position + let err = helper + .extend_lock_amount(router_ref, "user", 1f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The lock expired. Withdraw and create new lock" + ); + + // Imagine the user will withdraw their expired lock in 5 weeks + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(5 * WEEK)); + + // Time has passed so we can withdraw + helper.withdraw(router_ref, "user").unwrap(); + helper.check_xastro_balance(router_ref, "user", 100); + helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 0); + + // Create a new lock + helper + .extend_lock_amount(router_ref, "user", 50f32) + .unwrap(); + + let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(vp, 50.0); + + let vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 50.0); + + // Unlock the lock + helper.unlock(router_ref, "user").unwrap(); + + let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + + let vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 0.0); + + // Relock +} + +#[test] +fn random_token_lock() { + let mut router = mock_app(); + let router_ref = &mut router; + let owner = Addr::unchecked("owner"); + let helper = Helper::init(router_ref, owner); + + let random_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + let random_token_code_id = router.store_code(random_token_contract); + + let msg = astro::InstantiateMsg { + name: String::from("Random token"), + symbol: String::from("FOO"), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: helper.owner.to_string(), + cap: None, + }), + marketing: None, + }; + + let random_token = router + .instantiate_contract( + random_token_code_id, + helper.owner.clone(), + &msg, + &[], + String::from("FOO"), + None, + ) + .unwrap(); + + let msg = cw20::Cw20ExecuteMsg::Mint { + recipient: String::from("user"), + amount: Uint128::from(100_u128), + }; + + router + .execute_contract(helper.owner.clone(), random_token.clone(), &msg, &[]) + .unwrap(); + + let cw20msg = Cw20ExecuteMsg::Send { + contract: helper.voting_instance.to_string(), + amount: Uint128::from(10_u128), + msg: to_binary(&Cw20HookMsg::CreateLock { time: WEEK }).unwrap(), + }; + let err = router + .execute_contract(Addr::unchecked("user"), random_token, &cw20msg, &[]) + .unwrap_err(); + + assert_eq!(err.root_cause().to_string(), "Unauthorized"); +} + +#[test] +fn new_lock_after_unlock() { + let mut router = mock_app(); + let router_ref = &mut router; + let helper = Helper::init(router_ref, Addr::unchecked("owner")); + helper.mint_xastro(router_ref, "owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro(router_ref, "user", 100); + + helper + .create_lock(router_ref, "user", WEEK * 2, 50f32) + .unwrap(); + + let vp = helper.query_user_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + + let evp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + assert_eq!(evp, 50.0); + let evp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(evp, 50.0); + + // Go to the future + router_ref.update_block(next_block); + + helper.unlock(router_ref, "user").unwrap(); + router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK * 2)); + + helper.withdraw(router_ref, "user").unwrap(); + helper.check_xastro_balance(router_ref, "user", 100); + + let vp = helper.query_user_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + + // Create a new lock in 3 weeks from now + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK * 3)); + + helper + .create_lock(router_ref, "user", WEEK * 5, 100f32) + .unwrap(); + + let vp = helper.query_user_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + + let evp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + assert_eq!(evp, 100.0); + let evp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(evp, 100.0); +} + +/// Plot for this test case is generated at tests/plots/variable_decay.png +#[test] +fn emissions_voting_no_decay() { + let mut router = mock_app(); + let router_ref = &mut router; + let helper = Helper::init(router_ref, Addr::unchecked("owner")); + helper.mint_xastro(router_ref, "owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro(router_ref, "user", 100); + helper.mint_xastro(router_ref, "user2", 100); + + helper + .create_lock(router_ref, "user", WEEK * 10, 30f32) + .unwrap(); + + // Go to the future + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK * 5)); + + // Create lock for user2 + helper + .create_lock(router_ref, "user2", WEEK * 6, 50f32) + .unwrap(); + let vp = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + + let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(vp, 80.0); + + // Go to the future + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK * 4)); + + helper + .extend_lock_amount(router_ref, "user", 70f32) + .unwrap(); + + let vp = helper.query_user_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_vp(router_ref, "user2").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 100.0); + let vp = helper.query_user_emissions_vp(router_ref, "user2").unwrap(); + assert_eq!(vp, 50.0); + let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(vp, 150.0); + + let res = helper + .query_user_vp_at( + router_ref, + "user2", + router_ref.block_info().time.seconds() + 4 * WEEK, + ) + .unwrap(); + assert_eq!(res, 0.0); + let res = helper + .query_total_vp_at(router_ref, router_ref.block_info().time.seconds() + WEEK) + .unwrap(); + assert_eq!(res, 0.0); + + let res = helper + .query_user_emissions_vp_at( + router_ref, + "user2", + router_ref.block_info().time.seconds() + 4 * WEEK, + ) + .unwrap(); + assert_eq!(res, 50.0); + let res = helper + .query_total_emissions_vp_at(router_ref, router_ref.block_info().time.seconds() + WEEK) + .unwrap(); + assert_eq!(res, 150.0); + + // Go to the future + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK)); + let vp = helper.query_user_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_vp(router_ref, "user2").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + assert_eq!(vp, 100.0); + let vp = helper.query_user_emissions_vp(router_ref, "user2").unwrap(); + assert_eq!(vp, 50.0); + let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(vp, 150.0); +} + +#[test] +fn check_queries() { + let mut router = mock_app(); + let router_ref = &mut router; + let owner = Addr::unchecked("owner"); + let helper = Helper::init(router_ref, owner); + helper.mint_xastro(router_ref, "owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro(router_ref, "user", 100); + helper.check_xastro_balance(router_ref, "user", 100); + + // Create valid voting escrow lock + helper + .create_lock(router_ref, "user", WEEK * 2, 90f32) + .unwrap(); + // Check that 90 xASTRO were actually debited + helper.check_xastro_balance(router_ref, "user", 10); + helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 90); + + // Validate user's lock + let user_lock: LockInfoResponse = router_ref + .wrap() + .query_wasm_smart( + helper.voting_instance.clone(), + &QueryMsg::LockInfo { + user: "user".to_string(), + }, + ) + .unwrap(); + assert_eq!(user_lock.amount.u128(), 90_u128 * MULTIPLIER as u128); + // New locks must not have an end time + assert_eq!(user_lock.end, None); + + // Voting power must be 0 + let total_vp_at_ts = helper + .query_total_vp_at(router_ref, router_ref.block_info().time.seconds()) + .unwrap(); + assert_eq!(total_vp_at_ts, 0.0); + + // Must always be 0 + let period = get_lite_period(router_ref.block_info().time.seconds()).unwrap(); + let total_vp_at_period = helper.query_total_vp_at_period(router_ref, period).unwrap(); + assert_eq!(total_vp_at_period, 0.0); + + // Must always be 0 + let user_vp = helper + .query_user_vp_at(router_ref, "user", router_ref.block_info().time.seconds()) + .unwrap(); + assert_eq!(user_vp, 0.0); + + // Must always be 0 + let user_vp = helper + .query_user_vp_at_period(router_ref, "user", period) + .unwrap(); + assert_eq!(user_vp, 0.0); + + // Emissions voting power must be 90 + let total_emissions_vp_at_ts = helper + .query_total_emissions_vp_at(router_ref, router_ref.block_info().time.seconds()) + .unwrap(); + assert_eq!(total_emissions_vp_at_ts, 90.0); + + let user_emissions_vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + assert_eq!(user_emissions_vp, 90.0); + + let user_emissions_vp = helper + .query_user_emissions_vp_at(router_ref, "user", router_ref.block_info().time.seconds()) + .unwrap(); + assert_eq!(user_emissions_vp, 90.0); + + // Check users' locked xASTRO balance history + helper.mint_xastro(router_ref, "user", 90); + // SnapshotMap checkpoints the data at the next block + let start_time = Uint64::from(router_ref.block_info().time.seconds() + 1); + + let balance_timestamp = helper + .query_locked_balance_at(router_ref, "user", start_time) + .unwrap(); + assert_eq!(balance_timestamp, 90f32); + + router_ref.update_block(next_block); + helper + .extend_lock_amount(router_ref, "user", 100f32) + .unwrap(); + + let balance_timestamp = helper + .query_locked_balance_at(router_ref, "user", start_time) + .unwrap(); + assert_eq!(balance_timestamp, 90f32); + + router_ref.update_block(|bi| { + bi.height += 100000; + bi.time = bi.time.plus_seconds(500000); + }); + + let balance_timestamp = helper + .query_locked_balance_at(router_ref, "user", start_time) + .unwrap(); + assert_eq!(balance_timestamp, 90f32); + + let balance_timestamp = helper + .query_locked_balance_at( + router_ref, + "user", + start_time.saturating_add(Uint64::from(10u64)), // Next block adds 5 seconds + ) + .unwrap(); + assert_eq!(balance_timestamp, 190f32); + + // The user still has 190 xASTRO locked + let balance_timestamp = helper + .query_locked_balance_at( + router_ref, + "user", + Uint64::from(router_ref.block_info().time.seconds()), // Next block adds 5 seconds + ) + .unwrap(); + assert_eq!(balance_timestamp, 190f32); + + router_ref.update_block(|bi| { + bi.height += 1; + bi.time = bi.time.plus_seconds(WEEK * 102); + }); + helper.unlock(router_ref, "user").unwrap(); + + // Ensure emissions voting power is 0 after unlock + let user_emissions_vp = helper + .query_user_emissions_vp_at(router_ref, "user", router_ref.block_info().time.seconds()) + .unwrap(); + assert_eq!(user_emissions_vp, 0.0); + + // Forward until after unlock period ends + router_ref.update_block(|bi| { + bi.height += 1; + bi.time = bi.time.plus_seconds(WEEK * 102); + }); + // Withdraw + helper.withdraw(router_ref, "user").unwrap(); + + // Now the users' balance is zero + // But one block before it had 190 xASTRO locked + let balance_timestamp = helper + .query_locked_balance_at( + router_ref, + "user", + Uint64::from(router_ref.block_info().time.seconds() + 5), // Next block adds 5 seconds + ) + .unwrap(); + assert_eq!(balance_timestamp, 0f32); + + let balance_timestamp = helper + .query_locked_balance_at( + router_ref, + "user", + Uint64::from(router_ref.block_info().time.seconds() - 5), // Next block adds 5 seconds + ) + .unwrap(); + assert_eq!(balance_timestamp, 190f32); + + // add users to the blacklist + helper + .update_blacklist( + router_ref, + Some(vec![ + "voter1".to_string(), + "voter2".to_string(), + "voter3".to_string(), + "voter4".to_string(), + "voter5".to_string(), + "voter6".to_string(), + "voter7".to_string(), + "voter8".to_string(), + ]), + None, + ) + .unwrap(); + + // query all blacklisted voters + let blacklisted_voters = helper + .query_blacklisted_voters(router_ref, None, None) + .unwrap(); + assert_eq!( + blacklisted_voters, + vec![ + Addr::unchecked("voter1"), + Addr::unchecked("voter2"), + Addr::unchecked("voter3"), + Addr::unchecked("voter4"), + Addr::unchecked("voter5"), + Addr::unchecked("voter6"), + Addr::unchecked("voter7"), + Addr::unchecked("voter8"), + ] + ); + + // query not blacklisted voter + let err = helper + .query_blacklisted_voters(router_ref, Some("voter9".to_string()), Some(10u32)) + .unwrap_err(); + assert_eq!( + StdError::generic_err( + "Querier contract error: Generic error: The voter9 address is not blacklisted" + ), + err + ); + + // query voters by specified parameters + let blacklisted_voters = helper + .query_blacklisted_voters(router_ref, Some("voter2".to_string()), Some(2u32)) + .unwrap(); + assert_eq!( + blacklisted_voters, + vec![Addr::unchecked("voter3"), Addr::unchecked("voter4")] + ); + + // add users to the blacklist + helper + .update_blacklist( + router_ref, + Some(vec!["voter0".to_string(), "voter33".to_string()]), + None, + ) + .unwrap(); + + // query voters by specified parameters + let blacklisted_voters = helper + .query_blacklisted_voters(router_ref, Some("voter2".to_string()), Some(2u32)) + .unwrap(); + assert_eq!( + blacklisted_voters, + vec![Addr::unchecked("voter3"), Addr::unchecked("voter33")] + ); + + let blacklisted_voters = helper + .query_blacklisted_voters(router_ref, Some("voter4".to_string()), Some(10u32)) + .unwrap(); + assert_eq!( + blacklisted_voters, + vec![ + Addr::unchecked("voter5"), + Addr::unchecked("voter6"), + Addr::unchecked("voter7"), + Addr::unchecked("voter8"), + ] + ); + + let empty_blacklist: Vec = vec![]; + let blacklisted_voters = helper + .query_blacklisted_voters(router_ref, Some("voter8".to_string()), Some(10u32)) + .unwrap(); + assert_eq!(blacklisted_voters, empty_blacklist); + + // check if voters are blacklisted + let res = helper + .check_voters_are_blacklisted(router_ref, vec!["voter1".to_string(), "voter9".to_string()]) + .unwrap(); + assert_eq!("Voter is not blacklisted: voter9", res.to_string()); + + let res = helper + .check_voters_are_blacklisted(router_ref, vec!["voter1".to_string(), "voter8".to_string()]) + .unwrap(); + assert_eq!("Voters are blacklisted!", res.to_string()); +} + +#[test] +fn check_deposit_for() { + let mut router = mock_app(); + let router_ref = &mut router; + let owner = Addr::unchecked("owner"); + let helper = Helper::init(router_ref, owner); + helper.mint_xastro(router_ref, "owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro(router_ref, "user1", 100); + helper.check_xastro_balance(router_ref, "user1", 100); + helper.mint_xastro(router_ref, "user2", 100); + helper.check_xastro_balance(router_ref, "user2", 100); + + // 104 weeks ~ 2 years + helper + .create_lock(router_ref, "user1", 104 * WEEK, 50f32) + .unwrap(); + let vp = helper.query_user_vp(router_ref, "user1").unwrap(); + assert_eq!(0.0, vp); + let vp = helper.query_user_emissions_vp(router_ref, "user1").unwrap(); + assert_eq!(50.0, vp); + + helper + .deposit_for(router_ref, "user2", "user1", 50f32) + .unwrap(); + let vp = helper.query_user_vp(router_ref, "user1").unwrap(); + assert_eq!(0.0, vp); + let vp = helper.query_user_emissions_vp(router_ref, "user1").unwrap(); + assert_eq!(100.0, vp); + helper.check_xastro_balance(router_ref, "user1", 50); + helper.check_xastro_balance(router_ref, "user2", 50); +} + +#[test] +fn check_update_owner() { + let mut app = mock_app(); + let owner = Addr::unchecked("owner"); + let helper = Helper::init(&mut app, owner); + + let new_owner = String::from("new_owner"); + + // New owner + let msg = ExecuteMsg::ProposeNewOwner { + new_owner: new_owner.clone(), + expires_in: 100, // seconds + }; + + // Unauthed check + let err = app + .execute_contract( + Addr::unchecked("not_owner"), + helper.voting_instance.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let err = app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.voting_instance.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner + app.execute_contract( + Addr::unchecked("owner"), + helper.voting_instance.clone(), + &msg, + &[], + ) + .unwrap(); + + // Claim from invalid addr + let err = app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.voting_instance.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim ownership + app.execute_contract( + Addr::unchecked(new_owner.clone()), + helper.voting_instance.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); + + // Let's query the contract state + let msg = QueryMsg::Config {}; + let res: Config = app + .wrap() + .query_wasm_smart(&helper.voting_instance, &msg) + .unwrap(); + + assert_eq!(res.owner, new_owner) +} + +#[test] +fn check_blacklist() { + let mut router = mock_app(); + let router_ref = &mut router; + let owner = Addr::unchecked("owner"); + let helper = Helper::init(router_ref, owner); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro(router_ref, "user1", 100); + helper.mint_xastro(router_ref, "user2", 100); + helper.mint_xastro(router_ref, "user3", 100); + + // Try to execute with empty arrays + let err = helper.update_blacklist(router_ref, None, None).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Append and remove arrays are empty" + ); + + // Blacklisting user2 + let res = helper + .update_blacklist(router_ref, Some(vec!["user2".to_string()]), None) + .unwrap(); + assert_eq!( + res.events[1].attributes[1], + attr("action", "update_blacklist") + ); + assert_eq!( + res.events[1].attributes[2], + attr("added_addresses", "user2") + ); + + helper + .create_lock(router_ref, "user1", WEEK * 10, 50f32) + .unwrap(); + // Try to create lock from a blacklisted address + let err = helper + .create_lock(router_ref, "user2", WEEK * 10, 100f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The user2 address is blacklisted" + ); + let err = helper + .deposit_for(router_ref, "user2", "user3", 50f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The user2 address is blacklisted" + ); + + // Since user2 is blacklisted, their xASTRO balance was left unchanged + helper.check_xastro_balance(router_ref, "user2", 100); + // And they did not create a lock, thus we have no information to query + let vp = helper.query_user_vp(router_ref, "user2").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_emissions_vp(router_ref, "user2").unwrap(); + assert_eq!(vp, 0.0); + + // Go to the future + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(2 * WEEK)); + + // user2 is still blacklisted + let err = helper + .create_lock(router_ref, "user2", WEEK * 10, 100f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The user2 address is blacklisted" + ); + + // Blacklisting user1 using the guardian + let msg = ExecuteMsg::UpdateBlacklist { + append_addrs: Some(vec!["user1".to_string()]), + remove_addrs: None, + }; + let res = router_ref + .execute_contract( + Addr::unchecked("guardian"), + helper.voting_instance.clone(), + &msg, + &[], + ) + .unwrap(); + assert_eq!( + res.events[1].attributes[1], + attr("action", "update_blacklist") + ); + assert_eq!( + res.events[1].attributes[2], + attr("added_addresses", "user1") + ); + + let err = helper + .extend_lock_amount(router_ref, "user1", 10f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The user1 address is blacklisted" + ); + let err = helper + .deposit_for(router_ref, "user2", "user1", 50f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The user2 address is blacklisted" + ); + let err = helper + .deposit_for(router_ref, "user3", "user1", 50f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The user1 address is blacklisted" + ); + // user1 doesn't have voting power now + let vp = helper.query_user_vp(router_ref, "user1").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_emissions_vp(router_ref, "user1").unwrap(); + assert_eq!(vp, 0.0); + // Voting + let vp = helper + .query_user_vp_at( + router_ref, + "user1", + router_ref.block_info().time.seconds() - WEEK, + ) + .unwrap(); + assert_eq!(vp, 0f32); + // Total voting power should be zero as well since there was only one vxASTRO position created by user1 + let vp = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + // Total emissions voting power should be zero as well since there was only one vxASTRO position created by user1 + let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(vp, 0.0); + + // The only option available for a blacklisted user is to unlock and withdraw their funds + helper.unlock(router_ref, "user1").unwrap(); + + // Go to the future + router_ref.update_block(next_block); + router_ref.update_block(|block| block.time = block.time.plus_seconds(20 * WEEK)); + + // The only option available for a blacklisted user is to withdraw their funds + helper.withdraw(router_ref, "user1").unwrap(); + + // Remove user1 from the blacklist + let res = helper + .update_blacklist(router_ref, None, Some(vec!["user1".to_string()])) + .unwrap(); + assert_eq!( + res.events[1].attributes[1], + attr("action", "update_blacklist") + ); + assert_eq!( + res.events[1].attributes[2], + attr("removed_addresses", "user1") + ); + + // Now user1 can create a new lock + helper + .create_lock(router_ref, "user1", WEEK, 10f32) + .unwrap(); +} + +#[test] +fn check_residual() { + let mut router = mock_app(); + let router_ref = &mut router; + let owner = Addr::unchecked("owner"); + let helper = Helper::init(router_ref, owner); + let lock_duration = 104; + let users_num = 1000; + let lock_amount = 100_000_000; + + helper.mint_xastro(router_ref, "owner", 100); + + for i in 1..(users_num / 2) { + let user = &format!("user{}", i); + helper.mint_xastro(router_ref, user, 100); + helper + .create_lock_u128(router_ref, user, WEEK * lock_duration, lock_amount) + .unwrap(); + } + + let mut sum = 0; + for i in 1..=users_num { + let user = &format!("user{}", i); + sum += helper.query_exact_user_vp(router_ref, user).unwrap(); + } + + assert_eq!(sum, helper.query_exact_total_vp(router_ref).unwrap()); + + let mut sum = 0; + for i in 1..=users_num { + let user = &format!("user{}", i); + sum += helper + .query_exact_user_emissions_vp(router_ref, user) + .unwrap(); + } + + assert_eq!( + sum, + helper.query_exact_total_emissions_vp(router_ref).unwrap() + ); + + router_ref.update_block(|bi| { + bi.height += 1; + bi.time = bi.time.plus_seconds(WEEK); + }); + + for i in (users_num / 2)..users_num { + let user = &format!("user{}", i); + helper.mint_xastro(router_ref, user, 1000000); + helper + .create_lock_u128(router_ref, user, WEEK * lock_duration, lock_amount) + .unwrap(); + } + + for _ in 1..104 { + sum = 0; + for i in 1..=users_num { + let user = &format!("user{}", i); + sum += helper.query_exact_user_vp(router_ref, user).unwrap(); + } + + let ve_vp = helper.query_exact_total_vp(router_ref).unwrap(); + let diff = (sum as f64 - ve_vp as f64).abs(); + assert_eq!(diff, 0.0, "diff: {}, sum: {}, ve_vp: {}", diff, sum, ve_vp); + + router_ref.update_block(|bi| { + bi.height += 1; + bi.time = bi.time.plus_seconds(WEEK); + }); + } + + for _ in 1..104 { + sum = 0; + for i in 1..=users_num { + let user = &format!("user{}", i); + sum += helper + .query_exact_user_emissions_vp(router_ref, user) + .unwrap(); + } + + let ve_vp = helper.query_exact_total_emissions_vp(router_ref).unwrap(); + let diff = (sum as f64 - ve_vp as f64).abs(); + assert_eq!(diff, 0.0, "diff: {}, sum: {}, ve_vp: {}", diff, sum, ve_vp); + + router_ref.update_block(|bi| { + bi.height += 1; + bi.time = bi.time.plus_seconds(WEEK); + }); + } +} + +#[test] +fn total_vp_multiple_slope_subtraction() { + let mut router = mock_app(); + let router_ref = &mut router; + let owner = Addr::unchecked("owner"); + let helper = Helper::init(router_ref, owner); + + helper.mint_xastro(router_ref, "user1", 1000); + helper + .create_lock(router_ref, "user1", 2 * WEEK, 100f32) + .unwrap(); + let total = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(total, 0.0); + let total = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(total, 100.0); + + router_ref.update_block(|bi| bi.time = bi.time.plus_seconds(2 * WEEK)); + // Slope changes have been applied + let total = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(total, 0.0); + let total = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(total, 100.0); + + helper.unlock(router_ref, "user1").unwrap(); + + // Try to manipulate over expired lock 3 weeks later + router_ref.update_block(|bi| bi.time = bi.time.plus_seconds(3 * WEEK)); + + let err = helper + .extend_lock_amount(router_ref, "user1", 100f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The lock expired. Withdraw and create new lock" + ); + + let err = helper + .create_lock(router_ref, "user1", 2 * WEEK, 100f32) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Lock already exists, either unlock and withdraw or extend_lock to add to the lock" + ); + + let total = helper.query_total_vp(router_ref).unwrap(); + assert_eq!(total, 0f32); + let total = helper.query_total_emissions_vp(router_ref).unwrap(); + assert_eq!(total, 0f32); +} + +#[test] +fn marketing_info() { + let mut router = mock_app(); + let router_ref = &mut router; + let owner = Addr::unchecked("owner"); + let helper = Helper::init(router_ref, owner); + + let err = router_ref + .execute_contract( + helper.owner.clone(), + helper.voting_instance.clone(), + &ExecuteMsg::SetLogoUrlsWhitelist { + whitelist: vec![ + "@hello-test-url .com/".to_string(), + "example.com/".to_string(), + ], + }, + &[], + ) + .unwrap_err(); + assert_eq!( + &err.root_cause().to_string(), + "Generic error: Link contains invalid characters: @hello-test-url .com/" + ); + + let err = router_ref + .execute_contract( + helper.owner.clone(), + helper.voting_instance.clone(), + &ExecuteMsg::SetLogoUrlsWhitelist { + whitelist: vec!["example.com".to_string()], + }, + &[], + ) + .unwrap_err(); + assert_eq!( + &err.root_cause().to_string(), + "Marketing info validation error: Whitelist link should end with '/': example.com" + ); + + router_ref + .execute_contract( + helper.owner.clone(), + helper.voting_instance.clone(), + &ExecuteMsg::SetLogoUrlsWhitelist { + whitelist: vec!["example.com/".to_string()], + }, + &[], + ) + .unwrap(); + + let err = router_ref + .execute_contract( + helper.owner.clone(), + helper.voting_instance.clone(), + &ExecuteMsg::UpdateMarketing { + project: Some("".to_string()), + description: None, + marketing: None, + }, + &[], + ) + .unwrap_err(); + + assert_eq!( + &err.root_cause().to_string(), + "Marketing info validation error: project contains invalid characters: " + ); + + let err = router_ref + .execute_contract( + helper.owner.clone(), + helper.voting_instance.clone(), + &ExecuteMsg::UpdateMarketing { + project: None, + description: Some("".to_string()), + marketing: None, + }, + &[], + ) + .unwrap_err(); + assert_eq!( + &err.root_cause().to_string(), + "Marketing info validation error: description contains invalid characters: " + ); + + router_ref + .execute_contract( + helper.owner.clone(), + helper.voting_instance.clone(), + &ExecuteMsg::UpdateMarketing { + project: Some("Some project".to_string()), + description: Some("Some description".to_string()), + marketing: None, + }, + &[], + ) + .unwrap(); + + let config: Config = router_ref + .wrap() + .query_wasm_smart(&helper.voting_instance, &QueryMsg::Config {}) + .unwrap(); + assert_eq!(config.logo_urls_whitelist, vec!["example.com/".to_string()]); + let marketing_info: MarketingInfoResponse = router_ref + .wrap() + .query_wasm_smart(&helper.voting_instance, &QueryMsg::MarketingInfo {}) + .unwrap(); + assert_eq!(marketing_info.project, Some("Some project".to_string())); + assert_eq!( + marketing_info.description, + Some("Some description".to_string()) + ); + + let err = router_ref + .execute_contract( + helper.owner.clone(), + helper.voting_instance.clone(), + &ExecuteMsg::UploadLogo(Logo::Url("https://some-website.com/logo.svg".to_string())), + &[], + ) + .unwrap_err(); + assert_eq!( + &err.root_cause().to_string(), + "Marketing info validation error: Logo link is not whitelisted: https://some-website.com/logo.svg", + ); + + router_ref + .execute_contract( + helper.owner.clone(), + helper.voting_instance.clone(), + &ExecuteMsg::UploadLogo(Logo::Url("example.com/logo.svg".to_string())), + &[], + ) + .unwrap(); + + let marketing_info: MarketingInfoResponse = router_ref + .wrap() + .query_wasm_smart(&helper.voting_instance, &QueryMsg::MarketingInfo {}) + .unwrap(); + assert_eq!( + marketing_info.logo.unwrap(), + LogoInfo::Url("example.com/logo.svg".to_string()) + ); +} diff --git a/contracts/voting_escrow_lite/tests/simulation.todo b/contracts/voting_escrow_lite/tests/simulation.todo new file mode 100644 index 00000000..63fba753 --- /dev/null +++ b/contracts/voting_escrow_lite/tests/simulation.todo @@ -0,0 +1,394 @@ +use crate::test_utils::{mock_app, Helper, MULTIPLIER}; +use anyhow::Result; +use astroport_governance::utils::{ + get_lite_period, get_lite_periods_count, EPOCH_START, LITE_VOTING_PERIOD, MAX_LOCK_TIME, +}; +use cosmwasm_std::Addr; +use cw_multi_test::{next_block, App, AppResponse}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; + +mod test_utils; + +#[derive(Clone, Default, Debug)] +struct Point { + amount: f64, + end: u64, +} + +#[derive(Clone, Debug)] +enum Event { + CreateLock(f64, u64), + ExtendLock(f64), + Withdraw, + Blacklist, + Recover, +} + +use Event::*; + +struct Simulator { + // Point history (history[period][user] = point) + points: Vec>, + // Current user's lock (amount, end) + locked: HashMap, + users: Vec, + helper: Helper, + router: App, +} + +fn apply_coefficient(amount: f64) -> f64 { + // No coefficient in lite version + (amount * MULTIPLIER as f64).trunc() / MULTIPLIER as f64 +} + +impl Simulator { + fn new>(users: &[T]) -> Self { + let mut router = mock_app(); + Self { + points: vec![HashMap::new(); 10000], + locked: Default::default(), + users: users.iter().cloned().map(|user| user.into()).collect(), + helper: Helper::init(&mut router, Addr::unchecked("owner")), + router, + } + } + + fn mint(&mut self, user: &str, amount: u128) { + self.helper + .mint_xastro(&mut self.router, user, amount as u64) + } + + fn block_period(&self) -> u64 { + get_lite_period(self.router.block_info().time.seconds()).unwrap() + } + + fn app_next_period(&mut self) { + self.router.update_block(next_block); + self.router + .update_block(|block| block.time = block.time.plus_seconds(LITE_VOTING_PERIOD)); + } + + fn create_lock(&mut self, user: &str, amount: f64, interval: u64) -> Result { + let block_period = self.block_period(); + let periods_interval = get_lite_periods_count(interval); + self.helper + .create_lock(&mut self.router, user, interval, amount as f32) + .map(|response| { + self.add_point( + block_period as usize, + user, + apply_coefficient(amount), + block_period + periods_interval, + ); + self.locked.extend(vec![( + user.to_string(), + (amount, block_period + periods_interval), + )]); + response + }) + } + + fn extend_lock(&mut self, user: &str, amount: f64) -> Result { + self.helper + .extend_lock_amount(&mut self.router, user, amount as f32) + .map(|response| { + let cur_period = self.block_period() as usize; + let (user_balance, end) = + if let Some(point) = self.get_user_point_at(cur_period, user) { + (point.amount, point.end) + } else { + let prev_point = self + .get_prev_point(user) + .expect("We always need previous point!"); + (self.calc_user_balance_at(cur_period, user), prev_point.end) + }; + let vp = apply_coefficient(amount); + self.add_point(cur_period, user, user_balance + vp, end); + let mut lock = self.locked.get_mut(user).unwrap(); + lock.0 += amount; + response + }) + } + + fn withdraw(&mut self, user: &str) -> Result { + self.helper + .withdraw(&mut self.router, user) + .map(|response| { + let cur_period = self.block_period(); + self.add_point(cur_period as usize, user, 0.0, cur_period); + self.locked.remove(user); + response + }) + } + + fn append2blacklist(&mut self, user: &str) -> Result { + self.helper + .update_blacklist(&mut self.router, Some(vec![user.to_string()]), None) + .map(|response| { + let cur_period = self.block_period(); + self.add_point(cur_period as usize, user, 0.0, cur_period); + response + }) + } + + fn remove_from_blacklist(&mut self, user: &str) -> Result { + self.helper + .update_blacklist(&mut self.router, None, Some(vec![user.to_string()])) + .map(|response| { + let cur_period = self.block_period() as usize; + if let Some((amount, end)) = self.locked.get(user).copied() { + // Amount stays constant, no need to recalculate based on period + self.add_point(cur_period, user, apply_coefficient(amount), end); + } + response + }) + } + + fn event_router(&mut self, user: &str, event: Event) { + match event { + Event::CreateLock(amount, interval) => { + if let Err(err) = self.create_lock(user, amount, interval) { + dbg!(err); + } + } + Event::ExtendLock(amount) => { + if let Err(err) = self.extend_lock(user, amount) { + dbg!(err); + } + } + Event::Withdraw => { + if let Err(err) = self.withdraw(user) { + dbg!(err); + } + } + Event::Blacklist => { + if let Err(err) = self.append2blacklist(user) { + dbg!(err); + } + } + Event::Recover => { + if let Err(err) = self.remove_from_blacklist(user) { + dbg!(err); + } + } + } + let real_balance = self + .get_user_point_at(self.block_period() as usize, user) + .map(|point| point.amount) + .unwrap_or_else(|| self.calc_user_balance_at(self.block_period() as usize, user)); + let contract_balance = self + .helper + .query_user_emissions_vp(&mut self.router, user) + .unwrap_or(0.0) as f64; + if (real_balance - contract_balance).abs() >= 10e-3 { + assert_eq!(real_balance, contract_balance) + }; + } + + fn checkpoint_all_users(&mut self) { + let cur_period = self.block_period() as usize; + self.users.clone().iter().for_each(|user| { + // we need to calc point only if it was not calculated yet + if self.get_user_point_at(cur_period, user).is_none() { + self.checkpoint_user(user) + } + }) + } + + fn add_point>(&mut self, period: usize, user: T, amount: f64, end: u64) { + let map = &mut self.points[period]; + map.extend(vec![(user.into(), Point { amount, end })]); + } + + fn get_prev_point(&mut self, user: &str) -> Option { + let prev_period = (self.block_period() - 1) as usize; + self.get_user_point_at(prev_period, user) + } + + fn checkpoint_user(&mut self, user: &str) { + let cur_period = self.block_period() as usize; + let user_balance = self.calc_user_balance_at(cur_period, user); + let prev_point = self + .get_prev_point(user) + .expect("We always need previous point!"); + self.add_point(cur_period, user, user_balance, prev_point.end); + } + + fn get_user_point_at>(&mut self, period: usize, user: T) -> Option { + let points_map = &mut self.points[period]; + match points_map.entry(user.into()) { + Entry::Occupied(value) => Some(value.get().clone()), + Entry::Vacant(_) => None, + } + } + + fn calc_user_balance_at(&mut self, period: usize, user: &str) -> f64 { + match self.get_user_point_at(period, user) { + Some(point) => point.amount, + None => { + let prev_point = self + .get_user_point_at(period - 1, user) + .expect("We always need previous point!"); + + // No calculations needed as nothing decays + prev_point.amount + } + } + } + + fn calc_total_balance_at(&mut self, period: usize) -> f64 { + self.users.clone().iter().fold(0.0, |acc, user| { + acc + self.get_user_point_at(period, user).unwrap().amount + }) + } +} + +use proptest::prelude::*; + +const MAX_PERIOD: usize = 10; +const MAX_USERS: usize = 6; +const MAX_EVENTS: usize = 100; + +fn amount_strategy() -> impl Strategy { + // (1f64..=100f64).prop_map(|val| (val * MULTIPLIER as f64).trunc() / MULTIPLIER as f64) + (1f64..=2f64).prop_map(|val| (val * MULTIPLIER as f64).trunc() / MULTIPLIER as f64) +} + +fn events_strategy() -> impl Strategy { + prop_oneof![ + Just(Event::Withdraw), + Just(Event::Blacklist), + Just(Event::Recover), + amount_strategy().prop_map(Event::ExtendLock), + (amount_strategy(), 0..MAX_LOCK_TIME).prop_map(|(a, b)| Event::CreateLock(a, b)), + ] +} + +fn generate_cases() -> impl Strategy, Vec<(usize, String, Event)>)> { + let users_strategy = prop::collection::vec("[a-z]{4,32}", 1..MAX_USERS); + users_strategy.prop_flat_map(|users| { + ( + Just(users.clone()), + prop::collection::vec( + ( + 1..=MAX_PERIOD, + prop::sample::select(users), + events_strategy(), + ), + 0..MAX_EVENTS, + ), + ) + }) +} + +proptest! { + #[test] + fn run_simulations + ( + case in generate_cases() + ) { + let mut events: Vec> = vec![vec![]; MAX_PERIOD + 1]; + let (users, events_tuples) = case; + for (period, user, event) in events_tuples { + events[period].push((user, event)); + }; + + let mut simulator = Simulator::new(&users); + for user in users { + simulator.mint(&user, 10000); + simulator.add_point(0, user, 0.0, 104); + } + simulator.app_next_period(); + + for period in 1..=MAX_PERIOD { + if let Some(period_events) = events.get(period) { + for (user, event) in period_events { + simulator.event_router(user, event.clone()) + } + } + simulator.checkpoint_all_users(); + let real_balance = simulator.calc_total_balance_at(period); + let contract_balance = simulator + .helper + .query_total_emissions_vp(&mut simulator.router) + .unwrap_or(0.0) as f64; + if (real_balance - contract_balance).abs() >= 10e-3 { + assert_eq!(real_balance, contract_balance) + }; + // Evaluate historical periods + for check_period in 1..period { + let real_balance = simulator.calc_total_balance_at(check_period); + let contract_balance = simulator + .helper + .query_total_emissions_vp_at(&mut simulator.router, EPOCH_START + check_period as u64 * LITE_VOTING_PERIOD) + .unwrap_or(0.0) as f64; + if (real_balance - contract_balance).abs() >= 10e-3 { + assert_eq!(real_balance, contract_balance) + }; + } + simulator.app_next_period() + } + } +} + +#[test] +fn exact_simulation() { + let case = ( + ["bpcy"], + [ + (1, "bpcy", CreateLock(100.0, 3024000)), + (3, "bpcy", Blacklist), + (3, "bpcy", Recover), + ], + ); + + let mut events: Vec> = vec![vec![]; MAX_PERIOD + 1]; + let (users, events_tuples) = case; + for (period, user, event) in events_tuples { + events[period].push((user.to_string(), event)); + } + + let mut simulator = Simulator::new(&users); + for user in users { + simulator.mint(user, 10000); + simulator.add_point(0, user, 0.0, 104); + } + simulator.app_next_period(); + + for period in 1..=MAX_PERIOD { + if let Some(period_events) = events.get(period) { + if !period_events.is_empty() { + println!("Period {}:", period); + } + for (user, event) in period_events { + simulator.event_router(user, event.clone()) + } + } + simulator.checkpoint_all_users(); + let real_balance = simulator.calc_total_balance_at(period); + let contract_balance = simulator + .helper + .query_total_emissions_vp(&mut simulator.router) + .unwrap_or(0.0) as f64; + if (real_balance - contract_balance).abs() >= 10e-3 { + println!("Assert failed at period {}", period); + assert_eq!(real_balance, contract_balance) + }; + // Evaluate historical periods + for check_period in 1..period { + let real_balance = simulator.calc_total_balance_at(check_period); + let contract_balance = simulator + .helper + .query_total_emissions_vp_at( + &mut simulator.router, + EPOCH_START + check_period as u64 * LITE_VOTING_PERIOD, + ) + .unwrap_or(0.0) as f64; + if (real_balance - contract_balance).abs() >= 10e-3 { + assert_eq!(real_balance, contract_balance) + }; + } + simulator.app_next_period() + } +} diff --git a/contracts/voting_escrow_lite/tests/test_utils.rs b/contracts/voting_escrow_lite/tests/test_utils.rs new file mode 100644 index 00000000..015af405 --- /dev/null +++ b/contracts/voting_escrow_lite/tests/test_utils.rs @@ -0,0 +1,625 @@ +use anyhow::Result; +use astroport::{staking as xastro, token as astro}; +use astroport_governance::utils::EPOCH_START; +use astroport_governance::voting_escrow_lite::{ + BlacklistedVotersResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, + UpdateMarketingInfo, VotingPowerResponse, +}; +use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; +use cosmwasm_std::{ + attr, to_binary, Addr, QueryRequest, StdResult, Timestamp, Uint128, Uint64, WasmQuery, +}; +use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, Logo, MinterResponse}; +use cw_multi_test::{App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor}; +use voting_escrow_lite::astroport; + +pub const MULTIPLIER: u64 = 1000000; + +pub struct Helper { + pub owner: Addr, + pub astro_token: Addr, + pub staking_instance: Addr, + pub xastro_token: Addr, + pub voting_instance: Addr, +} + +impl Helper { + pub fn init(router: &mut App, owner: Addr) -> Self { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + let astro_token_code_id = router.store_code(astro_token_contract); + + let msg = astro::InstantiateMsg { + name: String::from("Astro token"), + symbol: String::from("ASTRO"), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + let astro_token = router + .instantiate_contract( + astro_token_code_id, + owner.clone(), + &msg, + &[], + String::from("ASTRO"), + None, + ) + .unwrap(); + + let staking_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_staking::contract::execute, + astroport_staking::contract::instantiate, + astroport_staking::contract::query, + ) + .with_reply_empty(astroport_staking::contract::reply), + ); + + let staking_code_id = router.store_code(staking_contract); + + let msg = xastro::InstantiateMsg { + owner: owner.to_string(), + token_code_id: astro_token_code_id, + deposit_token_addr: astro_token.to_string(), + marketing: None, + }; + let staking_instance = router + .instantiate_contract( + staking_code_id, + owner.clone(), + &msg, + &[], + String::from("xASTRO"), + None, + ) + .unwrap(); + + let res = router + .wrap() + .query::(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: staking_instance.to_string(), + msg: to_binary(&xastro::QueryMsg::Config {}).unwrap(), + })) + .unwrap(); + + let generator_controller = Box::new(ContractWrapper::new_with_empty( + astroport_generator_controller::contract::execute, + astroport_generator_controller::contract::instantiate, + astroport_generator_controller::contract::query, + )); + + let generator_controller_id = router.store_code(generator_controller); + + let msg = astroport_governance::generator_controller_lite::InstantiateMsg { + owner: owner.to_string(), + assembly_addr: "assembly".to_string(), + escrow_addr: "contract4".to_string(), + factory_addr: "factory".to_string(), + generator_addr: "generator".to_string(), + hub_addr: None, + pools_limit: 10, + whitelisted_pools: vec![], + }; + let generator_controller_instance = router + .instantiate_contract( + generator_controller_id, + owner.clone(), + &msg, + &[], + String::from("Generator Controller Lite"), + None, + ) + .unwrap(); + + let voting_contract = Box::new(ContractWrapper::new_with_empty( + voting_escrow_lite::execute::execute, + voting_escrow_lite::contract::instantiate, + voting_escrow_lite::query::query, + )); + + let voting_code_id = router.store_code(voting_contract); + + let marketing_info = UpdateMarketingInfo { + project: Some("Astroport".to_string()), + description: Some("Astroport is a decentralized application for managing the supply of space resources.".to_string()), + marketing: Some(owner.to_string()), + logo: Some(Logo::Url("https://astroport.com/logo.png".to_string())), + }; + + let msg = InstantiateMsg { + owner: owner.to_string(), + guardian_addr: Some("guardian".to_string()), + deposit_token_addr: res.share_token_addr.to_string(), + marketing: Some(marketing_info), + logo_urls_whitelist: vec!["https://astroport.com/".to_string()], + generator_controller_addr: Some(generator_controller_instance.to_string()), + outpost_addr: None, + }; + let voting_instance = router + .instantiate_contract( + voting_code_id, + owner.clone(), + &msg, + &[], + String::from("vxASTRO"), + None, + ) + .unwrap(); + + Self { + owner, + xastro_token: res.share_token_addr, + astro_token, + staking_instance, + voting_instance, + } + } + + pub fn mint_xastro(&self, router: &mut App, to: &str, amount: u64) { + let amount = amount * MULTIPLIER; + let msg = cw20::Cw20ExecuteMsg::Mint { + recipient: String::from(to), + amount: Uint128::from(amount), + }; + let res = router + .execute_contract(self.owner.clone(), self.astro_token.clone(), &msg, &[]) + .unwrap(); + assert_eq!(res.events[1].attributes[1], attr("action", "mint")); + assert_eq!(res.events[1].attributes[2], attr("to", String::from(to))); + assert_eq!( + res.events[1].attributes[3], + attr("amount", Uint128::from(amount)) + ); + + let to_addr = Addr::unchecked(to); + let msg = Cw20ExecuteMsg::Send { + contract: self.staking_instance.to_string(), + msg: to_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), + amount: Uint128::from(amount), + }; + router + .execute_contract(to_addr, self.astro_token.clone(), &msg, &[]) + .unwrap(); + } + + #[allow(dead_code)] + pub fn check_xastro_balance(&self, router: &mut App, user: &str, amount: u64) { + let amount = amount * MULTIPLIER; + let res: BalanceResponse = router + .wrap() + .query_wasm_smart( + self.xastro_token.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(res.balance.u128(), amount as u128); + } + + #[allow(dead_code)] + pub fn check_astro_balance(&self, router: &mut App, user: &str, amount: u64) { + let amount = amount * MULTIPLIER; + let res: BalanceResponse = router + .wrap() + .query_wasm_smart( + self.astro_token.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(res.balance.u128(), amount as u128); + } + + pub fn create_lock( + &self, + router: &mut App, + user: &str, + time: u64, + amount: f32, + ) -> Result { + let amount = (amount * MULTIPLIER as f32) as u64; + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.voting_instance.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + }; + router.execute_contract( + Addr::unchecked(user), + self.xastro_token.clone(), + &cw20msg, + &[], + ) + } + + #[allow(dead_code)] + pub fn create_lock_u128( + &self, + router: &mut App, + user: &str, + time: u64, + amount: u128, + ) -> Result { + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.voting_instance.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + }; + router.execute_contract( + Addr::unchecked(user), + self.xastro_token.clone(), + &cw20msg, + &[], + ) + } + + pub fn extend_lock_amount( + &self, + router: &mut App, + user: &str, + amount: f32, + ) -> Result { + let amount = (amount * MULTIPLIER as f32) as u64; + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.voting_instance.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + }; + router.execute_contract( + Addr::unchecked(user), + self.xastro_token.clone(), + &cw20msg, + &[], + ) + } + + #[allow(dead_code)] + pub fn relock(&self, router: &mut App, user: &str) -> Result { + router.execute_contract( + Addr::unchecked("outpost"), + self.voting_instance.clone(), + &ExecuteMsg::Relock { + user: user.to_string(), + }, + &[], + ) + } + + #[allow(dead_code)] + pub fn deposit_for( + &self, + router: &mut App, + from: &str, + to: &str, + amount: f32, + ) -> Result { + let amount = (amount * MULTIPLIER as f32) as u64; + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.voting_instance.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::DepositFor { + user: to.to_string(), + }) + .unwrap(), + }; + router.execute_contract( + Addr::unchecked(from), + self.xastro_token.clone(), + &cw20msg, + &[], + ) + } + + #[allow(dead_code)] + pub fn unlock(&self, router: &mut App, user: &str) -> Result { + router.execute_contract( + Addr::unchecked(user), + self.voting_instance.clone(), + &ExecuteMsg::Unlock {}, + &[], + ) + } + + pub fn withdraw(&self, router: &mut App, user: &str) -> Result { + router.execute_contract( + Addr::unchecked(user), + self.voting_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + } + + pub fn update_blacklist( + &self, + router: &mut App, + append_addrs: Option>, + remove_addrs: Option>, + ) -> Result { + router.execute_contract( + Addr::unchecked("owner"), + self.voting_instance.clone(), + &ExecuteMsg::UpdateBlacklist { + append_addrs, + remove_addrs, + }, + &[], + ) + } + + #[allow(dead_code)] + pub fn update_outpost_address( + &self, + router: &mut App, + new_address: String, + ) -> Result { + router.execute_contract( + Addr::unchecked("owner"), + self.voting_instance.clone(), + &ExecuteMsg::UpdateConfig { + new_guardian: None, + generator_controller: None, + outpost: Some(new_address), + }, + &[], + ) + } + + #[allow(dead_code)] + pub fn query_user_vp(&self, router: &mut App, user: &str) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::UserVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_user_emissions_vp(&self, router: &mut App, user: &str) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::UserEmissionsVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_exact_user_vp(&self, router: &mut App, user: &str) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::UserVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128()) + } + + #[allow(dead_code)] + pub fn query_exact_user_emissions_vp(&self, router: &mut App, user: &str) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::UserEmissionsVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128()) + } + + #[allow(dead_code)] + pub fn query_user_vp_at(&self, router: &mut App, user: &str, time: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::UserVotingPowerAt { + user: user.to_string(), + time, + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_user_emissions_vp_at( + &self, + router: &mut App, + user: &str, + time: u64, + ) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::UserEmissionsVotingPowerAt { + user: user.to_string(), + time, + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_user_vp_at_period( + &self, + router: &mut App, + user: &str, + period: u64, + ) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::UserVotingPowerAtPeriod { + user: user.to_string(), + period, + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_total_vp(&self, router: &mut App) -> StdResult { + router + .wrap() + .query_wasm_smart(self.voting_instance.clone(), &QueryMsg::TotalVotingPower {}) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_total_emissions_vp(&self, router: &mut App) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::TotalEmissionsVotingPower {}, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_exact_total_vp(&self, router: &mut App) -> StdResult { + router + .wrap() + .query_wasm_smart(self.voting_instance.clone(), &QueryMsg::TotalVotingPower {}) + .map(|vp: VotingPowerResponse| vp.voting_power.u128()) + } + + #[allow(dead_code)] + pub fn query_exact_total_emissions_vp(&self, router: &mut App) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::TotalEmissionsVotingPower {}, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128()) + } + + #[allow(dead_code)] + pub fn query_total_vp_at(&self, router: &mut App, time: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::TotalVotingPowerAt { time }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_emissions_vp_at(&self, router: &mut App, time: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::TotalEmissionsVotingPowerAt { time }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_total_vp_at_period(&self, router: &mut App, period: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::TotalVotingPowerAtPeriod { period }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_total_emissions_vp_at_period( + &self, + router: &mut App, + timestamp: u64, + ) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::TotalEmissionsVotingPowerAt { time: timestamp }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_locked_balance_at( + &self, + router: &mut App, + user: &str, + timestamp: Uint64, + ) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::UserDepositAt { + user: user.to_string(), + timestamp, + }, + ) + .map(|vp: Uint128| vp.u128() as f32 / MULTIPLIER as f32) + } + + #[allow(dead_code)] + pub fn query_blacklisted_voters( + &self, + router: &mut App, + start_after: Option, + limit: Option, + ) -> StdResult> { + router.wrap().query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::BlacklistedVoters { start_after, limit }, + ) + } + + #[allow(dead_code)] + pub fn check_voters_are_blacklisted( + &self, + router: &mut App, + voters: Vec, + ) -> StdResult { + router.wrap().query_wasm_smart( + self.voting_instance.clone(), + &QueryMsg::CheckVotersAreBlacklisted { voters }, + ) + } +} + +pub fn mock_app() -> App { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(EPOCH_START); + let api = MockApi::default(); + let bank = BankKeeper::new(); + let storage = MockStorage::new(); + + AppBuilder::new() + .with_api(api) + .with_block(env.block) + .with_bank(bank) + .with_storage(storage) + .build(|_, _, _| {}) +} diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 29427527..00b6a961 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-governance" -version = "1.2.0" +version = "1.3.0" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" @@ -16,7 +16,8 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cw20 = "0.15" -cosmwasm-std = "1.1" +cosmwasm-std = { version = "1.1", features = ["ibc3"] } cw-storage-plus = "0.15" cosmwasm-schema = "1.1" astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +thiserror = { version = "1.0" } diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index a5764418..d2af5841 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -61,6 +61,10 @@ pub struct InstantiateMsg { pub voting_escrow_delegator_addr: Option, /// Astroport IBC controller contract pub ibc_controller: Option, + /// Generator controller contract capable of immediate proposals + pub generator_controller_addr: Option, + /// Hub contract that handles voting from Outposts + pub hub_addr: Option, /// Address of the builder unlock contract pub builder_unlock_addr: String, /// Proposal voting period @@ -91,6 +95,16 @@ pub enum ExecuteMsg { /// Vote option vote: ProposalVoteOption, }, + CastOutpostVote { + /// Proposal identifier + proposal_id: u64, + /// The voter from an Outpost + voter: String, + /// The vote option + vote: ProposalVoteOption, + /// The voting power applied to this vote + voting_power: Uint128, + }, /// Set the status of a proposal that expired EndProposal { /// Proposal identifier @@ -108,6 +122,16 @@ pub enum ExecuteMsg { /// Proposal identifier proposal_id: u64, }, + /// Load and execute a special emissions proposal. This proposal is passed + /// immediately and is not subject to voting as it is coming from the + /// generator controller based on emission votes. + ExecuteEmissionsProposal { + title: String, + description: String, + messages: Vec, + /// If proposal should be executed on a remote chain this field should specify governance channel + ibc_channel: Option, + }, /// Remove a proposal that was already executed (or failed/expired) RemoveCompletedProposal { /// Proposal identifier @@ -192,6 +216,10 @@ pub struct Config { pub voting_escrow_delegator_addr: Option, /// Astroport IBC controller contract pub ibc_controller: Option, + /// Generator controller contract capable of immediate proposals + pub generator_controller: Option, + /// Hub contract that handles voting from Outposts + pub hub: Option, /// Builder unlock contract address pub builder_unlock_addr: Addr, /// Proposal voting period @@ -286,6 +314,10 @@ pub struct UpdateConfig { pub voting_escrow_delegator_addr: Option, /// Astroport IBC controller contract pub ibc_controller: Option, + /// Generator controller contract capable of immediate proposals + pub generator_controller: Option, + /// Hub contract that handles voting from Outposts + pub hub: Option, /// Builder unlock contract address pub builder_unlock_addr: Option, /// Proposal voting period @@ -320,9 +352,9 @@ pub struct Proposal { /// `Against` power of proposal pub against_power: Uint128, /// `For` votes for the proposal - pub for_voters: Vec, + pub for_voters: Vec, /// `Against` votes for the proposal - pub against_voters: Vec, + pub against_voters: Vec, /// Start block of proposal pub start_block: u64, /// Start time of proposal diff --git a/packages/astroport-governance/src/generator_controller_lite.rs b/packages/astroport-governance/src/generator_controller_lite.rs new file mode 100644 index 00000000..00fbbd28 --- /dev/null +++ b/packages/astroport-governance/src/generator_controller_lite.rs @@ -0,0 +1,184 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Decimal, Uint128}; + +/// The maximum amount of voters that can be kicked at once from +pub const VOTERS_MAX_LIMIT: u32 = 30; + +/// This structure describes the basic settings for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Contract owner + pub owner: String, + /// The vxASTRO token contract address + pub escrow_addr: String, + /// Generator contract address + pub generator_addr: String, + /// Factory contract address + pub factory_addr: String, + /// Assembly contract address + pub assembly_addr: String, + /// Hub contract address + pub hub_addr: Option, + /// Max number of pools that can receive ASTRO emissions at the same time + pub pools_limit: u64, + /// The list of pools which are eligible to receive votes + pub whitelisted_pools: Vec, +} + +/// This structure describes the execute messages available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Removes all votes applied by blacklisted voters + KickBlacklistedVoters { blacklisted_voters: Vec }, + /// Removes all votes applied by voters that have unlocked + KickUnlockedVoters { unlocked_voters: Vec }, + /// Removes all votes applied by a voter that have unlocked on an Outpost + KickUnlockedOutpostVoter { unlocked_voter: String }, + /// Vote allows a vxASTRO holder to cast votes on which generators should get ASTRO emissions in the next epoch + Vote { votes: Vec<(String, u16)> }, + /// OutpostVote allows a vxASTRO holder on an Outpost to cast votes on which generators should get ASTRO emissions in the next epoch + OutpostVote { + voter: String, + voting_power: Uint128, + votes: Vec<(String, u16)>, + }, + /// TunePools transforms the latest vote distribution into alloc_points which are then applied to ASTRO generators + TunePools {}, + UpdateConfig { + // Assembly contract address + assembly_addr: Option, + /// The number of voters that can be kicked at once from the pool.. + kick_voters_limit: Option, + /// Main pool that will receive a minimum amount of ASTRO emissions + main_pool: Option, + /// The minimum percentage of ASTRO emissions that main pool should get every block + main_pool_min_alloc: Option, + /// Should the main pool be removed or not? If the variable is omitted then the pool will be kept. + remove_main_pool: Option, + // Hub contract address + hub_addr: Option, + }, + /// ChangePoolsLimit changes the max amount of pools that can be voted at once to receive ASTRO emissions + ChangePoolsLimit { limit: u64 }, + /// ProposeNewOwner proposes a new owner for the contract + ProposeNewOwner { + /// Newly proposed contract owner + new_owner: String, + /// The timestamp when the contract ownership change expires + expires_in: u64, + }, + /// DropOwnershipProposal removes the latest contract ownership transfer proposal + DropOwnershipProposal {}, + /// ClaimOwnership allows the newly proposed owner to claim contract ownership + ClaimOwnership {}, + /// Adds or removes the pools which are eligible to receive votes + UpdateWhitelist { + add: Option>, + remove: Option>, + }, + // Update network config for IBC + UpdateNetworks { + // Adding requires a list of (network, address prefix, IBC governance channel) + add: Option>, + remove: Option>, + }, +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// UserInfo returns information about a voter and the generators they voted for + #[returns(UserInfoResponse)] + UserInfo { user: String }, + /// TuneInfo returns information about the latest generators that were voted to receive ASTRO emissions + #[returns(GaugeInfoResponse)] + TuneInfo {}, + /// Config returns the contract configuration + #[returns(ConfigResponse)] + Config {}, + /// PoolInfo returns the latest voting power allocated to a specific pool (generator) + #[returns(VotedPoolInfoResponse)] + PoolInfo { pool_addr: String }, + /// PoolInfo returns the voting power allocated to a specific pool (generator) at a specific period + #[returns(VotedPoolInfoResponse)] + PoolInfoAtPeriod { pool_addr: String, period: u64 }, +} + +/// This structure describes a migration message. +/// We currently take no arguments for migrations. +#[cw_serde] +pub struct MigrateMsg {} + +/// This structure describes the parameters returned when querying for the contract configuration. +#[cw_serde] +pub struct ConfigResponse { + /// Address that's allowed to change contract parameters + pub owner: Addr, + /// The vxASTRO token contract address + pub escrow_addr: Addr, + /// Generator contract address + pub generator_addr: Addr, + /// Factory contract address + pub factory_addr: Addr, + /// Assembly contract address + pub assembly_addr: Addr, + /// Hub contract address + pub hub_addr: Option, + /// Max number of pools that can receive ASTRO emissions at the same time + pub pools_limit: u64, + /// Max number of voters which can be kicked at a time + pub kick_voters_limit: Option, + /// Main pool that will receive a minimum amount of ASTRO emissions + pub main_pool: Option, + /// The minimum percentage of ASTRO emissions that main pool should get every block + pub main_pool_min_alloc: Decimal, + /// The list of pools which are eligible to receive votes + pub whitelisted_pools: Vec, + /// The list of pools which are eligible to receive votes + pub whitelisted_networks: Vec, +} + +/// This structure describes the response used to return voting information for a specific pool (generator). +#[cw_serde] +#[derive(Default)] +pub struct VotedPoolInfoResponse { + /// vxASTRO amount that voted for this pool/generator + pub vxastro_amount: Uint128, + /// The slope at which the amount of vxASTRO that voted for this pool/generator will decay + pub slope: Uint128, +} + +/// This structure describes the response used to return tuning parameters for all pools/generators. +#[cw_serde] +#[derive(Default)] +pub struct GaugeInfoResponse { + /// Last period when a tuning was applied + pub tune_period: u64, + /// Distribution of alloc_points to apply in the Generator contract + pub pool_alloc_points: Vec<(String, Uint128)>, +} + +/// The struct describes a response used to return a staker's vxASTRO lock position. +#[cw_serde] +#[derive(Default)] +pub struct UserInfoResponse { + /// The period when the user voted last time, None if they've never voted + pub vote_period: Option, + /// The user's vxASTRO voting power + pub voting_power: Uint128, + /// The vote distribution for all the generators/pools the staker picked + pub votes: Vec<(String, u16)>, +} + +#[cw_serde] +#[derive(Eq, Hash)] +pub struct NetworkInfo { + /// The address prefix for the network, e.g. "terra". This is determined + /// by the contract and will be overwritten in update_networks + pub address_prefix: String, + /// The address of the generator contract on the Outpost + pub generator_address: Addr, + /// The IBC channel used for governance + pub ibc_channel: Option, +} diff --git a/packages/astroport-governance/src/lib.rs b/packages/astroport-governance/src/lib.rs index 2f3beebf..655f6f2c 100644 --- a/packages/astroport-governance/src/lib.rs +++ b/packages/astroport-governance/src/lib.rs @@ -2,10 +2,13 @@ pub mod assembly; pub mod builder_unlock; pub mod escrow_fee_distributor; pub mod generator_controller; +pub mod generator_controller_lite; pub mod nft; +pub mod outpost; pub mod utils; pub mod voting_escrow; pub mod voting_escrow_delegation; +pub mod voting_escrow_lite; pub use astroport; diff --git a/packages/astroport-governance/src/outpost.rs b/packages/astroport-governance/src/outpost.rs new file mode 100644 index 00000000..134d68f5 --- /dev/null +++ b/packages/astroport-governance/src/outpost.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; + +/// Describes the execute messages available in the contract +#[cw_serde] +pub enum ExecuteMsg { + /// Kick an unlocked voter's voting power from the Generator Controller lite + KickUnlocked { + /// The address of the user to kick + user: Addr, + }, +} diff --git a/packages/astroport-governance/src/utils.rs b/packages/astroport-governance/src/utils.rs index 66960354..586fd050 100644 --- a/packages/astroport-governance/src/utils.rs +++ b/packages/astroport-governance/src/utils.rs @@ -1,10 +1,18 @@ use std::convert::TryInto; -use cosmwasm_std::{Decimal, Fraction, OverflowError, StdError, StdResult, Uint128, Uint256}; +use cosmwasm_std::{ + Addr, Decimal, Fraction, IbcQuery, ListChannelsResponse, OverflowError, QuerierWrapper, + QueryRequest, StdError, StdResult, Uint128, Uint256, +}; /// Seconds in one week. It is intended for period number calculation. pub const WEEK: u64 = 7 * 86400; // lock period is rounded down by week +/// Default unlock period for a vxASTRO lite lock +pub const DEFAULT_UNLOCK_PERIOD: u64 = 2 * WEEK; + +pub const LITE_VOTING_PERIOD: u64 = 2 * WEEK; + /// Seconds in 2 years which is the maximum lock period. pub const MAX_LOCK_TIME: u64 = 2 * 365 * 86400; // 2 years (104 weeks) @@ -26,11 +34,25 @@ pub fn get_period(time: u64) -> StdResult { } } +/// Calculates the voting period number for vxASTRO lite. Time should be formatted as a timestamp. +pub fn get_lite_period(time: u64) -> StdResult { + if time < EPOCH_START { + Err(StdError::generic_err("Invalid time")) + } else { + Ok((time - EPOCH_START) / LITE_VOTING_PERIOD) + } +} + /// Calculates how many periods are in the specified time interval. The time should be in seconds. pub fn get_periods_count(interval: u64) -> u64 { interval / WEEK } +/// Calculates how many periods are in the specified time interval for vxASTRO lite. The time should be in seconds. +pub fn get_lite_periods_count(interval: u64) -> u64 { + interval / LITE_VOTING_PERIOD +} + /// This trait was implemented to eliminate Decimal rounding problems. trait DecimalRoundedCheckedMul { fn checked_mul(self, other: Uint128) -> Result; @@ -105,3 +127,27 @@ pub fn calc_voting_power( .unwrap_or_else(|_| Uint128::zero()); old_vp.saturating_sub(shift) } + +/// Checks that controller supports given IBC-channel. +/// ## Params +/// * **querier** is an object of type [`QuerierWrapper`]. +/// +/// * **ibc_controller** is an ibc controller contract address. +/// +/// * **given_channel** is an IBC channel id the function needs to check. +pub fn check_controller_supports_channel( + querier: QuerierWrapper, + ibc_controller: &Addr, + given_channel: &String, +) -> Result<(), StdError> { + let port_id = Some(format!("wasm.{ibc_controller}")); + let ListChannelsResponse { channels } = + querier.query(&QueryRequest::Ibc(IbcQuery::ListChannels { port_id }))?; + channels + .iter() + .find(|channel| &channel.endpoint.channel_id == given_channel) + .map(|_| ()) + .ok_or_else(|| StdError::GenericErr { + msg: format!("IBC controller does not have channel {0}", given_channel), + }) +} diff --git a/packages/astroport-governance/src/voting_escrow_lite.rs b/packages/astroport-governance/src/voting_escrow_lite.rs new file mode 100644 index 00000000..9d6db04e --- /dev/null +++ b/packages/astroport-governance/src/voting_escrow_lite.rs @@ -0,0 +1,340 @@ +use crate::voting_escrow_lite::QueryMsg::{ + LockInfo, TotalVotingPower, TotalVotingPowerAt, UserDepositAt, UserEmissionsVotingPower, + UserVotingPower, UserVotingPowerAt, +}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, QuerierWrapper, StdResult, Uint128, Uint64}; +use cw20::{ + BalanceResponse, Cw20ReceiveMsg, DownloadLogoResponse, Logo, MarketingInfoResponse, + TokenInfoResponse, +}; +use std::fmt; + +/// ## Pagination settings +/// The maximum amount of items that can be read at once from +pub const MAX_LIMIT: u32 = 30; + +/// The default amount of items to read from +pub const DEFAULT_LIMIT: u32 = 10; + +pub const DEFAULT_PERIODS_LIMIT: u64 = 20; + +/// This structure stores marketing information for vxASTRO. +#[cw_serde] +pub struct UpdateMarketingInfo { + /// Project URL + pub project: Option, + /// Token description + pub description: Option, + /// Token marketing information + pub marketing: Option, + /// Token logo + pub logo: Option, +} + +/// This structure stores general parameters for the vxASTRO contract. +#[cw_serde] +pub struct InstantiateMsg { + /// The vxASTRO contract owner + pub owner: String, + /// Address that's allowed to black or whitelist contracts + pub guardian_addr: Option, + /// xASTRO token address + pub deposit_token_addr: String, + /// Marketing info for vxASTRO + pub marketing: Option, + /// The list of whitelisted logo urls prefixes + pub logo_urls_whitelist: Vec, + /// Address of the Generator controller to kick unlocked users + pub generator_controller_addr: Option, + /// Address of the Outpost to handle unlock remotely + pub outpost_addr: Option, +} + +/// This structure describes the execute functions in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received + /// template. + Receive(Cw20ReceiveMsg), + /// Unlock xASTRO from the vxASTRO contract + Unlock {}, + /// Relock all xASTRO from an unlocking position if the Hub could not be notified + Relock { user: String }, + /// Withdraw xASTRO from the vxASTRO contract + Withdraw {}, + /// Propose a new owner for the contract + ProposeNewOwner { new_owner: String, expires_in: u64 }, + /// Remove the ownership transfer proposal + DropOwnershipProposal {}, + /// Claim contract ownership + ClaimOwnership {}, + /// Add or remove accounts from the blacklist + UpdateBlacklist { + append_addrs: Option>, + remove_addrs: Option>, + }, + /// Update the marketing info for the vxASTRO contract + UpdateMarketing { + /// A URL pointing to the project behind this token + project: Option, + /// A longer description of the token and its utility. Designed for tooltips or such + description: Option, + /// The address (if any) that can update this data structure + marketing: Option, + }, + /// Upload a logo for vxASTRO + UploadLogo(Logo), + /// Update config + UpdateConfig { + new_guardian: Option, + generator_controller: Option, + outpost: Option, + }, + /// Set whitelisted logo urls + SetLogoUrlsWhitelist { whitelist: Vec }, +} + +/// This structure describes a CW20 hook message. +#[cw_serde] +pub enum Cw20HookMsg { + /// Create a vxASTRO position and lock xASTRO for `time` amount of time + CreateLock { time: u64 }, + /// Deposit xASTRO in another user's vxASTRO position + DepositFor { user: String }, + /// Add more xASTRO to your vxASTRO position + ExtendLockAmount {}, +} + +/// This enum describes voters status. +#[cw_serde] +pub enum BlacklistedVotersResponse { + /// Voters are blacklisted + VotersBlacklisted {}, + /// Returns a voter that is not blacklisted. + VotersNotBlacklisted { voter: String }, +} + +impl fmt::Display for BlacklistedVotersResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + BlacklistedVotersResponse::VotersBlacklisted {} => write!(f, "Voters are blacklisted!"), + BlacklistedVotersResponse::VotersNotBlacklisted { voter } => { + write!(f, "Voter is not blacklisted: {voter}") + } + } + } +} + +/// This structure describes the query messages available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Checks if specified addresses are blacklisted + #[returns(BlacklistedVotersResponse)] + CheckVotersAreBlacklisted { voters: Vec }, + /// Return the blacklisted voters + #[returns(Vec)] + BlacklistedVoters { + start_after: Option, + limit: Option, + }, + /// Return the user's vxASTRO balance + #[returns(BalanceResponse)] + Balance { address: String }, + /// Fetch the vxASTRO token information + #[returns(TokenInfoResponse)] + TokenInfo {}, + /// Fetch vxASTRO's marketing information + #[returns(MarketingInfoResponse)] + MarketingInfo {}, + /// Download the vxASTRO logo + #[returns(DownloadLogoResponse)] + DownloadLogo {}, + /// Return the current total amount of vxASTRO + #[returns(VotingPowerResponse)] + TotalVotingPower {}, + /// Return the total amount of vxASTRO at some point in the past + #[returns(VotingPowerResponse)] + TotalVotingPowerAt { time: u64 }, + /// Return the total voting power at a specific period + #[returns(VotingPowerResponse)] + TotalVotingPowerAtPeriod { period: u64 }, + /// Return the user's current voting power (vxASTRO balance) + #[returns(VotingPowerResponse)] + UserVotingPower { user: String }, + /// Return the user's vxASTRO balance at some point in the past + #[returns(VotingPowerResponse)] + UserVotingPowerAt { user: String, time: u64 }, + /// Return the user's voting power at a specific period + #[returns(VotingPowerResponse)] + UserVotingPowerAtPeriod { user: String, period: u64 }, + + #[returns(VotingPowerResponse)] + TotalEmissionsVotingPower {}, + /// Return the total amount of vxASTRO at some point in the past + #[returns(VotingPowerResponse)] + TotalEmissionsVotingPowerAt { time: u64 }, + /// Return the user's current emission voting power + #[returns(VotingPowerResponse)] + UserEmissionsVotingPower { user: String }, + /// Return the user's emission voting power at some point in the past + #[returns(VotingPowerResponse)] + UserEmissionsVotingPowerAt { user: String, time: u64 }, + + #[returns(LockInfoResponse)] + LockInfo { user: String }, + /// Return user's locked xASTRO balance at the given timestamp + #[returns(Uint128)] + UserDepositAt { user: String, timestamp: Uint64 }, + /// Return the vxASTRO contract configuration + #[returns(Config)] + Config {}, +} + +/// This structure is used to return a user's amount of vxASTRO. +#[cw_serde] +pub struct VotingPowerResponse { + /// The vxASTRO balance + pub voting_power: Uint128, +} + +/// This structure is used to return the lock information for a vxASTRO position. +#[cw_serde] +pub struct LockInfoResponse { + /// The amount of xASTRO locked in the position + pub amount: Uint128, + /// Indicates the end of a lock period, if None the position is locked + pub end: Option, +} + +/// This structure stores the main parameters for the voting escrow contract. +#[cw_serde] +pub struct Config { + /// Address that's allowed to change contract parameters + pub owner: Addr, + /// Address that can only blacklist vxASTRO stakers and remove their governance power + pub guardian_addr: Option, + /// The xASTRO token contract address + pub deposit_token_addr: Addr, + /// The list of whitelisted logo urls prefixes + pub logo_urls_whitelist: Vec, + /// Minimum unlock wait time in seconds + pub unlock_period: u64, + /// Address of the Generator controller to kick unlocked users + pub generator_controller_addr: Option, + /// Address of the Outpost to handle unlock remotely + pub outpost_addr: Option, +} + +/// This structure describes a Migration message. +#[cw_serde] +pub struct MigrateMsg { + pub params: Binary, +} + +/// Queries current user's deposit from the voting escrow contract. +/// +/// * **user** staker for which we fetch the latest xASTRO deposits. +/// +/// * **timestamp** timestamp to fetch deposits at. +pub fn get_user_deposit_at_time( + querier: &QuerierWrapper, + escrow_addr: impl Into, + user: impl Into, + timestamp: u64, +) -> StdResult { + let balance = querier.query_wasm_smart( + escrow_addr, + &UserDepositAt { + user: user.into(), + timestamp: Uint64::from(timestamp), + }, + )?; + Ok(balance) +} + +/// Queries current user's voting power from the voting escrow contract. +/// +/// * **user** staker for which we calculate the latest vxASTRO voting power. +pub fn get_voting_power( + querier: &QuerierWrapper, + escrow_addr: impl Into, + user: impl Into, +) -> StdResult { + let vp: VotingPowerResponse = + querier.query_wasm_smart(escrow_addr, &UserVotingPower { user: user.into() })?; + Ok(vp.voting_power) +} + +/// Queries current user's emissions voting power from the voting escrow contract. +/// +/// * **user** staker for which we calculate the latest vxASTRO voting power. +pub fn get_emissions_voting_power( + querier: &QuerierWrapper, + escrow_addr: impl Into, + user: impl Into, +) -> StdResult { + let vp: VotingPowerResponse = + querier.query_wasm_smart(escrow_addr, &UserEmissionsVotingPower { user: user.into() })?; + Ok(vp.voting_power) +} + +/// Queries current user's voting power from the voting escrow contract by timestamp. +/// +/// * **user** staker for which we calculate the voting power at a specific time. +/// +/// * **timestamp** timestamp at which we calculate the staker's voting power. +pub fn get_voting_power_at( + querier: &QuerierWrapper, + escrow_addr: impl Into, + user: impl Into, + timestamp: u64, +) -> StdResult { + let vp: VotingPowerResponse = querier.query_wasm_smart( + escrow_addr, + &UserVotingPowerAt { + user: user.into(), + time: timestamp, + }, + )?; + + Ok(vp.voting_power) +} + +/// Queries current total voting power from the voting escrow contract. +pub fn get_total_voting_power( + querier: &QuerierWrapper, + escrow_addr: impl Into, +) -> StdResult { + let vp: VotingPowerResponse = querier.query_wasm_smart(escrow_addr, &TotalVotingPower {})?; + + Ok(vp.voting_power) +} + +/// Queries total voting power from the voting escrow contract by timestamp. +/// +/// * **timestamp** time at which we fetch the total voting power. +pub fn get_total_voting_power_at( + querier: &QuerierWrapper, + escrow_addr: impl Into, + timestamp: u64, +) -> StdResult { + let vp: VotingPowerResponse = + querier.query_wasm_smart(escrow_addr, &TotalVotingPowerAt { time: timestamp })?; + + Ok(vp.voting_power) +} + +/// Queries user's lockup information from the voting escrow contract. +/// +/// * **user** staker for which we return lock position information. +pub fn get_lock_info( + querier: &QuerierWrapper, + escrow_addr: impl Into, + user: impl Into, +) -> StdResult { + let lock_info: LockInfoResponse = + querier.query_wasm_smart(escrow_addr, &LockInfo { user: user.into() })?; + Ok(lock_info) +} diff --git a/packages/astroport-tests-lite/Cargo.toml b/packages/astroport-tests-lite/Cargo.toml new file mode 100644 index 00000000..c6ed825d --- /dev/null +++ b/packages/astroport-tests-lite/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "astroport-tests-lite" +version = "1.0.0" +authors = ["Astroport"] +edition = "2021" +repository = "https://github.com/astroport-fi/astroport-governance" +homepage = "https://astroport.fi" + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cw2 = "0.15" +cw20 = "0.15" +cosmwasm-std = "1.1" + +cosmwasm-schema = "1.1" +cw-multi-test = "0.16" +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } + +astroport-escrow-fee-distributor = { path = "../../contracts/escrow_fee_distributor" } +astroport-governance = { path = "../astroport-governance" } +voting-escrow-lite = { path = "../../contracts/voting_escrow_lite" } +generator-controller-lite = { path = "../../contracts/generator_controller_lite" } +astro-assembly = { path = "../../contracts/assembly" } +astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +anyhow = "1" diff --git a/packages/astroport-tests-lite/src/address_generator.rs b/packages/astroport-tests-lite/src/address_generator.rs new file mode 100644 index 00000000..1ea21b9b --- /dev/null +++ b/packages/astroport-tests-lite/src/address_generator.rs @@ -0,0 +1,19 @@ +use std::cell::Cell; + +use cosmwasm_std::{Addr, Storage}; +use cw_multi_test::AddressGenerator; + +/// Defines a custom address generator that creates simple addresses that +/// always use the format wasm1xxxxx to conform to Cosmos address formats +#[derive(Default)] +pub struct WasmAddressGenerator { + address_counter: Cell, +} + +impl AddressGenerator for WasmAddressGenerator { + fn next_address(&self, _: &mut dyn Storage) -> Addr { + let contract_number = self.address_counter.get() + 1; + self.address_counter.set(contract_number); + Addr::unchecked(format!("wasm1contract{}", contract_number)) + } +} diff --git a/packages/astroport-tests-lite/src/base.rs b/packages/astroport-tests-lite/src/base.rs new file mode 100644 index 00000000..d8381e36 --- /dev/null +++ b/packages/astroport-tests-lite/src/base.rs @@ -0,0 +1,359 @@ +use cosmwasm_schema::cw_serde; + +use astroport::staking; +use astroport::token::InstantiateMsg as AstroTokenInstantiateMsg; +use astroport_governance::escrow_fee_distributor::InstantiateMsg as EscrowFeeDistributorInstantiateMsg; +use astroport_governance::voting_escrow_lite::{ + Cw20HookMsg, ExecuteMsg, InstantiateMsg as AstroVotingEscrowInstantiateMsg, QueryMsg, + VotingPowerResponse, +}; +use cosmwasm_std::{attr, to_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; +use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; + +use anyhow::Result; +use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; + +pub const MULTIPLIER: u64 = 1_000_000; + +#[cw_serde] +pub struct ContractInfo { + pub address: Addr, + pub code_id: u64, +} + +#[cw_serde] +pub struct BaseAstroportTestPackage { + pub owner: Addr, + pub astro_token: Option, + pub escrow_fee_distributor: Option, + pub staking: Option, + pub voting_escrow: Option, +} + +#[cw_serde] +pub struct BaseAstroportTestInitMessage { + pub owner: Addr, +} + +impl BaseAstroportTestPackage { + pub fn init_all(router: &mut App, msg: BaseAstroportTestInitMessage) -> Self { + let mut base_pack = BaseAstroportTestPackage { + owner: msg.owner.clone(), + astro_token: None, + escrow_fee_distributor: None, + staking: None, + voting_escrow: None, + }; + + base_pack.init_astro_token(router, msg.owner.clone()); + base_pack.init_staking(router, msg.owner.clone()); + base_pack.init_voting_escrow(router, msg.owner.clone()); + base_pack.init_escrow_fee_distributor(router, msg.owner); + base_pack + } + + fn init_astro_token(&mut self, router: &mut App, owner: Addr) { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + let astro_token_code_id = router.store_code(astro_token_contract); + + let init_msg = AstroTokenInstantiateMsg { + name: String::from("Astro token"), + symbol: String::from("ASTRO"), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + let astro_token_instance = router + .instantiate_contract( + astro_token_code_id, + owner, + &init_msg, + &[], + "Astro token", + None, + ) + .unwrap(); + + self.astro_token = Some(ContractInfo { + address: astro_token_instance, + code_id: astro_token_code_id, + }) + } + + fn init_staking(&mut self, router: &mut App, owner: Addr) { + let staking_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_staking::contract::execute, + astroport_staking::contract::instantiate, + astroport_staking::contract::query, + ) + .with_reply_empty(astroport_staking::contract::reply), + ); + + let staking_code_id = router.store_code(staking_contract); + + let msg = staking::InstantiateMsg { + owner: owner.to_string(), + token_code_id: self.astro_token.clone().unwrap().code_id, + deposit_token_addr: self.astro_token.clone().unwrap().address.to_string(), + marketing: None, + }; + + let staking_instance = router + .instantiate_contract( + staking_code_id, + owner, + &msg, + &[], + String::from("xASTRO"), + None, + ) + .unwrap(); + + self.staking = Some(ContractInfo { + address: staking_instance, + code_id: staking_code_id, + }) + } + + pub fn get_staking_xastro(&self, router: &App) -> Addr { + let res = router + .wrap() + .query::(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: self.staking.clone().unwrap().address.to_string(), + msg: to_binary(&staking::QueryMsg::Config {}).unwrap(), + })) + .unwrap(); + + res.share_token_addr + } + + fn init_voting_escrow(&mut self, router: &mut App, owner: Addr) { + let voting_contract = Box::new(ContractWrapper::new_with_empty( + voting_escrow_lite::execute::execute, + voting_escrow_lite::contract::instantiate, + voting_escrow_lite::query::query, + )); + + let voting_code_id = router.store_code(voting_contract); + + let msg = AstroVotingEscrowInstantiateMsg { + guardian_addr: Some("guardian".to_string()), + marketing: None, + owner: owner.to_string(), + deposit_token_addr: self.get_staking_xastro(router).to_string(), + logo_urls_whitelist: vec![], + generator_controller_addr: None, + outpost_addr: None, + }; + + let voting_instance = router + .instantiate_contract( + voting_code_id, + owner, + &msg, + &[], + String::from("vxASTRO"), + None, + ) + .unwrap(); + + self.voting_escrow = Some(ContractInfo { + address: voting_instance, + code_id: voting_code_id, + }) + } + + pub fn init_escrow_fee_distributor(&mut self, router: &mut App, owner: Addr) { + let escrow_fee_distributor_contract = Box::new(ContractWrapper::new_with_empty( + astroport_escrow_fee_distributor::contract::execute, + astroport_escrow_fee_distributor::contract::instantiate, + astroport_escrow_fee_distributor::contract::query, + )); + + let escrow_fee_distributor_code_id = router.store_code(escrow_fee_distributor_contract); + + let init_msg = EscrowFeeDistributorInstantiateMsg { + owner: owner.to_string(), + astro_token: self.astro_token.clone().unwrap().address.to_string(), + voting_escrow_addr: self.voting_escrow.clone().unwrap().address.to_string(), + claim_many_limit: None, + is_claim_disabled: None, + }; + + let escrow_fee_distributor_instance = router + .instantiate_contract( + escrow_fee_distributor_code_id, + owner, + &init_msg, + &[], + "Astroport escrow fee distributor", + None, + ) + .unwrap(); + + self.escrow_fee_distributor = Some(ContractInfo { + address: escrow_fee_distributor_instance, + code_id: escrow_fee_distributor_code_id, + }) + } + + pub fn create_lock( + &self, + router: &mut App, + user: Addr, + time: u64, + amount: u64, + ) -> Result { + let amount = amount * MULTIPLIER; + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.voting_escrow.clone().unwrap().address.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + }; + + router.execute_contract(user, self.get_staking_xastro(router), &cw20msg, &[]) + } + + pub fn extend_lock_amount( + &mut self, + router: &mut App, + user: &str, + amount: u64, + ) -> Result { + let amount = amount * MULTIPLIER; + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.voting_escrow.clone().unwrap().address.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + }; + router.execute_contract( + Addr::unchecked(user), + self.get_staking_xastro(router), + &cw20msg, + &[], + ) + } + + pub fn withdraw(&self, router: &mut App, user: &str) -> Result { + router.execute_contract( + Addr::unchecked(user), + self.voting_escrow.clone().unwrap().address, + &ExecuteMsg::Withdraw {}, + &[], + ) + } + + pub fn query_user_vp(&self, router: &mut App, user: Addr) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_escrow.clone().unwrap().address, + &QueryMsg::UserVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_user_vp_at(&self, router: &mut App, user: Addr, time: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_escrow.clone().unwrap().address, + &QueryMsg::UserVotingPowerAt { + user: user.to_string(), + time, + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_vp(&self, router: &mut App) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_escrow.clone().unwrap().address, + &QueryMsg::TotalVotingPower {}, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_vp_at(&self, router: &mut App, time: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.voting_escrow.clone().unwrap().address, + &QueryMsg::TotalVotingPowerAt { time }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } +} + +pub fn mint(router: &mut App, owner: Addr, token_instance: Addr, to: &Addr, amount: u128) { + let amount = amount * MULTIPLIER as u128; + let msg = cw20::Cw20ExecuteMsg::Mint { + recipient: to.to_string(), + amount: Uint128::from(amount), + }; + + let res = router + .execute_contract(owner, token_instance, &msg, &[]) + .unwrap(); + assert_eq!(res.events[1].attributes[1], attr("action", "mint")); + assert_eq!(res.events[1].attributes[2], attr("to", String::from(to))); + assert_eq!( + res.events[1].attributes[3], + attr("amount", Uint128::from(amount)) + ); +} + +pub fn check_balance(app: &mut App, token_addr: &Addr, contract_addr: &Addr, expected: u128) { + let msg = Cw20QueryMsg::Balance { + address: contract_addr.to_string(), + }; + let res: StdResult = app.wrap().query_wasm_smart(token_addr, &msg); + assert_eq!(res.unwrap().balance, Uint128::from(expected)); +} + +pub fn increase_allowance( + router: &mut App, + owner: Addr, + spender: Addr, + token: Addr, + amount: Uint128, +) { + let msg = cw20::Cw20ExecuteMsg::IncreaseAllowance { + spender: spender.to_string(), + amount, + expires: None, + }; + + let res = router + .execute_contract(owner.clone(), token, &msg, &[]) + .unwrap(); + + assert_eq!( + res.events[1].attributes[1], + attr("action", "increase_allowance") + ); + assert_eq!( + res.events[1].attributes[2], + attr("owner", owner.to_string()) + ); + assert_eq!( + res.events[1].attributes[3], + attr("spender", spender.to_string()) + ); + assert_eq!(res.events[1].attributes[4], attr("amount", amount)); +} diff --git a/packages/astroport-tests-lite/src/controller_helper.rs b/packages/astroport-tests-lite/src/controller_helper.rs new file mode 100644 index 00000000..edfbc9ce --- /dev/null +++ b/packages/astroport-tests-lite/src/controller_helper.rs @@ -0,0 +1,493 @@ +use crate::escrow_helper::EscrowHelper; +use anyhow::Result as AnyResult; +use astroport::asset::{AssetInfo, PairInfo}; +use astroport::factory::{PairConfig, PairType}; + +use astroport_governance::assembly::{DEPOSIT_INTERVAL, VOTING_PERIOD_INTERVAL}; +use astroport_governance::generator_controller_lite::{ + ConfigResponse, ExecuteMsg, NetworkInfo, QueryMsg, +}; +use cosmwasm_std::{Addr, Decimal, StdResult, Uint128}; +use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; +use generator_controller_lite::state::{UserInfo, VotedPoolInfo}; + +const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); +const PROPOSAL_EFFECTIVE_DELAY: u64 = 12_342; +const PROPOSAL_EXPIRATION_PERIOD: u64 = 86_399; +const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); +const PROPOSAL_REQUIRED_QUORUM: &str = "0.50"; +const PROPOSAL_REQUIRED_THRESHOLD: &str = "0.60"; + +pub struct ControllerHelper { + pub owner: String, + pub generator: Addr, + pub controller: Addr, + pub factory: Addr, + pub escrow_helper: EscrowHelper, +} + +impl ControllerHelper { + pub fn init(router: &mut App, owner: &Addr, hub_addr: Option) -> Self { + let escrow_helper = EscrowHelper::init(router, owner.clone()); + + let pair_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_pair::contract::execute, + astroport_pair::contract::instantiate, + astroport_pair::contract::query, + ) + .with_reply_empty(astroport_pair::contract::reply), + ); + + let pair_code_id = router.store_code(pair_contract); + + let factory_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_factory::contract::execute, + astroport_factory::contract::instantiate, + astroport_factory::contract::query, + ) + .with_reply_empty(astroport_factory::contract::reply), + ); + + let factory_code_id = router.store_code(factory_contract); + + let whitelist_code_id = store_whitelist_code(router); + + let msg = astroport::factory::InstantiateMsg { + pair_configs: vec![PairConfig { + code_id: pair_code_id, + pair_type: PairType::Xyk {}, + total_fee_bps: 100, + maker_fee_bps: 10, + is_disabled: false, + is_generator_disabled: false, + }], + token_code_id: escrow_helper.astro_token_code_id, + fee_address: None, + generator_address: None, + owner: owner.to_string(), + whitelist_code_id, + coin_registry_address: Addr::unchecked("coin_registry").to_string(), + }; + + let factory = router + .instantiate_contract(factory_code_id, owner.clone(), &msg, &[], "Factory", None) + .unwrap(); + + let generator_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_generator::contract::execute, + astroport_generator::contract::instantiate, + astroport_generator::contract::query, + ) + .with_reply_empty(astroport_generator::contract::reply), + ); + + let generator_code_id = router.store_code(generator_contract); + let init_msg = astroport::generator::InstantiateMsg { + owner: owner.to_string(), + factory: factory.to_string(), + generator_controller: None, + guardian: None, + astro_token: AssetInfo::NativeToken { + denom: escrow_helper.astro_token.to_string(), + }, + tokens_per_block: Default::default(), + start_block: Default::default(), + vesting_contract: "vesting_placeholder".to_string(), + whitelist_code_id, + voting_escrow: None, + voting_escrow_delegation: None, + }; + + let generator = router + .instantiate_contract( + generator_code_id, + owner.clone(), + &init_msg, + &[], + String::from("Generator"), + None, + ) + .unwrap(); + + let assembly_contract = Box::new(ContractWrapper::new_with_empty( + astro_assembly::contract::execute, + astro_assembly::contract::instantiate, + astro_assembly::contract::query, + )); + + let assembly_code = router.store_code(assembly_contract); + + let assembly_default_instantiate_msg = astroport_governance::assembly::InstantiateMsg { + xastro_token_addr: escrow_helper.xastro_token.to_string(), + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller_addr: None, + hub_addr: None, + builder_unlock_addr: "nocontract".to_string(), + proposal_voting_period: PROPOSAL_VOTING_PERIOD, + proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, + proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, + proposal_required_deposit: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), + proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), + proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), + whitelisted_links: vec!["https://some.link/".to_string()], + }; + + let assembly_instance = router + .instantiate_contract( + assembly_code, + owner.clone(), + &assembly_default_instantiate_msg, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap(); + + let controller_contract = Box::new(ContractWrapper::new_with_empty( + generator_controller_lite::contract::execute, + generator_controller_lite::contract::instantiate, + generator_controller_lite::contract::query, + )); + + let controller_code_id = router.store_code(controller_contract); + let init_msg = astroport_governance::generator_controller_lite::InstantiateMsg { + owner: owner.to_string(), + escrow_addr: escrow_helper.escrow_instance.to_string(), + generator_addr: generator.to_string(), + factory_addr: factory.to_string(), + pools_limit: 5, + whitelisted_pools: vec![], + assembly_addr: assembly_instance.to_string(), + hub_addr, + }; + + let controller = router + .instantiate_contract( + controller_code_id, + owner.clone(), + &init_msg, + &[], + String::from("Controller"), + None, + ) + .unwrap(); + + // Update the vxASTRO instance to include the controller + router + .execute_contract( + owner.clone(), + escrow_helper.escrow_instance.clone(), + &astroport_governance::voting_escrow_lite::ExecuteMsg::UpdateConfig { + new_guardian: None, + generator_controller: Some(controller.to_string()), + outpost: None, + }, + &[], + ) + .unwrap(); + + // Setup controller in generator contract + router + .execute_contract( + owner.clone(), + generator.clone(), + &astroport::generator::ExecuteMsg::UpdateConfig { + vesting_contract: None, + generator_controller: Some(controller.to_string()), + guardian: None, + checkpoint_generator_limit: None, + voting_escrow: None, + voting_escrow_delegation: None, + }, + &[], + ) + .unwrap(); + + Self { + owner: owner.to_string(), + generator, + controller, + factory, + escrow_helper, + } + } + + pub fn init_cw20_token(&self, router: &mut App, name: &str) -> AnyResult { + let msg = astroport::token::InstantiateMsg { + name: name.to_string(), + symbol: name.to_string(), + decimals: 6, + initial_balances: vec![], + mint: None, + marketing: None, + }; + + router.instantiate_contract( + self.escrow_helper.astro_token_code_id, + Addr::unchecked(self.owner.clone()), + &msg, + &[], + name.to_string(), + None, + ) + } + + pub fn create_pool(&self, router: &mut App, token1: &Addr, token2: &Addr) -> AnyResult { + let asset_infos = vec![ + AssetInfo::Token { + contract_addr: token1.clone(), + }, + AssetInfo::Token { + contract_addr: token2.clone(), + }, + ]; + + router.execute_contract( + Addr::unchecked(self.owner.clone()), + self.factory.clone(), + &astroport::factory::ExecuteMsg::CreatePair { + pair_type: PairType::Xyk {}, + asset_infos: asset_infos.to_vec(), + init_params: None, + }, + &[], + )?; + + let res: PairInfo = router.wrap().query_wasm_smart( + self.factory.clone(), + &astroport::factory::QueryMsg::Pair { + asset_infos: asset_infos.to_vec(), + }, + )?; + + Ok(res.liquidity_token) + } + + pub fn create_pool_with_tokens( + &self, + router: &mut App, + name1: &str, + name2: &str, + ) -> AnyResult { + let token1 = self.init_cw20_token(router, name1).unwrap(); + let token2 = self.init_cw20_token(router, name2).unwrap(); + + self.create_pool(router, &token1, &token2) + } + + pub fn vote( + &self, + router: &mut App, + user: &str, + votes: Vec<(impl Into, u16)>, + ) -> AnyResult { + let msg = ExecuteMsg::Vote { + votes: votes + .into_iter() + .map(|(pool, apoints)| (pool.into(), apoints)) + .collect(), + }; + + router.execute_contract(Addr::unchecked(user), self.controller.clone(), &msg, &[]) + } + + pub fn outpost_vote( + &self, + router: &mut App, + sender: &str, + voter: String, + voting_power: Uint128, + votes: Vec<(impl Into, u16)>, + ) -> AnyResult { + let msg = ExecuteMsg::OutpostVote { + voter, + voting_power, + votes: votes + .into_iter() + .map(|(pool, apoints)| (pool.into(), apoints)) + .collect(), + }; + + router.execute_contract(Addr::unchecked(sender), self.controller.clone(), &msg, &[]) + } + + pub fn tune(&self, router: &mut App) -> AnyResult { + router.execute_contract( + Addr::unchecked("anyone"), + self.controller.clone(), + &ExecuteMsg::TunePools {}, + &[], + ) + } + + pub fn kick_holders( + &self, + router: &mut App, + user: &str, + blacklisted_voters: Vec, + ) -> AnyResult { + router.execute_contract( + Addr::unchecked(user), + self.controller.clone(), + &ExecuteMsg::KickBlacklistedVoters { blacklisted_voters }, + &[], + ) + } + + pub fn kick_unlocked_holders( + &self, + router: &mut App, + user: &str, + unlocked_voters: Vec, + ) -> AnyResult { + router.execute_contract( + Addr::unchecked(user), + self.controller.clone(), + &ExecuteMsg::KickUnlockedVoters { unlocked_voters }, + &[], + ) + } + + pub fn kick_unlocked_outpost_holders( + &self, + router: &mut App, + user: &str, + unlocked_voter: String, + ) -> AnyResult { + router.execute_contract( + Addr::unchecked(user), + self.controller.clone(), + &ExecuteMsg::KickUnlockedOutpostVoter { unlocked_voter }, + &[], + ) + } + + pub fn update_blacklisted_limit( + &self, + router: &mut App, + user: &str, + kick_voters_limit: Option, + ) -> AnyResult { + router.execute_contract( + Addr::unchecked(user), + self.controller.clone(), + &ExecuteMsg::UpdateConfig { + kick_voters_limit, + main_pool: None, + main_pool_min_alloc: None, + remove_main_pool: None, + assembly_addr: None, + hub_addr: None, + }, + &[], + ) + } + + pub fn update_main_pool( + &self, + router: &mut App, + user: &str, + main_pool: Option<&Addr>, + main_pool_min_alloc: Option, + remove_main_pool: bool, + ) -> AnyResult { + let remove_main_pool = if remove_main_pool { Some(true) } else { None }; + router.execute_contract( + Addr::unchecked(user), + self.controller.clone(), + &ExecuteMsg::UpdateConfig { + kick_voters_limit: None, + main_pool: main_pool.map(|p| p.to_string()), + main_pool_min_alloc, + remove_main_pool, + assembly_addr: None, + hub_addr: None, + }, + &[], + ) + } + + pub fn update_whitelist( + &self, + router: &mut App, + user: &str, + add_pools: Option>, + remove_pools: Option>, + ) -> AnyResult { + let msg = ExecuteMsg::UpdateWhitelist { + add: add_pools, + remove: remove_pools, + }; + + router.execute_contract(Addr::unchecked(user), self.controller.clone(), &msg, &[]) + } + + pub fn update_networks( + &self, + router: &mut App, + user: &str, + add_networks: Option>, + remove_networks: Option>, + ) -> AnyResult { + let msg = ExecuteMsg::UpdateNetworks { + add: add_networks, + remove: remove_networks, + }; + + router.execute_contract(Addr::unchecked(user), self.controller.clone(), &msg, &[]) + } + + pub fn query_user_info(&self, router: &mut App, user: &str) -> StdResult { + router.wrap().query_wasm_smart( + self.controller.clone(), + &QueryMsg::UserInfo { + user: user.to_string(), + }, + ) + } + + pub fn query_voted_pool_info(&self, router: &mut App, pool: &str) -> StdResult { + router.wrap().query_wasm_smart( + self.controller.clone(), + &QueryMsg::PoolInfo { + pool_addr: pool.to_string(), + }, + ) + } + + pub fn query_voted_pool_info_at_period( + &self, + router: &mut App, + pool: &str, + period: u64, + ) -> StdResult { + router.wrap().query_wasm_smart( + self.controller.clone(), + &QueryMsg::PoolInfoAtPeriod { + pool_addr: pool.to_string(), + period, + }, + ) + } + + pub fn query_config(&self, router: &mut App) -> StdResult { + router + .wrap() + .query_wasm_smart(self.controller.clone(), &QueryMsg::Config {}) + } +} + +fn store_whitelist_code(app: &mut App) -> u64 { + let whitelist_contract = Box::new(ContractWrapper::new_with_empty( + astroport_whitelist::contract::execute, + astroport_whitelist::contract::instantiate, + astroport_whitelist::contract::query, + )); + + app.store_code(whitelist_contract) +} diff --git a/packages/astroport-tests-lite/src/escrow_helper.rs b/packages/astroport-tests-lite/src/escrow_helper.rs new file mode 100644 index 00000000..f1da02e8 --- /dev/null +++ b/packages/astroport-tests-lite/src/escrow_helper.rs @@ -0,0 +1,397 @@ +use anyhow::Result; +use astroport::{staking as xastro, token as astro}; +use astroport_governance::voting_escrow_lite::{ + Cw20HookMsg, ExecuteMsg, InstantiateMsg, LockInfoResponse, QueryMsg, VotingPowerResponse, +}; +use cosmwasm_std::{attr, to_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; +use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; +use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; + +pub const MULTIPLIER: u64 = 1000000; + +pub struct EscrowHelper { + pub owner: Addr, + pub astro_token: Addr, + pub staking_instance: Addr, + pub xastro_token: Addr, + pub escrow_instance: Addr, + pub astro_token_code_id: u64, +} + +impl EscrowHelper { + pub fn init(router: &mut App, owner: Addr) -> Self { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + let astro_token_code_id = router.store_code(astro_token_contract); + + let msg = astro::InstantiateMsg { + name: String::from("Astro token"), + symbol: String::from("ASTRO"), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: owner.to_string(), + cap: None, + }), + marketing: None, + }; + + let astro_token = router + .instantiate_contract( + astro_token_code_id, + owner.clone(), + &msg, + &[], + String::from("ASTRO"), + None, + ) + .unwrap(); + + let staking_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_staking::contract::execute, + astroport_staking::contract::instantiate, + astroport_staking::contract::query, + ) + .with_reply_empty(astroport_staking::contract::reply), + ); + + let staking_code_id = router.store_code(staking_contract); + + let msg = xastro::InstantiateMsg { + owner: owner.to_string(), + token_code_id: astro_token_code_id, + deposit_token_addr: astro_token.to_string(), + marketing: None, + }; + let staking_instance = router + .instantiate_contract( + staking_code_id, + owner.clone(), + &msg, + &[], + String::from("xASTRO"), + None, + ) + .unwrap(); + + let res = router + .wrap() + .query::(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: staking_instance.to_string(), + msg: to_binary(&xastro::QueryMsg::Config {}).unwrap(), + })) + .unwrap(); + + let voting_contract = Box::new(ContractWrapper::new_with_empty( + voting_escrow_lite::execute::execute, + voting_escrow_lite::contract::instantiate, + voting_escrow_lite::query::query, + )); + + let voting_code_id = router.store_code(voting_contract); + + let msg = InstantiateMsg { + owner: owner.to_string(), + guardian_addr: Some("guardian".to_string()), + deposit_token_addr: res.share_token_addr.to_string(), + marketing: None, + logo_urls_whitelist: vec![], + generator_controller_addr: None, + outpost_addr: None, + }; + let voting_instance = router + .instantiate_contract( + voting_code_id, + owner.clone(), + &msg, + &[], + String::from("vxASTRO"), + None, + ) + .unwrap(); + + Self { + owner, + xastro_token: res.share_token_addr, + astro_token, + staking_instance, + escrow_instance: voting_instance, + astro_token_code_id, + } + } + + pub fn mint_xastro(&self, router: &mut App, to: &str, amount: u64) { + let amount = amount * MULTIPLIER; + let msg = Cw20ExecuteMsg::Mint { + recipient: String::from(to), + amount: Uint128::from(amount), + }; + let res = router + .execute_contract(self.owner.clone(), self.astro_token.clone(), &msg, &[]) + .unwrap(); + assert_eq!(res.events[1].attributes[1], attr("action", "mint")); + assert_eq!(res.events[1].attributes[2], attr("to", String::from(to))); + assert_eq!( + res.events[1].attributes[3], + attr("amount", Uint128::from(amount)) + ); + + let to_addr = Addr::unchecked(to); + let msg = Cw20ExecuteMsg::Send { + contract: self.staking_instance.to_string(), + msg: to_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), + amount: Uint128::from(amount), + }; + router + .execute_contract(to_addr, self.astro_token.clone(), &msg, &[]) + .unwrap(); + } + + pub fn check_xastro_balance(&self, router: &mut App, user: &str, amount: u64) { + let amount = amount * MULTIPLIER; + let res: BalanceResponse = router + .wrap() + .query_wasm_smart( + self.xastro_token.clone(), + &Cw20QueryMsg::Balance { + address: user.to_string(), + }, + ) + .unwrap(); + assert_eq!(res.balance.u128(), amount as u128); + } + + pub fn create_lock( + &self, + router: &mut App, + user: &str, + time: u64, + amount: f32, + ) -> Result { + let amount = (amount * MULTIPLIER as f32) as u64; + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.escrow_instance.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + }; + router.execute_contract( + Addr::unchecked(user), + self.xastro_token.clone(), + &cw20msg, + &[], + ) + } + + pub fn extend_lock_amount( + &self, + router: &mut App, + user: &str, + amount: f32, + ) -> Result { + let amount = (amount * MULTIPLIER as f32) as u64; + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.escrow_instance.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + }; + router.execute_contract( + Addr::unchecked(user), + self.xastro_token.clone(), + &cw20msg, + &[], + ) + } + + pub fn unlock(&self, router: &mut App, user: &str) -> Result { + router.execute_contract( + Addr::unchecked(user), + self.escrow_instance.clone(), + &ExecuteMsg::Unlock {}, + &[], + ) + } + + pub fn deposit_for( + &self, + router: &mut App, + from: &str, + to: &str, + amount: f32, + ) -> Result { + let amount = (amount * MULTIPLIER as f32) as u64; + let cw20msg = Cw20ExecuteMsg::Send { + contract: self.escrow_instance.to_string(), + amount: Uint128::from(amount), + msg: to_binary(&Cw20HookMsg::DepositFor { + user: to.to_string(), + }) + .unwrap(), + }; + router.execute_contract( + Addr::unchecked(from), + self.xastro_token.clone(), + &cw20msg, + &[], + ) + } + + pub fn withdraw(&self, router: &mut App, user: &str) -> Result { + router.execute_contract( + Addr::unchecked(user), + self.escrow_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + } + + pub fn update_blacklist( + &self, + router: &mut App, + append_addrs: Option>, + remove_addrs: Option>, + ) -> Result { + router.execute_contract( + Addr::unchecked("owner"), + self.escrow_instance.clone(), + &ExecuteMsg::UpdateBlacklist { + append_addrs, + remove_addrs, + }, + &[], + ) + } + + pub fn query_user_vp(&self, router: &mut App, user: &str) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::UserVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_user_vp_at(&self, router: &mut App, user: &str, time: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::UserVotingPowerAt { + user: user.to_string(), + time, + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_user_vp_at_period( + &self, + router: &mut App, + user: &str, + period: u64, + ) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::UserVotingPowerAtPeriod { + user: user.to_string(), + period, + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_vp(&self, router: &mut App) -> StdResult { + router + .wrap() + .query_wasm_smart(self.escrow_instance.clone(), &QueryMsg::TotalVotingPower {}) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_vp_at(&self, router: &mut App, time: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::TotalVotingPowerAt { time }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_vp_at_period(&self, router: &mut App, period: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::TotalVotingPowerAtPeriod { period }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_user_emissions_vp(&self, router: &mut App, user: &str) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::UserEmissionsVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_user_emissions_vp_at( + &self, + router: &mut App, + user: &str, + time: u64, + ) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::UserEmissionsVotingPowerAt { + user: user.to_string(), + time, + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_emissions_vp(&self, router: &mut App) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::TotalEmissionsVotingPower {}, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_emissions_vp_at(&self, router: &mut App, time: u64) -> StdResult { + router + .wrap() + .query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::TotalEmissionsVotingPowerAt { time }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_lock_info(&self, router: &mut App, user: &str) -> StdResult { + router.wrap().query_wasm_smart( + self.escrow_instance.clone(), + &QueryMsg::LockInfo { + user: user.to_string(), + }, + ) + } +} diff --git a/packages/astroport-tests-lite/src/lib.rs b/packages/astroport-tests-lite/src/lib.rs new file mode 100644 index 00000000..abd8f49c --- /dev/null +++ b/packages/astroport-tests-lite/src/lib.rs @@ -0,0 +1,54 @@ +#![cfg(not(tarpaulin_include))] + +pub mod address_generator; +pub mod base; + +use address_generator::WasmAddressGenerator; +use astroport_governance::utils::{get_lite_period, EPOCH_START}; +use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; +use cosmwasm_std::{Empty, Timestamp}; +use cw_multi_test::{App, BankKeeper, BasicAppBuilder, FailingModule, WasmKeeper}; + +#[allow(clippy::all)] +#[allow(dead_code)] +pub mod controller_helper; + +#[allow(clippy::all)] +#[allow(dead_code)] +pub mod escrow_helper; + +pub fn mock_app() -> App { + let mut env = mock_env(); + env.block.time = Timestamp::from_seconds(EPOCH_START); + let api = MockApi::default(); + let bank = BankKeeper::new(); + let storage = MockStorage::new(); + + BasicAppBuilder::new() + .with_api(api) + .with_block(env.block) + .with_bank(bank) + .with_storage(storage) + .with_wasm::, WasmKeeper>( + WasmKeeper::new_with_custom_address_generator(WasmAddressGenerator::default()), + ) + .build(|_, _, _| {}) +} + +pub trait TerraAppExtension { + fn next_block(&mut self, time: u64); + fn block_period(&self) -> u64; +} + +impl TerraAppExtension for App { + fn next_block(&mut self, time: u64) { + self.update_block(|block| { + block.time = block.time.plus_seconds(time); + block.height += 1 + }); + } + + fn block_period(&self) -> u64 { + get_lite_period(self.block_info().time.seconds()).unwrap() + } +} From 9aac0a79127421e3cb161bd87f4b43b630c9e165 Mon Sep 17 00:00:00 2001 From: Donovan Solms Date: Fri, 18 Aug 2023 08:25:03 +0200 Subject: [PATCH 03/47] Implement interchain governance (#30) * feat(hub): Add Hub contract * feat(outpost): Add Outpost contract * feat(interchain): Add documentation * feat(hub): Add all unit tests * feat(outpost): Add tests * feat(interchain): Forward blacklisted address to Hub * feat(hub/outpost): Only unlock vxASTRO when kick is confirmed * fix(outpost): Relock vxASTRO on Hub failure * Update contracts/hub/src/state.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * Update contracts/hub/src/execute.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * Update contracts/hub/src/ibc_misc.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * fix(hub): Use reply_on_success instead of reply_always in staking/unstaking * fix(hub/outpost): Use minimal proposal format in queries * Update contracts/hub/src/execute.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * feat(hub): Add ability to query user funds stuck on Hub * fix(hub): Implement whitelisted IBC packet accept * fix(hub): Use reply_on_success instead of reply_always * fix(outpost): Add non_exhaustive to Hub enum * fix(outpost): Move to IBC channel whitelisting * fix(outpost): Add additional IBC auth check tests * fix(hub): Fail transfers with memo not for us * Update contracts/outpost/src/execute.rs Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> * Update contracts/hub/src/ibc.rs Co-authored-by: Sergei Shadoy * fix(outpost): Cache votes to prevent spamming * fix(hub): Improve test coverage * fix(actions): Bump to Rust version 1.68 * fix(assembly): Add additional safety checks for Outpost voting * fix(hub): Track balances of xASTRO minted at timestamps * feat(assembly): Enable guardian to remove Outpost votes * fix(hub): Remove temporary CW20-ICS20 TransferMsg * fix(Hub): Add invariant check for emission voting power * Update contracts/hub/src/ibc.rs Co-authored-by: Sergei Shadoy * Update contracts/hub/src/ibc.rs Co-authored-by: Sergei Shadoy * fix(hub/outpost): Check channel support when updating config * fix(hub): Track total balance separately * fix(hub): Add paging to Outposts query --------- Co-authored-by: Timofey <5527315+epanchee@users.noreply.github.com> Co-authored-by: Sergei Shadoy --- .github/workflows/code_coverage.yml | 2 +- .github/workflows/tests_and_checks.yml | 2 +- Cargo.lock | 469 +++-- INTERCHAIN.md | 216 +++ assets/interchain-emissions-voting.png | Bin 0 -> 67014 bytes assets/interchain-governance-voting.png | Bin 0 -> 64141 bytes assets/interchain-ibc-channels.png | Bin 0 -> 55006 bytes assets/interchain-stake-astro.png | Bin 0 -> 97310 bytes assets/interchain-unlock.png | Bin 0 -> 80521 bytes assets/interchain-withdraw-funds.png | Bin 0 -> 65482 bytes contracts/assembly/Cargo.toml | 1 + contracts/assembly/src/contract.rs | 124 +- contracts/assembly/src/error.rs | 6 + contracts/assembly/src/migration.rs | 3 + contracts/assembly/tests/integration.rs | 267 +-- .../generator_controller_lite/src/contract.rs | 4 +- contracts/hub/.cargo/config | 6 + contracts/hub/Cargo.toml | 41 + contracts/hub/README.md | 139 ++ contracts/hub/src/contract.rs | 133 ++ contracts/hub/src/error.rs | 70 + contracts/hub/src/execute.rs | 1602 +++++++++++++++++ contracts/hub/src/ibc.rs | 678 +++++++ contracts/hub/src/ibc_governance.rs | 727 ++++++++ contracts/hub/src/ibc_misc.rs | 230 +++ contracts/hub/src/ibc_query.rs | 170 ++ contracts/hub/src/ibc_staking.rs | 329 ++++ contracts/hub/src/lib.rs | 14 + contracts/hub/src/mock.rs | 298 +++ contracts/hub/src/query.rs | 72 + contracts/hub/src/reply.rs | 161 ++ contracts/hub/src/state.rs | 169 ++ contracts/outpost/.cargo/config | 6 + contracts/outpost/Cargo.toml | 44 + contracts/outpost/README.md | 131 ++ contracts/outpost/src/contract.rs | 123 ++ contracts/outpost/src/error.rs | 51 + contracts/outpost/src/execute.rs | 1322 ++++++++++++++ contracts/outpost/src/ibc.rs | 662 +++++++ contracts/outpost/src/ibc_failure.rs | 656 +++++++ contracts/outpost/src/ibc_mint.rs | 180 ++ contracts/outpost/src/lib.rs | 11 + contracts/outpost/src/mock.rs | 218 +++ contracts/outpost/src/query.rs | 174 ++ contracts/outpost/src/state.rs | 34 + packages/astroport-governance/Cargo.toml | 2 +- packages/astroport-governance/src/assembly.rs | 17 + packages/astroport-governance/src/hub.rs | 145 ++ .../astroport-governance/src/interchain.rs | 164 ++ packages/astroport-governance/src/lib.rs | 2 + packages/astroport-governance/src/outpost.rs | 97 +- packages/astroport-governance/src/utils.rs | 41 +- tarpaulin-report.html | 660 +++++++ 53 files changed, 10311 insertions(+), 362 deletions(-) create mode 100644 INTERCHAIN.md create mode 100644 assets/interchain-emissions-voting.png create mode 100644 assets/interchain-governance-voting.png create mode 100644 assets/interchain-ibc-channels.png create mode 100644 assets/interchain-stake-astro.png create mode 100644 assets/interchain-unlock.png create mode 100644 assets/interchain-withdraw-funds.png create mode 100644 contracts/hub/.cargo/config create mode 100644 contracts/hub/Cargo.toml create mode 100644 contracts/hub/README.md create mode 100644 contracts/hub/src/contract.rs create mode 100644 contracts/hub/src/error.rs create mode 100644 contracts/hub/src/execute.rs create mode 100644 contracts/hub/src/ibc.rs create mode 100644 contracts/hub/src/ibc_governance.rs create mode 100644 contracts/hub/src/ibc_misc.rs create mode 100644 contracts/hub/src/ibc_query.rs create mode 100644 contracts/hub/src/ibc_staking.rs create mode 100644 contracts/hub/src/lib.rs create mode 100644 contracts/hub/src/mock.rs create mode 100644 contracts/hub/src/query.rs create mode 100644 contracts/hub/src/reply.rs create mode 100644 contracts/hub/src/state.rs create mode 100644 contracts/outpost/.cargo/config create mode 100644 contracts/outpost/Cargo.toml create mode 100644 contracts/outpost/README.md create mode 100644 contracts/outpost/src/contract.rs create mode 100644 contracts/outpost/src/error.rs create mode 100644 contracts/outpost/src/execute.rs create mode 100644 contracts/outpost/src/ibc.rs create mode 100644 contracts/outpost/src/ibc_failure.rs create mode 100644 contracts/outpost/src/ibc_mint.rs create mode 100644 contracts/outpost/src/lib.rs create mode 100644 contracts/outpost/src/mock.rs create mode 100644 contracts/outpost/src/query.rs create mode 100644 contracts/outpost/src/state.rs create mode 100644 packages/astroport-governance/src/hub.rs create mode 100644 packages/astroport-governance/src/interchain.rs create mode 100644 tarpaulin-report.html diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index 81008ffd..659112ad 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -47,7 +47,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.64.0 + toolchain: 1.68.0 override: true - name: Run cargo-tarpaulin diff --git a/.github/workflows/tests_and_checks.yml b/.github/workflows/tests_and_checks.yml index 12ca532d..4005a142 100644 --- a/.github/workflows/tests_and_checks.yml +++ b/.github/workflows/tests_and_checks.yml @@ -47,7 +47,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.65.0 + toolchain: 1.68.0 override: true components: rustfmt, clippy diff --git a/Cargo.lock b/Cargo.lock index c33e9aff..2853d801 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,19 +15,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" - -[[package]] -name = "ap-native-coin-registry" -version = "1.0.0" -source = "git+https://github.com/astroport-fi/astroport-core?branch=merge/release#ff923ddac6b99c10bed53c7af6c8c4a5d2f5995e" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", -] +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "astro-assembly" @@ -35,6 +25,7 @@ version = "1.6.0" dependencies = [ "anyhow", "astroport-governance 1.3.0", + "astroport-hub", "astroport-nft", "astroport-staking", "astroport-token", @@ -45,7 +36,7 @@ dependencies = [ "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "ibc-controller-package", "thiserror", "voting-escrow", @@ -55,33 +46,60 @@ dependencies = [ [[package]] name = "astroport" -version = "2.4.0" -source = "git+https://github.com/astroport-fi/astroport-core?branch=merge/release#ff923ddac6b99c10bed53c7af6c8c4a5d2f5995e" +version = "2.9.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" dependencies = [ - "ap-native-coin-registry", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", "cw-utils 0.15.1", - "cw20", + "cw20 0.15.1", "itertools", "uint", ] [[package]] name = "astroport" -version = "2.9.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" +version = "2.10.0" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#11e7a81d4b18a40bed916177061a549633e02b1b" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw20", + "cw-utils 1.0.1", + "cw20 0.15.1", + "cw3", "itertools", "uint", ] +[[package]] +name = "astroport" +version = "3.3.2" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#d881a4ec782a1cef1a0c262880e9d6868b015e15" +dependencies = [ + "astroport-circular-buffer", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", + "cw20 0.15.1", + "cw3", + "itertools", + "uint", +] + +[[package]] +name = "astroport-circular-buffer" +version = "0.1.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#d881a4ec782a1cef1a0c262880e9d6868b015e15" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "thiserror", +] + [[package]] name = "astroport-escrow-fee-distributor" version = "1.0.2" @@ -94,7 +112,7 @@ dependencies = [ "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "thiserror", ] @@ -125,7 +143,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw1-whitelist", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "protobuf", "thiserror", ] @@ -133,31 +151,50 @@ dependencies = [ [[package]] name = "astroport-governance" version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance.git?branch=merge/release#f7381bb095c15a34c8cb4ba51911506f4d216462" +source = "git+https://github.com/astroport-fi/astroport-governance?branch=feat/merge_hidden_2023_05_22#1e865abe55093d249b69b538e2d54472b643d6c7" dependencies = [ - "astroport 2.4.0", + "astroport 2.10.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw20", + "cw20 0.15.1", ] [[package]] name = "astroport-governance" version = "1.3.0" dependencies = [ - "astroport 2.9.0", + "astroport 3.3.2", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw20 0.15.1", + "thiserror", +] + +[[package]] +name = "astroport-hub" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport 3.3.2", + "astroport-governance 1.3.0", "cosmwasm-schema", "cosmwasm-std", + "cw-multi-test 0.16.5", "cw-storage-plus 0.15.1", - "cw20", + "cw2 1.1.0", + "cw20 0.15.1", + "schemars", + "serde", + "serde-json-wasm", "thiserror", ] [[package]] name = "astroport-ibc" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#2b98128012c373d4ed1cb5459e69f38461e262e2" +source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#f9f4def037d117275de31fffef36ddda388baf48" dependencies = [ "cosmwasm-schema", ] @@ -174,6 +211,28 @@ dependencies = [ "cw721-base", ] +[[package]] +name = "astroport-outpost" +version = "0.1.0" +dependencies = [ + "anyhow", + "astroport 3.3.2", + "astroport-governance 1.3.0", + "base64", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.16.5", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", + "cw2 1.1.0", + "cw20 0.15.1", + "schemars", + "semver", + "serde", + "serde-json-wasm", + "thiserror", +] + [[package]] name = "astroport-pair" version = "1.3.1" @@ -184,7 +243,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "integer-sqrt", "protobuf", "thiserror", @@ -200,7 +259,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "protobuf", "thiserror", ] @@ -223,7 +282,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "generator-controller", "voting-escrow", ] @@ -247,7 +306,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test 0.16.5", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "generator-controller-lite", "voting-escrow-lite", ] @@ -261,7 +320,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "cw20-base", "snafu", ] @@ -289,7 +348,7 @@ dependencies = [ "cosmwasm-std", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "cw20-base", "snafu", ] @@ -339,6 +398,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + [[package]] name = "block-buffer" version = "0.9.0" @@ -357,6 +422,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bnum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" + [[package]] name = "builder-unlock" version = "1.2.3" @@ -369,7 +440,7 @@ dependencies = [ "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "thiserror", ] @@ -387,9 +458,12 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -399,15 +473,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "cosmwasm-crypto" -version = "1.2.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75836a10cb9654c54e77ee56da94d592923092a10b369cdb0dbd56acefc16340" +checksum = "7e272708a9745dad8b591ef8a718571512130f2b39b33e3d7a27c558e3069394" dependencies = [ "digest 0.10.7", "ed25519-zebra", @@ -418,18 +492,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.2.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c9f7f0e51bfc7295f7b2664fe8513c966428642aa765dad8a74acdab5e0c773" +checksum = "296db6a3caca5283425ae0cf347f4e46999ba3f6620dbea8939a0e00347831ce" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f00b363610218eea83f24bbab09e1a7c3920b79f068334fdfcc62f6129ef9fc" +checksum = "63c337e097a089e5b52b5d914a7ff6613332777f38ea6d9d36e1887cd0baa72e" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -440,9 +514,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae38f909b2822d32b275c9e2db9728497aa33ffe67dd463bc67c6a3b7092785c" +checksum = "766cc9e7c1762d8fc9c0265808910fcad755200cd0e624195a491dd885a61169" dependencies = [ "proc-macro2", "quote", @@ -451,11 +525,12 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49b85345e811c8e80ec55d0d091e4fcb4f00f97ab058f9be5f614c444a730cb" +checksum = "eb5e05a95fd2a420cca50f4e94eb7e70648dac64db45e90403997ebefeb143bd" dependencies = [ "base64", + "bnum", "cosmwasm-crypto", "cosmwasm-derive", "derivative", @@ -464,16 +539,15 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", - "sha2 0.10.6", + "sha2 0.10.7", "thiserror", - "uint", ] [[package]] name = "cosmwasm-storage" -version = "1.2.5" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3737a3aac48f5ed883b5b73bfb731e77feebd8fc6b43419844ec2971072164d" +checksum = "800aaddd70ba915e19bf3d2d992aa3689d8767857727fdd3b414df4fd52d2aa1" dependencies = [ "cosmwasm-std", "serde", @@ -481,9 +555,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -688,6 +762,19 @@ dependencies = [ "serde", ] +[[package]] +name = "cw20" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011c45920f8200bd5d32d4fe52502506f64f2f75651ab408054d4cfc75ca3a9b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.1", + "schemars", + "serde", +] + [[package]] name = "cw20-base" version = "0.15.1" @@ -699,13 +786,28 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw-utils 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "schemars", "semver", "serde", "thiserror", ] +[[package]] +name = "cw3" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171af3d9127de6805a7dd819fb070c7d2f6c3ea85f4193f42cef259f0a7f33d5" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.1", + "cw20 1.1.0", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw721" version = "0.15.0" @@ -785,9 +887,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" [[package]] name = "ecdsa" @@ -818,9 +920,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" @@ -844,13 +946,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -865,12 +967,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "ff" @@ -912,7 +1011,7 @@ dependencies = [ "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "itertools", "proptest", "thiserror", @@ -937,7 +1036,7 @@ dependencies = [ "cw-multi-test 0.16.5", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "itertools", "proptest", "thiserror", @@ -956,9 +1055,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -985,12 +1084,6 @@ dependencies = [ "ahash", ] -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - [[package]] name = "hex" version = "0.4.3" @@ -1009,7 +1102,7 @@ dependencies = [ [[package]] name = "ibc-controller-package" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#2b98128012c373d4ed1cb5459e69f38461e262e2" +source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#f9f4def037d117275de31fffef36ddda388baf48" dependencies = [ "astroport-governance 1.2.0", "astroport-ibc", @@ -1017,15 +1110,6 @@ dependencies = [ "cosmwasm-std", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "integer-sqrt" version = "0.1.5" @@ -1035,17 +1119,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "io-lifetimes" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "itertools" version = "0.10.5" @@ -1057,9 +1130,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "k256" @@ -1070,7 +1143,7 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2 0.10.6", + "sha2 0.10.7", ] [[package]] @@ -1081,9 +1154,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libm" @@ -1093,15 +1166,15 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", "libm", @@ -1109,9 +1182,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -1137,25 +1210,24 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" +checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" dependencies = [ "bit-set", - "bitflags", + "bitflags 1.3.2", "byteorder", "lazy_static", "num-traits", - "quick-error 2.0.1", "rand", "rand_chacha", "rand_xorshift", @@ -1203,17 +1275,11 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quote" -version = "1.0.27" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -1269,7 +1335,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1291,16 +1357,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.19" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags", + "bitflags 2.4.0", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1310,16 +1375,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", - "quick-error 1.2.3", + "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "schemars" @@ -1361,15 +1426,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] @@ -1385,13 +1450,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -1407,9 +1472,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa", "ryu", @@ -1431,9 +1496,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", @@ -1506,9 +1571,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -1517,35 +1582,35 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -1574,9 +1639,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "version_check" @@ -1598,7 +1663,7 @@ dependencies = [ "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "cw20-base", "proptest", "thiserror", @@ -1637,7 +1702,7 @@ dependencies = [ "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", "cw2 0.15.1", - "cw20", + "cw20 0.15.1", "cw20-base", "generator-controller-lite", "proptest", @@ -1659,132 +1724,66 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows-targets" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.0" diff --git a/INTERCHAIN.md b/INTERCHAIN.md new file mode 100644 index 00000000..ca158b4d --- /dev/null +++ b/INTERCHAIN.md @@ -0,0 +1,216 @@ +# Interchain governance + +To enable interchain governance from any Outpost, we deploy two contracts. The Hub contract on the Hub chain and the Outpost contract on the Outpost chain. The Hub chain is defined as the chain where Assembly and the ASTRO token is deployed. + +This enables the following actions: + +1. Stake ASTRO +2. Unstake xASTRO +3. Vote in governance +4. Vote on emissions through vxASTRO + +This document covers the flow of messages, permissioned IBC channels and how failures are handled to make this as safe as possible for a user as well as the protocol itself. + +## Architecture + +To enable interchain governance, the following contracts need to be deployed: + +### Hub + +1. Hub +2. CW20-ICS20 with memo handler support +3. Assembly +4. ASTRO token +5. Staking +6. xASTRO token +7. Generator Controller +8. vxASTRO + +Technical details on the Hub is available [in this document](contracts/hub/README.md). + +### Outpost + +1. Outpost +2. Outpost xASTRO with timestamp balance tracking +3. vxASTRO + +Technical details on the Outpost is available [in this document](contracts/outpost/README.md). + +### IBC + +The following diagram shows the IBC connections between these contracts: + +![IBC connections](assets/interchain-ibc-channels.png) +*IBC connections diagram* + +The CW20-ICS20 contract allows CW20 tokens to be transferred to other chains where they end up as standard IBC tokens. This requires a contract-to-transfer channel, that is, a channel between the contract port `wasm.wasm12345...` and the counterparty chain's `transfer` port. + +The Hub and Outpost requires a contract-to-contract channel, that is, a channel between the Hub contract port `wasm.wasm123hub45...` and the Outpost's contract port `wasm.wasm123outpost45...`. Both contracts contain configuration parameters that will **only** allow these two contracts to communicate - it is not possible to send messages to either contract outside of this secured channel. + +Do note that the Hub may have multiple Outposts configured at any time, but the Outpost may only have a single Hub configured. + +## Flow of messages + +### Prerequisite IBC knowledge + +1. In an IBC transfer (or sending of a message) the initial transaction might succeed on the source chain but never make it to the destination resulting in a timeout. + +2. When an IBC message is received by the destination, they reply with an acknowledgement message. This may indicate success or failure on the destination side and may be handled by the source. + + +### Staking ASTRO from an Outpost + +> xASTRO on an Outpost is using a custom CW20 contract that tracks balances by timestamp instead of sending the tokens over IBC. We require this to verify xASTRO holdings at the time a proposal was created to vote in governance. + +In order to stake ASTRO from an Outpost, the user must meet the following conditions: + +1. ASTRO tokens transferred over the official CW20-ICS20 channel + +![Stake ASTRO from an Outpost](assets/interchain-stake-astro.png) +*Stake ASTRO from an Outpost* + +The flow is as follows: + +1. A user sends IBC ASTRO to the chain's IBC `transfer` channel with a memo indicating they want to stake the tokens. See [the Hub's messages for details](contracts/hub/README.md) +2. The CW20-ICS20 contract forwards the ASTRO and memo to the Hub contract +3. For staking, sends the ASTRO to the staking contract +4. xASTRO is minted +5. xASTRO is sent back to the Hub contract +6. Issue an Outpost IBC message to mint the corresponding amount of xASTRO on the Outpost +7. The Outpost contract mints the xASTRO to the original sender's address + +Failure scenarios and how they are handled: + +1. Initial IBC transfer fails or experiences a timeout + + The funds are returned to the user by the CW20-ICS20 contract + +2. Handling the memo, ASTRO staking, xASTRO minting or Outpost minting message fails + + The entire flow is reverted and the ASTRO is sent back to the user by the CW20-ICS20 contract + +3. Minting of xASTRO on Outpost fails or experiences a timeout + + The staked xASTRO is unstaked and the resulting ASTRO is sent back to the initial staker. + +4. IBC transfer of ASTRO back to the user succeeds, but experiences a timeout or transfer fails + + The ASTRO is sent back to the Hub contract together with the intended recipient of the funds. The Hub will hold these funds on behalf of the user, but exposes a path to allow the retry/withdrawal of the funds. The funds are **not** lost. + + +### Voting in governance from an Outpost + +In order to vote in governance from an Outpost, the user must meet the following conditions: + +1. xASTRO tokens on the Outpost +2. The xASTRO tokens must have been held at the time the proposal was created + +![Vote in governance from an Outpost](assets/interchain-governance-voting.png) +*Vote in governance from an Outpost* + +The flow is as follows: + +1. A user submits a vote to the Outpost contract. See [the Outpost's messages for details](contracts/outpost/README.md) +2. The Outpost checks the voting power of the user at the time of proposal creation by doing three things + * If the proposal is cached already, use the timestamp. If not, query the Hub for the proposal information + * Query the xASTRO contract for the user's holdings at the proposal creation time + * Query the vxASTRO contract for the user's xASTRO deposits at the proposal creation time +3. Submit vote with voting power to the Hub +4. Hub casts the vote in the Assembly on behalf of the user + +Failure scenarios and how they are handled: + +1. Initial vote fails or experiences a timeout + + An error is returned in the contract and written as attributes + +2. Casting of vote on Hub or Assembly fails + + An error is returned in the contract and written as attributes + +### Voting on emissions from an Outpost (vxASTRO) + +In order to vote on emissions from an Outpost, the user must meet the following conditions: + +1. xASTRO tokens locked in the vxASTRO contract on the Outpost + +![Vote on emissions from an Outpost](assets/interchain-emissions-voting.png) +*Vote on emissions from an Outpost* + +The flow is as follows: + +1. A user submits a vote to the Outpost contract. See [the Outpost's messages for details](contracts/outpost/README.md) +2. The Outpost checks the current voting power of the user by querying the vxASTRO contract +3. Submit vote with voting power to the Hub +4. Hub casts the vote in the Generator Controller on behalf of the user + +Failure scenarios and how they are handled: + +1. Initial vote fails or experiences a timeout + + An error is returned in the contract and written as attributes + +2. Casting of vote on Hub or Generator Controller fails + + An error is returned in the contract and written as attributes + + + +### Withdraw funds from the Hub + +In order to withdraw funds from the Hub, the user must meet the following conditions: + +1. The user must have ASTRO stuck on the Hub + +![Withdraw funds from the Hub](assets/interchain-withdraw-funds.png) +*Withdraw funds from the Hub* + +The flow is as follows: + +1. A user submits a request for withdrawal to the Outpost contract. See [the Outpost's messages for details](contracts/outpost/README.md) +2. The Outpost sends the request to the Hub over IBC +3. Hub checks if the original sender on the Outpost has funds +4. If the user has funds, send _everything_ to the user through the CW20-ICS20 contract +5. User receives IBC funds + +Failure scenarios and how they are handled: + +1. Initial request fails or experiences a timeout + + An error is returned in the contract and written as attributes + +2. Checking for funds fails or the user has no funds on the Hub + + An error is returned in the contract and written as attributes + +2. The transfer of funds fail or experiences a timeout + + The funds are returned to the Hub contract and captured against the original sender's address again + + +### Unlock vxASTRO on Outpost + +When vxASTRO is unlocked, the votes cast by the user previously need to be removed from the Generator controller on the Hub. + +![Unlock on Outpost](assets/interchain-unlock.png) +*Unlock on Outpost* + +The flow is as follows: + +1. Submit the unlock to the vxASTRO contract +2. The unlock will be processed and the kick message sent to the Outpost +3. The Outpost will forward the unlock the the Hub +4. The Hub will execute the kick on the Generator controller +5. When the votes have been removed, the confirmation is returned to the Hub +6. The Hub writes back the IBC acknowledgement containing the success +7. The Outpost receives the IBC acknowledgement and starts the actual unlock period on vxASTRO + +Failure scenarios and how they are handled: + +1. Initial request fails or experiences a timeout + + An error is returned in the contract and written as attributes + +2. Failing to remove votes + + An error IBC acknowledgement is returned and the vxASTRO does not start unlocking diff --git a/assets/interchain-emissions-voting.png b/assets/interchain-emissions-voting.png new file mode 100644 index 0000000000000000000000000000000000000000..4bc20181fd61d6ce8c4f3cc2f4a060aac3d9922c GIT binary patch literal 67014 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 nd!5kp@gJjYhyZ*`u5Y(T{z$3iO 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 literal 0 HcmV?d00001 diff --git a/assets/interchain-governance-voting.png b/assets/interchain-governance-voting.png new file mode 100644 index 0000000000000000000000000000000000000000..ba6b2bf91f16023701d4ad0e3ab423b20ae7dfe3 GIT binary patch literal 64141 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 YasNPm98;R}2k@tMU+Z3xvQ_Z^0Z!K&l>h($ literal 0 HcmV?d00001 diff --git a/assets/interchain-ibc-channels.png b/assets/interchain-ibc-channels.png new file mode 100644 index 0000000000000000000000000000000000000000..92393773212f71cb9cf9f8ad68f8600892697685 GIT binary patch literal 55006 zcmeFZcT^Nx^DYdS5Cj1g6eK6fN|YQ_a*&)s2@;2#qkw`61Cn!2A}~lA7@~q?$$4N1 z0>UulocWsbI^g-e=iarxweDK?k9+(>+wSS^+O=yxRrTzue*aui3J-@A2Ll5GPe%Hw z3I@g%QVfhsxY$>LGt|B;9Kb)99VKMcuz^2b*e35VFsLwOo{Fov87v`OU#ky}uCAMT zWGfmk)b8Tr;P}({*H+wBU@iWBSAox#n@>HH2|8l5sxKxcfJIZgI}s!zU$pB!WEU9! z@&Q(mN;7(j&;IEma4rc$ zoc5I9e{pZw6>L(a#E&@teED@+xbG!ws@E7;|Miz09P2VTS3&y9e_i?O*J3@b_5UF2 znTU@;nEuWE3|UzJJ(RP{ROYjj|M0%ErzSx$#AhsSsbK!MIA@pNJh6Lqg+U>n{cwl! zy$yvy@WZ?AR*`SqLA z{+umk?E0-7ucFPafp3N(6f9vPJ0xvgT_T0dY;U3oaIuw&^fGDh6;)Y(N2)iBqO~A) z=eFDSw(BfZaCI2Nc3ZiyVw5&aYV-1!5;1c(y@SO5EYEJHY2&}qjyD) z)EMLwm=#F*BC@`}xivBVlfDD-Xv}5F?zFr+S+HDm+THY~R{hIJ>&aqO99&#_<)YC# z5US5*)P8^e3j19|wQcH~w{PW+2X0$D7cJlN7^_%y@--}V2{>F`hu97mhbM)HhxcV@ ztL7=wP9)E;{3>=BBpt1BPCWelGT@*O>MX~%*stl5y&R*ESW(%Vw!XejI>H+XFSm+5 zvGKDREpgG8saA=IG?`m0kBE+@-&Tpp1`o-Jqu15vL@|5i_4rzPJkn!zPEOA6y9xXp zqLcazoHRc@4Oi-_SNA67?Xb|sNx+Jgdz+hG8!CZ=gEM>NcdQ?$3H{#G)b2jN6WOT? zZ6Cgs+YUP(u1Z^P^A%x1abtTe7KO>k%ZCV>Ha06bOg^cP8MCcbCc3rQZe?4?DCn{j z;VQcKH0*uM{K>ZK{M_3R8H9cAxZ<`lN9^K8M~#*!C~f zTt7!k<<3Z=O=1bB(O!>ifMoURuIPb$vr-bxBRG3L<0EQuo4RRBBpjA$6^1bUbQMet zePwyq+%`KiGjnz$ym2ncrxx*Ohxa&nS9IvhL*2%vgoqeu%i9#ugXvBiIQR}3kELp^ zr_1W1i0D>_)Le^b&9ju_)%*ybg^z-}J?S@K^Ic=J5ig%09-YJ9*I=w2qVe`768+}~ z8SXnt`z!YzJ}QQ6t&HloH;3#55A)#mFeZGH2@l6a&Gx0f+j-tt{RqZ~GapDGS1pxWAlZ4`_U z9DN5DuCwT5i96#(d%Giamc>)iPsHxM$PSOi_;Z_KXFST&j!1{ba?QnqQ2LuT2dxa( zoN;70@WvRtxAO$IXSyZZR8M^-6o#QwT{)J%YrXUAS!DL>Zx4FzQ1_d8u_y^07i7`V zx81anyxs_%_E~X0YkY!BI?tw6kZz`WPsIA2NL+MCOc{a0 z;#T}h;W`_i0c{-vfqB@CGE|z)h)Iw z)=wO=VRlXt`KUq64PSCf^f#vEDb#vy(O=vWl9(pxDT{ItRY8Xde$4Cy8C-&_UlRA1 zlWtAO*u&O8&K~FW zD>0sDCs`U*`&H7EqdbQ7lXs&#ry_(*ROTcn^XlO?GVkX5&Mfy*hQ1v2wyO900`vza z9~73MOzfso=5XvMhVfZ9Ke`z1NY)x@nfDhg3v?#%+S~qE#_Y`z*PYVRQd{`^xVK## zI%GUblrCPVklxl|+`ATu6b=3DbNoKjzThwi5qA`S5boap0KQ+K6NyR|eMBO*KZs(%T`TB zn8x8J{K`Fd?>KKAa1s9;^?0wrT7!#wQ;<2N%-EVP^KBA2ygYpJXhS^y7$Hn-9B&%-=lc^q()}&pqW6$c| zIO4kygVGhZ!K%aH&n9ds5d_d#@UcD1loUF?OvAZ8;+O+I$uAhu8QKrx(=VQ6@yr8t zTkj;zdJdJH!c#w>9x?9Y$%sCQ3?PaT`Wve$CwU2fn@lQolq9_=eLm>mcs%i_g*xi#^gM$3`dQsb*nvaWxI;p@KXtY z2km}h*YAry5|qqe0)JMZhB}*C%i1;d1`)ENyAgt9n#=_MsLfSM#Z;FK)H8{iYmnwo>=>b|@ug7=(G zUeEAkZt;=j#f8UfuV^K1fObjqZ+--prDYRNVM{b?+e_1`Dw#Z@DPcXZMzpDDAQnEJ=>z-Y?!;Wh9+Y z>U6NVkVY}|*#ehIlZpxlM*>UL+P&Xi6pO`ftG`F4EY~r2M}<~o0`$emit1pZ8J-6v z+sMkBY`vi`m!lbG)Av~&SJRk0Ss)@jJY7}FLAK~cu<6*?NG9cYsJv}4ACx#_bc@Ka zIFFv!sKhuZg8aZ*HDS9dvmAVT>yECVSz~i&pNbSNyzI_c+rf6l1jjLA;>OK;UMX}7 z2O-cj*CvMoBnxP)%)z#Bm-5TC@d%@D)3rBGB!EOg{n`LCU}ym-%^AR#~tHWwfjjP|61^P zZyGE-Om3_8QdTx%X?Ro!`0pxQGJu$>DkD19n_-9Cs@t3xhuyGP)-c4ZZCle2A}D#& z3H6lMc1Ue!S#U#Mdj%^*STb+9>Onu0$Y|*+3zH|LqO2Fu-cnY z!uKprTru{y7^;!172|fD%K8tdlIp4gyIrd~wgmh1hZjeSYzIKY;#?Nl+arM-yiH9N zzl=t4mZ1{t(-+_~@7SAwNeI0Pgh9(mUmf4~t-|N`LrzI|{u?{u24@HzIN0>@E zXb6RMpC>@hB+^5jN7mZ+6LYMsUeC|Ywi9{hfto6m<=3aF`V3+Wy( z`^=FRrY_)Yz8W(*no+IDCm^J2R@gOBTnTaS?G_3ejBYOrjXn5cRD6$(mnOphJmHJ{$|RgTk~kV1c%$#*LuF!igGGwap@j#c zxHXGId9<|bbh%WbAJr6ZL z(@c%=9GJkZx*c~4_9}dj#z5bVIFbaRwwfgrGt+IHI}T$h#*?QDT-sIE*=IpxB!iMG zM~ClLqi(2sd~jeA2QE*D*G6i$jHkEcc!h8M7}EiRcT8cbj#BJ*?U!Mx(KZV|SHv47 zJkzbM6zkS^uGwgW)|x90gI}^%meB_-!Aj~yR5dibP!X}mY-%VxJpSx9nP`3g3mLg+ z2r_Q!F~S%2Li85YSYY#hQsYrSHB(OK^EAG>qpb}COI?1d(^bTdZ6%$0ZH#IY6C?xkX^*~y?CE*g6NGlh@u`K6`W zzC;nhLj%qs@$WyuHt1q%?O7`{QWkxXPqLUGQV*mlJWLJ z(SICWuEh{H?r|l=d-_jzQ|$o^SU`Q4IW;<1M6()yJwlvAtmp2Q{?kinH~u&Iuzdh3 zYigBN=N)=IpC<>v-C{ks1l(7>NqWWpQ8E68uhSmw9lj%``P&tFv-qeR zRCXI!jJ1-+K0bb4B8x2XT(*9*@K-oz7Mq5HJznQnM3+NHUYh-~+_K%c-dhhpFo=Bf zuFu(o_&=j!WtA6jF=x><6j8Apcy}}ARdTqC7ndVcK8{-*|GvB^?Y8pua|8R@hWq{d zQ@P0rVUyXlU^?!tffo9d0a7rFMq!NTQ9Yx{SAK8Pudri!4GzV#OyIM>*q&gjbl+DW zB42}3*~Dt(FHgGF-c;0()@5)s);c{F_T^|XAD{Fe$$W-&cZC&w9OglsdqP%dC5sq871ZB3C>YH2do2Um$%a-hS(&gZrVE zkDJDe%|M=wu3+C-K0|Kn##>8wY|8(Td&FCR`Tq&L`$xdF5?Z-`A$Q+fUf+5eg{RF zph&^q~v{+Dnd1dAB5#M|x86{CVrb0{grkws)=Z}Qr zd~a@+b5ZPKLQGzl;!aU1{A<$w#HQa0Pa&c*-%@jREd8(5aJDY?q+X}>ib4iHpr_~0 zC%n!DR+;c1Vx0BdlK%YJ2_f+0FqZOv1h*+BOn*rwXFk5*KW{R)5N&;POKR$0r*xsWe|_+UF8+NHYZxbn&naRA|VHaYqJ<*4iaGLNNTn+yAx7Rk)ex0_?8sNx} zeh7wN=9LuBjhlU+Wg=>}`$Xw^d4DZj+GF^1`NM|~vyDo1AM1}-Fx~e~Qz91b9x-)@dX5iY?XSF2IMxC7Nq1%_8pFOZJ+xkU(KcSMTEfZYU+Pze z^P4p6`V3UeY0v#Ky6`!sS8z6P>DUGI0FuEkF9WMkhc`BPtU|4KsNeecSRY3M+@qOF zWEGl&$GlvZD;EAH@Qqiu<71(?%MFbU*#KaQ|J0_^VVDcC==5Q8$W z(~1SQ8FtS%T>hjm#9jlIa_Mrn=|7UYpA>;14d(H|NX|{*PbwCR4q%3<+}Qt-!Vq%? zhE$+f+gAJM`_9J0ABL5cE1I2~lT$JB2liR?BqSu-2VwU8#4=;Gx@Yxv)aejzEBI5I2KOkApzqgy%Z6Qv zp~Q%}ibZ;XK|p02@zt&rF2lNG8%TjcwZYEvSkWWn;(6R>-z|BFM5wL_yI0#yW)K#i~& zH!DS5Ar)DfC{QcG+7h7XTSc8h=L+#+BhX7_2ILe&N%w&EjL^$tM4r+XV3OI z+8{Y=Yxyrdz4&OAieZ!POI^tP`ZdmN5|WPL$ei|E!;tWUZ58e%GG?YWnSLxm|R4` zcFc~xGjUq}aD#1>5cbvSTahq#9l3;t9D0<5T3S<|)i=14%KM0T6gVvpe0v)i?$LQA zIM%Y2zjM%Y{TWG*D|(HH^A1qzxy1ZeRw{KRkyv|u{T3UNyk&54kD9i0X2=ANDi z`cBX71!gcA&&S~jFQr#qDJ;W!k5p7pgZ;?CYQ3}tTv9gavToi!d!UAplzoK*k94_W z>q$_STz{yFQc*EbuX6BB1u7ji3Uyd+9?iDcB!4ey!fIL3 z7p&$XO60R*=CK<3cy$|PK~UKMR#_TIPbLem^lniEDm4unztWK>?#m-txL_8D6u%a=Dffr<<^ znaQefCmJ-m5SRn&OKcZo_&2o+tXpgxOk6Bg6usO`Gad^_NlPnkYZ+0x#fN*Ck(r?v z7Z)E-uRS`-E?@Sdhc~oX`NK}4Lfb^CYtf-69^M_Y`}laO&T7V^M%2Fo!HEbMuScJ3 zm12j@z!x7_`Q1Wt`{H67r`hx;_oAWE9B4H06T^=Bapn5=Xcz-pFhF7AbT4R$jCsBW zhY4~vthsfTru9lP!V^NEg5?UC(1*SL5{a@PzfA9mOe-$tMLL&PUZ z!EQ;eq5klY-d^Q{-P@zN_51#f0`u;ZrX;qP(*%9qvBwEG#1UQV4A zBQG?5FpUkBKYI16!e$;Boj+8W3OXnQtCT&o-<|Fa;mzp%fJ?p|1{5w#qK5MHZB%3N zNIC0vd(BF0ju~Jli!3JZLSNmud6UIe8&{OAb-aF647uwL=f!;r!g1*&3cEts{72@zROKPAhq|!dzgXu4c7BzWTClwa+;O!zB@s$C$9ayZ*(|#2y?I^4JIj;4VvtJf${XZfb%$ND zC85mngUVptidBNcW`C*4s`u+UsfGQIy_>CFYnFNST{7cMJ2a~vI{c`-zM?c5@w@^e zwDlFymoLSqUG9&7bDIK8Z3CwKO6-ghPhrq%Y1vBO3aebd^C~l&F1KJW@+cH- z%1P<~(3G}!HlcL}Y}n|3b$JU#q$)uHfO#TCvh!)nj?CHIm-%dOUEkD@a$EmWvV%9U zxgK_q!hyce@5QYb;N}-7LiqW*xiC^-3-!)zAfE6~mW)J$%b%eL0X8on#{Z?8{nlPs z5iFk<$rFfC5nute$Vxy|Jf5u!HD>@kcy{LlcJ`R$>OK=qxJ_God(5X#(lWC09|t_P z>1Z;JXr03;s!cn1M*$yeVrnM$)UA?^ZXlWgOeU*mZ4J{cMS+HlT~|kGVealaW*KER z{|R2mD=apNU6&ggB=U4CfY7B+nueD4(zowCXdU?(;}CFDLq`D8rINX|QFf2Dy{~rBC|~TaR}Soiz;^fC}Ui^EOT(u1gqz=*vm!32{HB zwz%_KRyP#@>mw0e3g^8c*hwK@2asH*A)x{_(+Mo&*bQ-Uaee(&t|(S}fhxNM3NEt= zEEyTaC_JFi#mOm3kK5!q@`03Ky7$QN3f);6gS-&(#AuBf&nwgaJwV_GQx$`@5U%l@OY9<*Oo3gATEpoiY+;$1_W%Fa%9xd5M!`~K4_4O=`TESosLnF+d z+}E7r&_j<1`6Nz+!e;qK5##AnQHOem2irnv_*)s7>(LX2O<-L(Dgo>`&lVNtR8@%;fR6Ggd+&cR1kl3nq_Z}G; zEt1rnqP?}COyBE0`sJ4;0EbDisCNsEHfxcu6*|cUeIn%JMH&ln9~fET$4q`3QHq7c z48of^pwqIWufHsTHO9%S3{z8VE|HAgXyy&OmoHufx*Qo1LC2QB6FpH|B^Rqm9$HfJ zalIP0Y1+=mWjX~r&W42wl}8wUT+LAp3Ji?lXMEbQ<(Oj70(o|hE&uJmb02)M*wom! zJbJqS>X-FsrF~>Daahy_btdSs(X{r%+qlE{dG;2QPyV@ z3sog&SGzX%C5?dCq)%n7RRutzbAoc^?`l6*DXm)1){#GzfK$4>T0{%W$Q7!Cfg zF(dpo^8T_J%F3{Qv}**2g^2QL*_K`ZH{NrP0*g0-O*WuN^k|2W?=vWTMN9}l5bXlL z=t35^f(=xVFDZB8x1;^l)2Qa&@KvpJjZ|B(!|sv-Xqbki;nw-3KG--nx7^C7&v#cUDEG8m5+(^hgMF;?IIA?P>#? zTBLyMNgWWY?$$-5CAN%6w*k#YZT`>XvVpA1rmAckRkckvmsD3vmu{2nFu#V0UP0Jd z0J>9c!+6o?K6T9xe;NMnxY$U1=u=U1#}fZ0LclL-2KP;13}x|%azGQ{Ll_J#Uhg$@ z6v^esbY|yV{lGuUU*d9nH7_=4DM!e4XomZJRC++SWBTk_uM9}HB1)j#s+#8kC<@uE zkT~}r)>4yl!_AxT7kXO&9 zDMsQC>t_~H0%-*+qS_?}=;3s&;kY4^qF^N9uje^v3h62wgI4S39PT3Ng{xe{huFgO zcHNxU$k58Pg93SJcMSLNIc8w7_LSMv7K=u;Ujgc9sdsXXikbR(1$s}C^TP+YA{GsWDgo2oTeciq<%l^q(uClc{hN>L?(b*YU zu(UKoaf^ti#AFnQg!`gbXpupzM+@N*b$C801dr+cG8$<3ZD06W43)iqV@VTd_?$}6 zHM?!Vg3Gj}i(95JHlvJKhiXmOSf=93Hvx7E(vbX;z z?|Jr_$WFVxZs+Rq<6?WbmQ+V%lb#mi?=Mk|u@T&BtiNC`K2vigm=3f{AtNIjp>I*m z^Jbi$Hqh`vXHD# ziqxs_NC#r8oGm+=!HN10-1n!e@N;s{)~+dPXc@9{bz3#tc9lXUUqfLx{vsC-z)1Xd z`3XnQ)eAY(ZetIIzaz$xl#-HBQ_He4es>ip*gm{6TIF4RwA$&azh-XQ^qGg#bH!_S zHzY)U-)rl!`)Z?-{#2#yDdxWA_|p#jj_4vrg-9SF*3y6&WLoi2M8!|~Fy?C%MHe+I zMSNxQ!?k%0{Bn?^}Rbe9^12kD{a;WEyO9kPc`nY4UFzo$QSCo8O9L z;i!HJltadvvS_q1rh=K{c?+lKhyk_-k>nl`_goWbA|^Ct4!~l>US(y(&f=%1xHO5}mQxMlERP;Nnp-hF3{6(i z(t5zgoh08w)hl}l1M+np%dT!Om?dLo`5( zdPICQy}f-gQM%P*o$Gjby{}6{eehkyv!j8yM%Ktk|E!anwXkECBvC`*&w2hY9^|bU z5R=LB1r%Q`2lcmuhK#HpQCuetrspi7yDwyu8CF{D)V9*!l>NtiLcFo45faYp2kM$3|thnJ1z5~;2gUD8uisy6=ye|+%4X|G&06%{W z2`DhA?E+g`k9_glTAEM?IAPYJ`t7sy?*J_Ibe ziqK4+aY>;AN#UVFRb6&+`|y;+v}(E4@bPg1{t918^3gbsfNsjE6)p)>1rMdwFl0_r zQK!}=*)Ev^)L%YhWQ?%iRz(Spy`fvpSpy*9^J8g4IH! zKfDegJwo8)dx3JeCXsX53=ntGlVkGRw>zEnhL34i z0U30O*{VqVPA4mJpt!cod}6?gZk*${P-^yocV?dOH%X>xFFm+(cJeEF?dWHa6zG_mOB2WSsA z+CfY?U6D|#!0I|T^b7R(>j+@s`7YSGza8bWiS@X_->CiVBsvMk02b*H*|`Qjr-_BH zC<>)Le%e=mm&KioSM!@-+8-aw7*u%O`Qo!~n1d+V$cVa{(W+n_C4V4C-ypL95x4IW%Py>iAD?7cR$l=;p1P& zjr4D5cwvFR{P>Dr!IJg+4vJLoEouHA^E`Fkujfq_r*M{*rGY>}dtk_r z^2_v>DtC#ftNUC$GbTTeDG&qOY6;~i`f}y|MHj2_o4>_R@&}a4lxBaPaK8Q;f9tNx zKHf%dy|DK$=B%7jhP5BKMbe2~-gGoVKMV9DaesGmh^^r~bQW+s42(H%?MNY57%Hu| z9xF5Oaq8}#U7Ef`ZPv%j7&<-W~0Io4jhJYC(b)#q^A& zz0oyyF{z=%ckKr|OH7{_x*s=&nGEm5W_^28OTedn#ZSL*Z0`Lh4c13{H^WB zhrslhO(wBi?Bc7@^nB8mgU!?#s&C*Lo{NgDb_MfSmfBl`4DiKj`{U>`48Wg{t^}Ru zEt(f%0SyMsfZ1#Od-MMvg}u>yE7>QBUKRjw+$O#wWBrT7Y(~Xwmef}Gku*;KL~H|> z%GhYp?6hvgc7`1ODgWWHD1wEZqiWqLf65DebDGZehZ)5>&WwC$9zK`;xt^C!t|NL` zo&|RX#4ny^#(JiD{)QR9%*zrIp$8w%>HIUlFhvLWG9|3{PkvADS2-*?u)!i|tV;4Ka_FtizI$b;L9)ka+beg$Ip`LD=By| z?H25u7XhLU?1veTsFw6@pL?ncu)y$Jqz5vy+JngI^U&~5Q>3E8l269R79{yym*Mx~ z4))6xr!Ove%NIz8?Qj&0miPrKo9@t4!MtK9diG>o2~w~7VL&D zYh|n#C_7T0zs%3_EW7L!h#LDn2EYD0*!&2b#}~dC15*jW6Zqe0=?gtl0bh**NP_~# z@{1+>$JHqrK+T>dV=#-~?|dACVZ}`$7r1&(@c}(KbpURLtu_|@<4jT65dq-A==Yua ze{tChcm?HT5Sm5&>WH-@unc3L54?Cq*E;}C@4d_-`YpvLcB~fN%wk|O&LzvV6|QhN zHV3Mc32t7sq@b!=o7~Lv*N76%-$H@T@w*Aj?Dr3fcN-NyMUw z0V1LgAvN>+T~pTAXJ_xUjxeE2`U(_`e2Y}MbQ~OTO@>qCk%Ro|?38dx9qv3w$pkJN zxQ8~(r|4)4YG9UC*Us!doxHCiN^ihko91jPTD@v;=vMq)GN_|GwoLTbBgTW5Cnd?8 z-d)7?6CLm2Ys>!28>4Rq9XjU7>A9l_^BFgOx}s-wN)4D1CH2rv^wQzNdiIPalK~Q4 zQLn{3Y_%{{FZCpkkc1va`k37ynqk!&ChznmbKFzzCQ!rhm@PKshtOyV7h>?GZv_!0 ziMx)oh7i9nttNCuXx3k{TGDho7xk{QP_PeS-f2v(T0HR~b-AWYQQc~0n_8l zSX!uia4%s*euuI(rnU9qsP#9!{whw74V`^iDbbbDR5n7w^ghV5jBy`l5}Cml-R+2a zGFs8<%%Yl|UYgO$?X(f6<@~_%UeV>*QNH=MF5!DNvxw7UtAue^Ii>v>-@5QuU@JNJ zguHeQr^3l$?sP<~=7QvXcAer5aDlvHNaXZ<#Xxwpl;i`&1emg(*1lbuQJl9wHrZC!1#d8K?EqCQ-+pSnCZqDM=#S3P|_LVp;PA;=?hGWkw-dyTZK z7!)=(gR@*ZAHcMV+>uubwnD$QM=1ROeDM(u3wR*YK%RlyY_19vbGiu~`S7@Ic ztx;J#FAA(VI>CxBbqm6!vqG8o;vVotC$gim@M%&}=HH8Q3ZnDbZ5UI-443Z%wH&Ke z>D60HQOv}b>qR}{Cps9+3{(R~ZOB#UcC4yeNjz4F@B)H^pUlE4e#xMIy?1EgNuFzb z#A0%^h3DZFGw?1f>I6a74$^dX5nm5+*PZKBBI%eTf%b~%6xbBbHp>Z8n0_Zg8$Jfs z9AISoBp2bO*o);~-ZoB?g_=&Ew^|7UbOHLHO}g2Z#5&Tqo>yw6s<`^b!k}~?*T!u2 zE^%wAZFgh~<^2V$?>uC?)gRYCzXWBc2U=(HJS`PN9n~qAj_k8>U)E)pah}<7jEq28 z1Zrw&vEp&F)|iXo?jjOX*~6Es@!O0=$sRa6M@`IXlg-8R{gOS5d2e(3=Tp3UslH|@ zK-jkTY?0OnneUEKJlZJDPxhwYNijs+JZ;|(tnKVq%y(SN z)T~TfPlP4TAvj{KyuGu*6Nid;PYr#1)43M&{ERzEl{sH+2*5TnphOhJ%*M`^2XY71 z3JR-*yA?j0nC{Al59EETHxF!9JjkH;qby1*79I?ykVX+xINN%iPYIP>8Ag5#C8&W9 zJ!@k8ejz{f_|&UxmfLd?2NIHk7R@zmeRb*)xP)x4t@2R3p#wcO2T7eJ7Kh@RMc`u; zf`0I=$#%rgxp4~cY}(ZbUG5)CeY~(O_|EPtuw}KK!{{P6-%p6r@Ige{@?C+nc)dc} zBRIEQ&vQP=S0XcC^XHPjX2U#Edt-FgtH3u%QnZ=ZrcAgu@&ZtDqoRv#8VTua}@dp>HRGQuK z0R+O+DEaQajNQI0AGYG~EDZ^D>!MK7S_7%b1T*8~La7#y5|0^?UC)@%A2(Dp2WH*) zN=i$I98Yh0EI#;--=~@@r^_^SI@&68D5M}|kH0dk#!3YhS1c1b6|DUmyGsg~`ls0Kwr28frU*@zk7`_;Rz zWVpz)&9Y^VX~hkmy2TCY)A93PiPoUZ5ZC=YWI?;)MX!$1tMUjCI!Q%3NIz+(A-C6h z{=!~IX0*W1c%D~ELN2kv8x@_52orQMjj9g4VvT|>aU4SojB0Qv+hg=Sxt!>Xin(PA z)mJ=mcU*GF9VTlxrW+jU5%vnh4ynrlU~_qy50QH#v{u@@7Fu!|NbX;v!hBEg7u7)3 zihdqZ6W|n8IVpNx8@pLJA>m7##foSH(veVEZI*g>qY_+O-Nh|uTsEs_sc3_G>q!tI zntj=dq0P`BP8;HV=p;+!l-*x?yf-`oIUpac7l*e%iu4rdDP7FP7Fmb^QVlu^M#L7zYeC;2V~KC* zFzHG)ZF;9F&Lfr>#DrcM4FWQ5vKR{dJtl)ddMHjJh$EW^vul_RwC zd=Nv`VM!rQiSE#Sho7YH`^)J-jtRi0wlrfQ*WCB6876wSXpdOFoLTYKu7b>jhrfkt zhZGW8%(?ES=t3siMYMVrR+?h<7U$hNT-x=kcr!q(#-_d=(EEMJ#X2L*+_&-jSDwvD zI!hw23TfaBa!TB?6NN4BlG zO~WCyf47Dqb0K$@P&7dihBLz68gRf9&L8a3ib`d*_scBIt(27N_U3kZ>m}q8?bJin zce1slrbNs-iFA1m?FDO(?%TM|L6%jVYMUZnclkGtk8hBLXjRUU6`Q@(1G@KZUPrJ6 zI~(5?tP^_zEor*Cxh(u~t`43@%O=``XJX9=d`i&zyFpRcwrR}DT8&G;56Kc+AJtZq z>pW$AL;*=#2a7{BJ7tlUH?aWku*G+M!K~ISZ8G|^@~{%IOAXmonWYMvA{xb>#n9D3 zl-6BKrWAzk=+pq#lgTnuT#U(m>v+iEU6w0BVWoL9SFXHWIcX<0?Ae=@D@E8KJh?CP zI3}uqXsKwKLtbwsTlF`9F2&R5yHpnygo%*X%SydbbV$|j`E<|NhdS|V)b_7cG-iK& zf0@}AZKwq+vZ4 z4@9d}E2Jx_q{9MbpG8!~f4JgSJdbzUX$X$_m-1)%j2^+EA*~4b%2u@ z3X2@m9K~06ysuIr(^>E1JFQ%+P0Ow#fPAzH)`9qCudpAFJaeeFk=#5I+?+c(C1d2- zIssMu3a{e-vCb=3+^L&m*>kXv|0l$~HD+ehMqiHZ8`D3cF zm}T1$>SPnUB*~<~{pES@2hi+B(#pHz8Q?QtU?WRe4-d%Bvrk;>cgP+svJ8hUQ1K#; zD^BAwtW1ZZhRkboL2|wVR}&Jm$+j`)fGGWH zF6ZQWY!aVnMEpS|Ne`tX5dN13zPXa%#50f_0WzEY&_ppkWYJ@$8nS!<#dU(FRCv!* z>w7+gFHcyDkNyH(b7FRd&<(tIspdIs$4uU+vDx$?@o_0ILC6TQ3)2pR-`yDIVGu;U zlV>4r*EWxjHKc;ZWFk)2KPXrkl)p?R<1tU19ZvlL|EO z36{M8k7ZzVyJ}n5~Xo3vO$8!Ixp5g=&y80BlxR9!EZ8ePLZLLCk($*W`OzC zfWhvfSp~bYuNyp`CdftZ-NvaC1^Qyef{=tt$q(C(%J$(Wr_@CE2pOEMp2-Di$N=e* zHYDQ6UqZXwC$#`ksno~iK+nvqN18np<-&F=OR+O9lf#W@Qs`*@eP5j^kidXqBOePq z4DzaQ3oHcFOi@( z?`D79Y0m*&;}MdNqSPDJ*5S>Y@e%x(!jc-I&lFjQcmP(ll37mZ1vb1`D8brHufBuo8s9$PilzDydV0*8u{M#1sR`^{ zzd%mnncF5_Gnq}C`zU9JCihEot?A@WckLs_Tvj63HX3wsXKTA-<-*uIvC`+#@6ypR zw7S9$O^*fnD%$#%X3l!Y6AK}2D&l>?BK9^PN^Z=2{w9q)4p=!P`%>-ao{V226bkON<+ZrPq9AvF8BCUV7v(ql4=xpOw{ft> z^lxwHbKIwsktwiP`1s)5l5PNbY~LtzbVF>K9!4zy@^T0Pu zHziUWjW|eDbY8=(2k~L^zaB;8P0wUv0`h|@qLL?ESDbYnlqDG>G7=-9G61_zbghBK^5kW%qw)Nhk zjNV2IK@h!nq723uZ4Abk^VmBg`~3dz`F6g%zx{3M{oHlkYh7!tYpJF;<`c!6WO?y- z$c9PNoN+>ivcw>+Sm@mY8*H^+)4*K3Sdc85pEVBI8k`{#x<-=Lc5 z^le)WJ32Z{7DAnb*Mzlwz7m2o0uZL=i?^q2GWsVgjD&MueZN6j3di2{6c?FD)*Qt=m?6=$U#kEHs;<`75 z%IXmGT6${i@fGWRLphq?w}xig`GpCE9)eoB+O%Gna6H%K%FHwkUuJZs-`a{sx@T0A zHIs2?$Bikao<>3*lyyA*kM-So zoy;RBMnTWuuAGAgpQYTo7ir~S8bRlvx_?=;R1}vuL&6Y?=7wKe{Is8+uE{GEpx3*F z7tM|tlKw^z2?@y%1H$)Z(s!mWl9ryt0#G4YwK$=8o8tJ0V#iKt;f$JI^qIARDZvEc zzpTs2oRtSRy4@uFq7zzI!sBBY4;(VvC@=d_LTvYE?P>16>3K@~da6P_T5Dr(M5g|d zRZ_~FdD@YoUAWDn6`rJ`fhb$!+73QEB99xKc*9|mLpTaa=tOAhDtXuGwokY7w3gQEr`jrC`P(DXLHlHhM??H@Zk+O{$NZ zR6qz#XIZ7GEPaKl(i{1f0~JCy*E?06oPxDEPtQyGX%vz zpdYpS$cKl&5oZE|=dYpmfhivRmWj2wB-x)wgvk7NY`7BnCN0Ag@31ipDN#ZvnN{j{ zgA{uya^PHJ?9a_?Ld|CWV!NJI=#=-!`QkwO=Etu{e)Gxfz#7gNPvNJoeF4a=p*6Ji z-HHZ62nxcj?IXbaB5PC*3 z8!?ic{`;e$O91Mo{knT52g)WJuuE-CVeTLL05zKn1_suu^V zMMTzz=za438Yz~0Ts^`NKBtc&`>TRmG5!saCiKc+_c0*@yNY0D9-g35wR1Duq$dLD zojC5&?Pc4WGlp(M32iN!dMqJ9FhjO$+1tkaPeo3Wf!^LyRa3hq)WSIIYDvj8!%ZYr zPo!!^Z5Nmo>*z&By7{h@%GxO#9QgA?eENfN@WCC3RT=pc+{wljPb9gq0Pk#uV1q=k z->M`<1wb_9v7hNr4yE_^OFM|MTO|~385OCx373pazTdIT=F|iZ&#kCOK9!yrb-y*J zV||$^3xEfW54LRWP9Ao0s{cOPe3t=y!0&D1BZYtc>%{r-N(YbySv=%y{{Q;dFTa9# zGZ6jv-=n>SuiWL}$n#0J{T=TB%->HSUyN52%5fSkaI%`m%aWoDEDIts-si+uoPVh| zo>v2U8Ge$veSxW074W>#c0~OY`K2*4trC0)dd9YN=5#6Fasq;-jVdPVS38ZVmu}Oo zl3D!@6FFY0ML^3sy!&g>j^CP^W@`O@3pCb<0rGMVlWf=gel7DROJnHDUUhUC;V+vD z=&AjfY(hD_A22dV&$n+W0SO>vW#!E{HYNk0z{-O zhmj~f{PodnBf;l5(IRr_sf(LQHA37@iFQNy=B!iXmG|4% zn|aXgBT8`BZ$GdozuWZY;AGp~Ch$>&7 ziu|p2FkJ!CyuXK>M8~>lq{y3R-ct|vbIQ%l4KIB195A3tiy>TkoP8;MzMrG|Iq^Op z0vZ)CxdJ<-Bkx(-0wOuh84c-{pe%a&P6m`8`McWt8an|lb=&UBI#jI$hLn^V$82It zXU0F~q#-F);Ph>y1x{U?+2yWX58fZ}Ld-&*^EY;y4{DW^#GjXt9Fu-}<)c-nuf=a&r4CP=+!tI7fQY1RF@iJy?Cx3c+0HBvW_79iF_DoOb825ilQZ#I z)k&XOSz8V==OV?PkaP@jK5|IalMQJKNGX~eW&E%&S-XXltSmh^G%{(ibowk#w5~ndkWXkXGp8NBq9w({+*I)fvqJ|kXq{jifBhd-c! z4za$0w%n$g)PYKo&|WVmPDeZN37Wwx(9BwiUOo3kM1D&L7r-3tvyVwQe{vijorQ#& z;P1Q{jZ6q_hK-cCIe7dw9=hfhjhPULn zvl{!`yWta^i{{?+?7Q|o+7My-CEe+CUZF7zn|{jlmI&vbmN%;NVa4n_PCm|U#^K$I z9Yd2sA2{lZpoe->=P3y_tZrU7hpc^1@U?8m!D8^PZb`XJx(%acnGG$Ka9mK`Px6aC zyN<=_kNsdwi(g+!r7QU z!YlXS;lnbFzTG0y)Bh4(TSMJr^9@U*dU7k(KpTRuX~S1(B+mN>x6}O0G7sr1`}|q_ z?lL(sa)ZJs4Q0}iE-}M@mz=7z^xIZSAEb6`wy#c$-^1b36Xp!)bb#up*Le>^IbpVR zvX}~k;NiSJv{e~MDSQPzOLjKsttM>T6gv|C6@gFaXCf)`|WJ#}e(@vZ}T zG_MYl^Qfhf6>pyuLCKz@QD*pqdJNu@=ODy%qETQE32JuT`_Xot+MfN42y$gt! zA&pK)2)p~&h73YN7mEA&p{cJ7r@y1~yg$YKy8IgFj}aC8*_8cXI>PMr9DxLayVgWi zOK;sREKabr!H9RdOkpPvJk=-uc{L~vnB@X)Yp1*N1Y7+3`(q@Gk??;ij8}5Xfjhh4 zV(!VL{yw|^{2Xvq-#iT#{PUH!4iH2$9QXfG!!7x@{_yX{|9d1bAOB74|2^A>jG8Zc z2^kh0&ZMUG;l_#1`cC}w#f9Y=ix%CAPmxhAqp@#=PtUEv4Oz00<@f3{9mDPn*Y0sg z>*`cQPttnvv`@k0=V=S0K14>^!Zzqm8>7E?LotaRYvA_$=&u35Pd;PSqFbpj9}yDs zT2o(7#;nokkE5HzuF7i8saN?#V8*GkY=7S7c7bz9BLi&uK-_8S(5$^0)%~S>K$3DO2^~%I{pfW+A)BP>L-hNhnT{jv8QJvt;LDz$+D>=Ug~3na zQ7Oe^i^iE&GhSmm6OT>s3HxegMiC31TfX_&_MmXD*7ouAV$Z|`YT`fqcI=^^ozUBu za%au+LHZYSSA|T5cT03J7ikD;Dgy|r>=Z_FK$;QtUI%7IVR|y zwBWgy>jAU<v&?awSL5n-JOe*LI_RxR_DekHdkPOa-h{xRA>` z2e*SKa*rDICeThzbkx5uipGZoLtYDsLOb^@&{)HEPdU?YS9Iow&q@1XDXG8443HJ| zx6yIn%Mx@%tw3gH$JV8Yy|&ztn(S~3xHlKeI??{9|Jqx zeH#FIS-K`v1bK>Go_;CLbQ~{#^;-t}|GZ9X0VQgAh(iT-YVQ8=jE^b6T}#NDdqR`; z$5+4;pvTHGx?{llf3|$Yh z%}b6)ri5flv+2$TQNE>jIx>xULeEkq-;tBMd|#OFNrm+6ERoaFdN@XOPiy^XR%yS* z1{K*lCPduuQ1@?#kOvRo@kF(>Dqmo_5<&pm-daH6T~rxrQ9J4b~bEwBXB5@C(Q z>Rq!0r|`p_n)nQ3f3kxi?xpUq>k>?A1P`--$WV>02(x5rwoji3KZn_6G7x^bZVqs* z?IxhOo@e(&LZug&r#TzH}exRv=lyx|5eQb(wOz>7C&k1{ie zNEK7$oGK0v4wBm=RK!LayAuYaW_t-mJ+P+L^TRXO9x%|*>awU>dOwr79Knop5AenP zooVj9d~9}%#Ef4RPdR(>hi;KYSnrYpBXD<5md znVHEeD_^$)oOuj{-NN%XVjmmJgXm%Ka`pq1iYf@%0JHjp*ftYmmeX(%I!6@mi_NqaybA1B%S!zxj!A7 zzrEI1@IbFNAwmC#sK;kOoaAR=UgFb-{qc(F2|YS+ z$&kelfnK%Z`Lq6aQNO490Q9^1jqVSzcd93N?;ZCFZ>jia#N_YCUinx9Oy%Gwq2FWt zR|htaEB@g}{`xQc5Nxvf~FV`QK$LSCLTi`!;*#Dgi-2P0A z;Ge^gqv;Ls7y~?}L7ckvY83!}cIgrcD+r{dQ*O>&=jIp#a0$w(sj1Z^u@L{AM!XT- zXlYYFPvh~m+DxOG*mI-L<(O%DfzRR=Te8dKljhG1_F0F!t%l#n%o5ysu|+3w$WJxi zF$9I7GX#!>NERSuE-m_mh=%zyLUg8GupsT~H{~R>F>L!wu>ylZPgeP3z}R?BB* zs;gmB#1745jCAOk87C2#;25r7UVkPJK@{ur_x1M;pmZ=+)H|ZrGkg<&;da#~mP` z;(^14(H1t|I`s2r2hA9Ckq5q61>h#sF46DdVP%za+g-k6mgp6*vp&=eE|v5tb1u*B zY?4x7RgtW<0%d!7x^>28C$9ygdz)3N1eFDDOxCv^&C;z*4OH69en7|n{Q2r8e(dQn z!QEpGfcbNA&se6jPbMh*+c&~{TwGBX4Hcv1BC@}?cIn>cWqu;;yn2@{$u|Ep-P@|X z)hTjOI{{;!M>3)~t8h+(k{9w04a`30JBBn3Rtd(58NJkoZAOgaAkPrZ#qDFQZFh_om=j^=Hl@~vggTZn8Y4oIEyWR@;rRq z6?QRMCAu6j3ZFM6q*mo)b$jRC) zm9p!2neIC;y;u=5sk+U!33n*tX>-SyVp|of55tSS-F@i?)-^41E!4fpMRPEpLd0-- z@;jm?4N&|Zc1BjZ1|@o9T@QIK9q+l5`y=k*2$7vW%prPU$GI9`ADe8*G<2 z^0^v%dQ1WJc-MVw-ALmIjcS`BwuL8p)=a}&J1Y-h?dxyb=sYv@4t?xRy9Kuw1LX{> zv0Xp3XR3IxI8UrTgbMLd=qZYbF$AwCP0u?y1)<_HV8>%!-UCeV9aE3v1#PmR^xW_y zN4J+b)l+SvL)gg}K+gkDR48;tb1hUE2Zzt^{AE**u7~+AAjepo$W;^+hKv?Rd@V(L znE$@!t~@&n!~sP{bO4%y_vlrvN-rNk*-7+ssAKp2{MF25^xaXvrlg2FOQ|D50Mp;0 zKBHmrp)Pj+^*RCOb&j$BZhyY^b5g`L)7f`k{aH$umAKmdK$G^8#aA}pkKEcA7iyAm zT6ZA)N`8x4ZYy{c4t?-%mW3^Ly|G+dzb zeK96@D8(-05R*v0AnAF|yk(V#o!t&ZgM&vfT`ZAzryqCn9BPpCcUmV!mGacGUvEgb z#l`^ntcRWFFxhz~Qw26~#c^rIMD;kz?uaqEDaHwOg$3?(<<`l0%|(qYR#O?;LnnB* zFmMPn?Pt zIbMbV(u%XL2wiX5?&-s7)lXs@8_?&Vtk$KP(=MI3BZd^lQQX36C%>YIqyyl$jW`MFNh5xXhsKUlfv-m(D6JDlnA-LIQ6|`h#sx=Jd zwA7KVBAUne1suNJBP3b4-$$XNDY%a@R%w@+7qg$xf+43j0Y;Z9Z0!_$dh4|SYHx4<8NdHU(fxVC()60v&BEq}@k&eD(aGsv&_S=*fF3Z z#O`bnk(nsYBYbzAdeO~tk1C~j+N%MER*uc8Q*SjtQ~73yMlk<}n~@HWB!{oBRR1H6 z&bm*C;d1*~7boty0JMXLf;*W`&ovT6zShgir|{f3Vb3gtg}HYU`ssd{U+l1Df4~rK zLTr-R@J*2gNPCTR)&S``T5c~x^;GAk@WTClw^pl>iKl#b>G_J1N$T_WdfMDX*C(q7 zK=m7krBjx}&j;Mtg6Vh_yYGpit~UF11OPl@>xaJH48bUKAV5qrH;mN2@|bx}JHs&v z8hFP8s6*`D-MvqUOkMGP1^Q^E-0=x06y6zAQ0BaIgYcE~%f3VH67wS~XVJO?hC4E! z8CZP$8Ch3u-sH6pE_S=+d%)ZUT;tTcJNa!@4M2Lwhp9XFcykY@Uu@cKcz#cs!geON z8?L)HPR-ld#l6Ti>tDzfj@fjn-rm(+7~Q(}RM>fq34=oz#inAUvG+T&Zr-#%?6Yej z=eaZ)vxeCRL+U4pVzUz;wBig(s;}6LOh-^^>8y;_CwipTEyXOqV2v@fOZCVEkr!y8 zBoMqS0n_kd3#bb%?<`8m2E^g+fLf)rEX77qAU4g7@_ds4-6lMLP-=S3haEdDAynJ?6b=}jHI z`Z@`M)am;R9cYYJxRS|)&|JN66AS~SjU1b&j|Kc^z`r29wt&^6ApCuk58X zbREPHIH43sOQ-?u?(ViFba;E_r(5O1%0}%q$I2QjR@(-A2}Rz*HP3-%@|#HN5k1dX zlGk-*!d0Z_=@JM_ESTmRcDAd-u1<5bjEPeAAkeqe*`jf| zp@+!eByVk16^NW6D;7HT+N;J82r#PK*@dr7G&GDikJD1QpNmL6Yv{Irs4h_X%KR;e zgkPw1V@*8MLRmR8W?;Y*v}GkK;r`HcE}O{mE9=7nXRF656HDR6@RfMjQ$f^V9<%|u z6uvg12{#u(-o~`9zYIW!7YlxjU9^`xz+6m9w@mIvQMqy7yVu_Rs?B*(a;|Jl;Hk)C z*c;g^2TKBbwfXeJyLZMYwz+jC3Y^jk%H79oJU2*c zlOdbx07GYpK6#J0ka*SJazxJ{;*~hsQ@;W=$}D)a9ZGk=)>*2sPpz&=dIDeVPn)}Y2L14Wwj$%^eJT1-!fHX7b~>W zI#^~gARoU^MKY0Dci2pUwe9k1JAhxS0KZ332vFtY49M1vEG9;Yjd#66>#SRjXv@Iw zAt-DJm378DJU^CaWS#eGTV?8ifc{4%u~*w9g4PL(>lUmn6d1F|C_-? z(Ylb@{LSiYRF`f=RW&1d7T-v!wduIzH-fg+0Hr0HHZpEj)=cYqDf~||0f%w3lN;|ze{X)~Z*2Q(&Z`shU zkEp*|K_*tjHO_+{>?Sq9xj+_?sqZp9=G{X5vt>?o{OvZ_xdRHtYx^LP2OqoNj+i!v z^=Gzw3eT7OsSQDNIfbL1OmZj+N}7W}^eP176SC;sP_Nud=Uj#r?`Z0vwXWeW z3L;$*i0R$caHdQ8hm|U&knl?bVrb~5?Xk6W#yjW2sE3chlUnqpVuPz}Awq>}8>YmG zX&lr%Lq@IHNe=JJxC{qd+$2BLrR>EW{`AG$gPuDJB%mm4r8vdE3XT4-;5F@$a~DVOt;2Qdt~(_iHmQZ zu^!xmN7fUUXUqHP;kT2QZIJ@j=+~ApVzQHY&9UI`)*QAS(58P6@MX&2vwX6fH zYG!i@_aqNo*}_#3kz#L7dNKb#m?LrgOUl=lrSqjkXWsW9f-0x4>rTRd3@8__hrZpe z)kn++Ga&1K%*I!5Pc@wvz{L7?rMCEGRDuWY>{#@ORH#li?Z>^3(;_Oy56AO?xw&-I zf1qjd;52Rqu|Xd8FbxruLbGm#HIL`{%hBnH)8&ST z&3LV89+rOQvHAB>^-2%l?s=LQYtGqG+`=Q)RsGuT>f@mAOLAp1Y#RC5_$rxX2h1&O zU3>xPlAv|I2TlMyo*!7T<^Uqo@0-Coc{w#)Af@!lZpyJEt4St`&OBgU)XteAe!Svo zzmvli1ugOmy3Ey{6BS!o=hb=4Rn6*FDb2~hS{U{Ws(*I;_+|I6WoUCa_%Q?p3$Bsa z>NoA2());Ye6&p!QUB$_&CWLEi3%bjq8izDC+%7X?G(r1$6FKS^|L?TS5P@Fn(6NM zOEi-S-#faS*xV9g6(X6{X^Ex7gQ7fN{u4e`lT?a*)S;$SZW8D!uOPWUXr+ z-^iS;b`8_#hU^q{Nm*z&rSnBDN-MIVgZjUAGD+07(RyIJ!`RaLzB!BfxygzexC6Rq zC>wml<8EvLFOdr|4VvLQMtoAQ`c}BKHxpI?bSw6_8a} zN$SDljsRSJ$@KEvI#s(cmp_CSi>uSr?A(QXkIX@qSg^eJ*M##~{2VvOXu=FfiMeJ z)$Iwv+ozxb_(0R$WGxDY!o?t>jOOCd2iQ&ZnCj^mjWulLG&VUty;y2{$@)UMZ&u7_ z&`e!>Nm7^(IqhuIfCWvXzUx?vx&yedrI;##OGN}-d$`#Hv9EiU@Bgu2&kDXQrb)uv z2HMJ~-QUHozDToaby`_Xgm>^Ra)gZ2%FuS`t=hQsh8?Pu_Ys+p4G4Z&Dt$Tks#;vr z&q;FG!Kc3@fTEVmgnyz9zg=4a4kw{d8C0);Ijk(~)}e!8jzfJMi}?d8P9QU@mgkmG zE@9(3C7y;I3Fn>egDmIKUgoVe5@<&@@=m)XW`^EDV-s7TALxz4R`YNu@Lk569Ktsz zUg``p5h@E==6@DhdTNm=nQ!7X|?a8oU@zX@=IAQ{(fF|;tl1otHiyQX3pSQq$7 zNp_OE+|d?>c(V5E?vt3PJZKf)ednU106W%0T$3P8ql5xh6-kr`9U0&J?t@vyNw_y6 zaJL@aFIUflBXA4T#7D^Ws9}~Pf45)(ND)zRL;b$MK2E72oTO(2J2}wPiF7{FUu(|q zs!1Gz)KB~8YpISaiedbqQ9x17j)ElBg_vi80-Ws`P~SiEFOSa-wFbDu7aqdAM3pn}O-k41SZ zlZug~aQz+97geu};Qfmnddn8@G0mC$^W+w#g3iOkT;qW)-KCP)3hPmHDkbe!9Z*7V zgr!A;1KC$#Im({T_TxQK;5WC)vR)zH0>RuU3Jy(}0Uwf2wah6FihjvUtZUmy`L#GH z_2^Zei>?hdESNH>RiS!$EBX68-iwJFPy2b8|DaC=8WWT3S@xAI@dyFJM<)4)^toOp zmJYo8dbK=t0M+nF`N$Qe&ugzty7Dw1juFA(sx?t&1v{8*(pZDcya&3u>(1+`Wj#$I zx{G7)<*>Hdnjsy5A&YQb@@p*mhX-aGSicqJwI+&Mh!lw1}dJ>%)#+ zaf78b%soL5^Z1B%XRGHdSmunk&-J^#CFK=pe=V=3 zzYuJhXk6^*$U~e|In7P%W^CFV>Tdbb%&`34Fw5(mFquIuU*80Q8;{Dg$opnr)#I?} zi&LB%qkd@2_YZJu=b*jhb??DQpTuT|NO74N6;UH)cpn03EE z*u#qkIhp;xDvNrb2a*y;-|;YHAJ>fd7XGe_LaLWqLGy$CX|(9E8*5eLUFv`%qO0#% zK37px%~6fCf+v7{))VL1-<~b;E`lBN{IQQ*|8(Kh|MkzSl=FZmi{_onO<=tL@9%(T zyg8h~;C3ii7e3wD0MV<0WBStxH9 za(?>cA;1`~>0f%1nW=Ss7Mi66yOnT4bqwfQYZoG0x^r#soUDL&K1pvn?=bWV{3HSR zDwVA4N0+D8bLGr=yt$HrUQCIvf4ZM?mE;AsUeDm<8YOT`V3+L@i8j`tr*^ z_RPPY08pTi9EaLgb(H;%HvV(O-gimvI|z3)zBtL}EoK8IU=((T*Iil>ezP>j@A@zL zW$)4J0DM$u*>v+Cfc%~KAdzfHnlc>^@!h}}zf%f-rUQb@iNW=}GN;cm&ml1BCq;L> zo&Gg0RdS$H=y%=B|2ynh5WwEsCMYsNCqGXIR$*B7+xgRJxjfqw2DhMf78?F^93Q;62O3^i!R??2l&uv$Bzr|GpgBWTd+^MK1~D9r%6j)6!oiV%DlFFzt*w*luavPDGI~KJ zWM?iT_qp-gLzolQci8pl_T@SHOiTZSoE#qga>@E6*fVYrtl>7RuhvY;fj|wbx^+$1nHWMuK4SHMv&y9YZVAo}sQb97O z!WEKYfZ9$~jDYpv>od<8&uec)4Q}*K(!gG=@R~Hw+8`Od`9x+*QS>KdgB;WF&pgfLq`{I zyfdT%!B@qZ3W>T!w_ms}d@@^D7n`9OL1w(Y@GY%zUIoO)79><}pn@#DXKB)PpjW*q zBlA7f3rqm-2RK7a%rZM}JvY%|3cnn>!#9&`>*{`Ku|Opn z;5CiclYMMw>n?(=QB&R5SEd#x7tEh*H+~R z8B(!BZTb0A?idRBJjx)GAJ6m`Z{td~W=kKMOgLi~mW}8a=iBoZV2RjFnUU990}B2% zMu?Ew`wGK0-4W!XhTO?2SPf{;g=*vx&%$Jcdnd{22tbBJ;*$+2eIk1JDl>n?UQSs) z)C;65ocCJCKk9wcHCN1l=>w%o4t93OI+nIKuzVT#57+n%)nxZQ-?0Zp&I+2&DNoIV zTO${4Sd10sdF*Wx3LztTDbU;W1Q(v_r<9ewFmOXERc}#M%HXvAj&<1pVpq~fAOz+> zEqFr0V9_^`9zv!yhq!wGg;oUQ&b{`-fQ7bm-sU8cZ?5cq8|Y~dh{{_;L8>2&`#*k( zBZ;wzI1AEkPZ3r3hwm=;zwjD!{E(5cciu;2@5kk_)Vga>5t|f%ZE9rN7O@BoQ4Djp zxye$;f4h`Bl*qTG#1+A?-zViW$iM;GfQm81gdoOc8xSYD+imZHyukp`)a>|M--^!F$uOA8(e|od zvG}$hp7yGauX`vwpg>T^6jQs&N zrJ%CJ_{GAv5v%}b5dFxwwGk2^PU5GgyX%gj3fQmT@4HqljVJ|kTA_8vc(Z?ur$|m* zH;p_GtH9rJtjBz^uuJocp-P(8MBEK$Ge~5?yCP|;SK^*of8T0ACk)$+aZx2s_kg?r zGHh$HymaTvrII&D<(r@%&LDPvJ_!&_L3gLu7DqK)MX+l1A}zRg8Yk`q@M`k;7EHrL8*pibXC3&8k4G|uPQwZNl7|HLd^##!qBLYWAx}A$oHNOs zQ6r_k4kMOErm+<>Zl5SAqpDD1ab*(gHJgv9wsgiaLGm%s{cg0!4tbYozDnK~dp^H? zS6d2?knp51<_6q=*rG2RL#P=Ll?W7yRfi4`d4$|4A|W7^e~Fr|D>2513Lz&IO!ji- zX$Gt9OO9_%Nr2MdDn~PTTl#p_^@Mi`ilN{MXIVM9w}ZiwEf-w$nE-~0IKa{0Y-w!V zu{;==;Zt*4AUT=q#z5WkmRsEI?HBtCm7)>6W>SVu9O}B?ml+V(Erc(-LU$ir*38#5 z2Z#)+1Mb_I}bTNl)^=vwkTfmE+ep7lmaHn__8 zlM=_+E3Z-1d?I1TV$7f2`>QN<3gA%tPDx6})k{)fJT$InF;i2VL9~xs&rY9wO;V=0 zR0!JdSMvA62I^bWzNxp0;<=zSSTVsH$EQqU6%ies(Z>13A-|Ux1bd+sG;B0du@^*| z>}Atr*FM4!KEC-}zLUyu->I3KUO92uF9+Lsug&2)_DgPLtTQ3xDVNpZCJ%&1szs^< z@t9xsVpHU)?=B##Rqy(->lNdVN!#OOp= z@DLB2gUQNGM@SX8VNV~Rno5aO(sgc0cDs3_k6rr z9abWc6ho?+Pi30c&GA?4v%bJ_f>IfS1lH`8$q>(SOcGHPNwhlWo$x}*kO+t5j5n*7 zLBU-%)Ot-sL98|R1H|aiHkRA;IyW@(!KJCK!|4Nt`DB*x`i!(9&~|`hlqN?zGjpM@ zmp9RyGGs0drw%o(o&@wsCKdY02KyL5Y83->{Ep$R32-(fm47(+8n&BNHV4A3Og$wvP99uL7L^(EKMm{Vq%$ZegXq z@Um9>(Q&a3*aX={sh zToH2WNhF?W)fH^cKoPQRuvQsS-q`qjFQxfKF8XnSXmx~>@HtEWC>}>GXBPSZuw((3 zIA7juiGsf~JQDM=%#(ArY&QnAE zSh>wSjN6IfXl`a$x2&p4@tndD9T~jS{vS>Dn3w131$^-VCij!)iE#@U*Z1vgi7m!N z1`0;4KnoSCsz$mSrt9GaTOC*uem%~xvo*Jh(SKP^dE4{w&@F?FVTaa70I_z zNPf2jCR(hc^^xmWW0&BHuPoL&rx5?V@23#S`1@u#dTs8Onjre-igNpoZh3p}q^Kid z##FRj!UOlji4hI8q*C)z5$w`oBQsb*7d=Lqo?AHjWnjOi=yUmy#za#2%ZU7eq4EN) zM8lpbrgILNNCB?WCJkOKsxM<194~r$qvDa1R$*}%QGM=tHmFmcwQk~b^ki30K!GX< z@M+TJ+dt7Mu9y&gQl08ve=Cy99{G@%L>-rzGdazG$g$TqnsBavZ3+?-w4Rt>Ewe9) z%6jJS?b*&F_D6u+ou#FgiV(=u3BqR z{?6xxUer=)QRrFcK?NJ$!S_;pE@(?34Vlho)(PnrvX)x#qIsZG>??D$J+7x+<^eM? z&8n%s(6$`VX6)ci!7==>hf(7E<2#W(ZDy&7XbF!!Uv2JXn%ux*tvC&}pCnho$@9`q zVGt0me9{g7luC9ybm^+kMEhemFUYWZ65eI1&)Pd0J`)Ee>9ex+PUEV#n~JJu2&FIc zpMf|oE;SrtIVwjxtJ%~OBl?!)JJ<}dbr0i?vK)8T-vqX;*{4pG$6R)-BLPo;f7{a)Idh19j#T>VUgz+d ztr2p}*aJRa%6>iQxg#`yA^B)JNPs-EAPu)eYoDn-aJhP0vrX8L8KT`4 zA2EQYsdONjAe2`!u4+y_%|)Mm=1qu;to#WryMKYo@NJNY!1~c&u{oe0_qKi~zuU?Z z1d&Wbov@&qmw~?E38yvtz)zza<|M7}sW^%gr#yUX64AnCs{M=-8oF=4lWaRI7orKR z3g^a(yw&}MCzd$sr&jz}1^cUMLxgL6^1kkJ49@m7gm7IdRjVxBC+6C*9Ga|r^qDAe z0B(K5G9=Luf{dcpS?g(BnXv+b$|fq_aGon~%L17-2UJ?m!2OIS?3HM+8eU{mVtbou z6ceVnr77$6rvRVg+5Cou_3jwg@rQ$y78X;tItvs%@3v67X_tD%z|OPyo8P6)8ZC}l zT^$l&%peu2WO6P}dY)7%*@FvrFAJ%hnE45p{zV;U&HQAZIimQ4>50$5`y5daX&pELVnXy9HITB&L^b(o zEi9Mx1TV{B^d2V+@(|G#8~L36cZldOXvF(mFwWuv=`7s|2;f}DfAG)h|EcUm6IJO!}XNq3swj{ka6?*?Hjq+I9HrGTkV$*vMA7*x!|5xH3I zM`~a25h)=L`@=KU0~J>W3qReyJ^GL$Gb7_gbm5WmFnE%SNUEj((L?nbXKQvqu!nu? zh}Rg0))1FcT7_4NYTwCbm{5{toGz~ET1D@t{ZwY&^}RAi?`acy4$ zP|`W<#&0ImE6==d+6#8{dV|k7iUMYd1#po!w?4QxP;}o$B{d`KcKgNjn(w0z4{A^a z070?BbbOISeO$S+PNn(n6&l&~?^7s)$4*b#Naqe@-Bwfa7L_koG=z|&>sP_X862fW z#hl)Y$t*~D&y(pE&mbbhLHlHr&!T_rjWYxc*Y>P6y`qMLLFvf=gJ2WL2H^1Z@Nhp= z6UtZNz?5@9#SJ{1Q>~`zHNAG5n=@@fmcqjm?!p`=db0AvVs^^cq%G`*Kh+zAtCOiV zU;iX*>Ab$~F_sG-zRMz|W4;l)=UF2=Y)fZwn%n--2AGJw`uZ}m6C_SCB_-$f+D&b! zG{>$@0~goAluAr{Jx*%9y4?^OWl=iP86-w?Plg;y{c|$oZB2r`n$1{grD_RH=7)lj zBoBP);#OZqREf4vhcdWy@$fm8&`&-)^T17U8neK+uib-=o~Q&hdTMz5W~3^DI>=B zrIelG^z7SJ<>dOTKN)pAPUeo%oYv(WG33`1uC%#zJsZM-5fxC^nO-9i00S0Fe#Ag9 zf*D}+e4u`=D})Uio{H0jx!y{4!_kt`U01Q&tfUiFO-v0c3$-v zhuudftMu5OU9J-h341{*I)inn7TMStvBNVeUmm!XbV?oYQ5{%wt-1#ZC#C9Kq#K*W zNtXH@L|cD`KUkT*Cv$`%_Q~|LH0=E=doJDgop$V;JC_F41DaXl5(U;3;nJ~AMTmDtGbp-GNN@;peRUW zvqP}NQll|^+#6awRPUO9e_|H0vZ!mMLnQ(j>Tj86eR{mOViNepseD(~C|$>5442(R7~YK4 zf}te%LsnKHIc|{qJ9qOuWi|`d(J!u2C~5a;%B*{LIl};=XexnmCxGEKtPZd_k~x%|54w#+r=_Wll{tp+waYti zE1r*ep}+PzkPKajNeSaOXV<4<_^ntI1965Tp=8kCLc)>hWpi-IP}2J&yV~ygJ;6k* zZ9w6D@Dt-a%GmK{9!xynn(+mwC;(~GC99p9mP=EqeHpOOKSWV$^{6iARFc?6QV~$rMQXFZGqdc zs&6q#;{<|VS$Ezp?{A*cb};P{emUZ&afg3DL9@>gBwds35AE?|Mgdp|a($q#T36Qe zK#+D30I?9ZiTY|RYlj)wdv@f|%Ru~)3x*`hp);}x?YrYELvkYH% zyK~SiikGIO8UHdoT)H(u3R|c^1e0BMPZBWm$*^>}IjZ5#NojHa4f?!)kazF%p7Iy| zK2#JjvL(YSJS!IWXl(CP5gBjoQY_o9I`uz;TkKF-x=Xv2aNj6p*`S;GJyW4&_?;8SCh@e3X79vfBgkplS0YxAny#)gz(u5G{fxHh(DBoUQ-;ek2 zYkobM%*?q@nRCvZnIw{HCp#kBqRj*juQ0WJ(x&MND#8@nF0*sUi%VHfVzPceyMp-@ zaP@O#(YdQsINF;C)JBV!>-M|qsqc1j#=x1%!7)2%9wX-P>}N_7Cb0-`4A<*vwkx*& z(Jo!JENBlzu>7O#rY+iKDg7PM$NF7Qru8Mxw#%U%d2Sy2I%p@CXm@noOgXyDOH5})YgvB5-kUAabClGz2a4t|J-;) zxmYj{ZBI;+s~RpZy6i)}oCRbGS`)J<#L>6|?)wGlVs@(WTd59kIC);eP@hn|VhwIB zaDWli=X$WKgQD6yfAM>iSy-S3tlYe$_E!wa>tGvKd5@98q~=VJ9w$e+z#ocTA1}p- zXujtfGMS@eg7Hs(%M2E)&DIH^PmuOl2a6E)3!p(TTwF?2VJID@;03u|c#pTt06HGwNMOrbq~h}K#O37ziw z)r<-*(G?2Sfklu0YWDU$8;^uR?a6Q;k?_xB5X+*zg(blmI>!-G#@i8-( z3?ur+AWrYvCI ze_A#OY(5ysGc(5(c|~2?nwRa>HD@BX(5=x}(?^V6kPko~Of2c)f$dJ)4^8orIneOX zo6`T9cVnn;T4W9-xlKoPn^->!>%K)Z%xmK>#+wjc83QNOSx%`3wz_Ns+kITslA477n&<9{;d*Ve2g>@8Wd~;HwBKc zV&`Q2hV*fF>(iFQEBQ>v-eo{mJj`>tqEs&L+!p^^q5_nDo9SMCabOc{TWKh>TF=u* zGs{mg24@QJoDRYv`59xTi~)G7ZE70Wz&_=@3_hJb04|y=SPyLq#8KeEv5~j;OggGa z4N{qSaLWHNvU~Nui^M4gW;pP*wCZy{**BsY!UPnUddZ*_#FuEeX)Cp~4ejNp7?&Qx zD;Y;l9cDDwY&7R4b{TFmH6_R6m%gwZ8+NF-68N#i{NQV4aSVNwJ_C%;m|uiJ^6n8H zjh5S<+&3MIy>yS;xNkKBLTYidE_%2*s>d=1TiO=Im;zuPahAtM_x_X13?7;7WUmfs zzt-#F0uP2tI^gbTIaI;49BM*T9dQ2A&%v*SD>N{5U%EVcNF*h{mnh=&l4&O)TSPx- z{lcUzZd4o9t4M#|qDKO5vW$tZg@i0B(;U9e*VBUr2E^kZ&5TSn9ASNG9Zn5;ofk^{ z{MRk|_veLftVox6ucok*Z!&TIufm50*58}MwFjVdDRN8o5D01?rj@97i0rr+hw3c= z6YTyt?|1C`a5f>_#>s)pq#+)eC~Jo7EH5v8QFCZzjnE2yo+bYjq8_s+BMX><{IL@%YF764Gk#ygTv?ljPR$NaePUx1Udv+`u(n6(4Wk z!!ww=t_3Szpc;^dYE~&}nu?AO4HKFrc)E>&TI3fx8Z3IP?|#403~d{qZl)FCv4?%D zr?2p$=1bDgQnA0Rk&i!0GDYUA9)C@VQ_^99m03~SMEStoR53qsQ_1JgpWo+`HM?J4 z-tPXes_R@m2zGs@U+#X!H$V04r!FsTIE5cKuQ}n_M8jNe-mP5H135*sg5eg@uSB#+2&ibPAObvoEVmi1( z8k(AQR>1t8X$f&u+)0RVF0xqyB6`T`sfFqZlzeoDwGrxVq06idK3-oWUl3^ry>07y z8h7g#E$yQ`!TcOHPIm0ULxI*`=g|qJwwg!VINujiIf)tyd<~rWc9}Vp@R;0Wx0A`P z;qH-|vGhjiZsWgIJ13W3+L^dJiGgdBj0Ui%&P24!bkj6>As{#ziFWq4@mxA16E%|X z;)#KlWY)`!D=>lV5h&l7a!b1|&YHPU^+xP#umTeyH+NLh*+J>xNGFN{JDVHA4_p>t zFboHl&?3V9#HOlS;Rv+>`F%CW;fZp>Twzr>ubPi<(c!%#RzhlA(HOIa&ib+_kgkWF zJC{1weBq*{g5~~O&RZ?i`k>vghhR&AQzcU7oHvFDGk~J|yj-KmTilsh4*ia;$ z;K2)1)1%tM&tm)QSsLyCblcN2pusLbxvx=5HeST{$&xDj&`4*B=sxQQ! z{QM|zG6%i<@`x`J}z z-rtEcA=IQiCK~GEs_d%N8#>9Z9jRkU*fMy99_)0+}iC$#*^LIdaOf@el`u!hobwiPdS`hNVn~n1L%JA3>p|r%B z6-9YeyCkhjNhW!O@V{xiTF8kh>~2QxzH(wMo3yr*8Q zU?)=NW`?x_16`rsHPaZH?wbN_%7cj>^5@BeG2a9F6tg@+`+;p=%@Y+;m4kz&-OOYC z4@nwE@1aHYJ{@&yl&*0~PrcS;LK)|=k)qk_9Li>gN^ZsC_lTY5 zNX_#Jf=SYh_;SY%2=(I&X}X(6se(4(jCGYb;+NuRa{|XKlV?jHPKczV{nAk7g|tHU zG5$s(U4%6@u+PRv5QX0NhJp<}GZG0^``WLLy~T>GyXu`X1<}oSHQ*FV2eV$!#%Bdn z*Ie*fT!+%f`fq}3%5@f`NG}}rBltK2Q$Y$Aj%bjOGipMkK3N(4YObnwPCFYE6QTnX z)2(L%nYk*{>Y^>O8BwA27r-|fi+J`iLedmd;5!hzNQ`wcAySFeQU~eWJkd`6^nEqd z+fpBwj)N6*9be3%dS#7zsAY-K9Dz!)S0a#HB` zZQH@TJ?tX@qdm0n!Vfq$0pyhIgYo(S_?rNXn|hW~yNGOY{8L>!CihcayKU#^>e}&Y z1`VAHa0e1^Dt8skc3c`N?YyO)^!3%)!y*U)X$)snKf#9ADupT zFsfc>1|)!^HPWM3XR0x`mA;q&g{f=KkU%{L*uq&C$ns^IyI-0;;X@?-kCANgsF`1F+Ul?+A_q~gZ}UjO)k zOwuF;67wM0zLUkvxV9x)YGH{PEAc1CZ}m^|MN)^EKe3Y0Jr_R^*6!}`b(zwqN3 zG_rVVs!<6RzEaFh^T5v0z(a)vTFStjT*rC*)Ym`6M@ZJt<|2^!5exa;7S9y-XM)Ov z!ztszNN)HiIGNUs?@GRzwtowR3F0{P_uDxQxtH@lDArm5#I5rrJqxAI)sQ$MD^ zph-rW;{;nWwD^)!Qn>D@?FTk8pZ;RWIumhBcf}shn=Fp?Yq*Y|GnqL^6q}5pPY%uw zIR@#HE&y4TXl`v+mq*BaCzVWTz{%43l<}*z*_i>q2V{*pWUmd3)y3$~<-54Ga-(I< zoBbcCZs-07SX|#>q4P!|6Jmbc=Ifv1#j$fuhHmial86ylPfrhM_J9~r4M03Q+yh&v zgb-K5)fGtaF;xCqV~H*wKZ!~`mO;_4&|&Jm7-@iJ9v)N8(pijdO>b9zSp}h7!}^b2 zMmL#8GYlXhs_G^z_^uXY zvk9z!6MtK8jSC&*EeW6Ht}7TealIeg-PZH3UCM%D>5P5;BAj*OWaAS*$MXpCWAg18 z`XFSOGQ)u$T1zfnj_ZC)5OCuBz)b>&bChqXbZe6Yp#mGKbWIDy9t0N~r-o)-u6cxv z!)iR8a}BVuKEC*v+|_T>wN0kKGNiyjaOW&gkcc~zCcWFIZ$^HE6~|I+y%nb6^R7%_ zD-izw9i<2%w2)9j6_j2i zkPt#>(nIf^yF6#>b|3xj{q;Tf&+!K?Sjk#*jydKi?|8?2sjezdNzOoi;=~C`MTPqs zCr*%%oH%icjO;YX$HexV>s} z`uTaQL9_7M?S}|5@{sqH1p@_8_z)TmI`kYIM;+mY_MD*F1fAw!9?~+A))C+6=g+T$ zTd%dP?;DLbzm9u*-=+KNu3_;_s>#&-VT^k{m0AXF`4Cye&7qmnOD%KaB=K)QGKa~C zlTx^+W$gthyNb3T)k3^q$u*x2OW+N|j^d;@CBN zlHa}K%({A2BTx1D*;$N180F>b37@j2UTkP7lKqWe??R=n8HmDV5__3JIP_lSY+Sg*|Z=$RMxNsHrJ3=4p-In@g?JxzL z`L?z$3tr`ghP66Uslu9ls5=&&n@#YLTiFUXtb5YpwD+($$IB+!--@SYFJHdwGI77~ z%3ZG=gA8FIA?=V5n3c7GsU1`!N9nWFG8+}%WT9u}r#>{W5M^{8JX%6pmtl$)8<%4( zxu;&TNKb6~JMj;lnmOw~W(eZtn8jsOf#A-b{EzPa(D@>v;0xg)_86y_oDSVKrnnoe zP?|4_30w)V;#%{4M4xEco0`KBQlCzxx&Ev)DZInAW!1w$CRbeZDnpE=HWhR43CDqC zcMaG>_k4}2GDu&`H0Fdk%9Y zY~D%txF+0<2M<}JgvRXE>(kH9kLrRRz+0uPFzK~r0pI9eMyvHJHIVm}D`syHP|EWI zP`2f{ohNEvkmk}mUSV3rIx06_v!oniQv((6x`T8IL2TjCSKQ4^D)^Xk)QXuexJ@ky zaB%1?NV$Gl=$UH$`fce0uUlsm+@;Tv8Jd%w-TpqO!*aRqRBU|w^?^@KIS?r^mce?p zEpMrGDI61_s!_NpSA_xT>NHYq#AjS{U4=m;_NB-uhc-m(X-nTvqY+4ESnZbI>&TyH z!nbi)<772|XlN^pG=Y&;S$C%<&>q5cx{@{Eb_omnCGR)ay97;LIE|e#T7}Hw}Kya)kf0Sf;;!XQGRQU298~l5Y+=51%Fjc zq0=(+1qOzBIhZB4PDc-O;C*qsTefCn>&M%%<-Milwy1WJr+2J--V9V9jowi^YTood zIv^1_#2z_jWmb6X%-;5%OAwhXE6N*G?QxyoxV*c*ejJZGkrX zk#e-yB9UCjStkb9S|rx*aFO!~M%b=d-8LJQ!+ZCjDI-0pC159pInMo=w+zOJBcpoj;3s}#DGmYjT}xaKIzu+QFmC)l@^G70lT+`&OKUULH?__L9D0b7+RE@0l< ztMN4}6WkYasNCv(dzc*c8PxVSmyfP93Oii%6%cnBq~4t~ALSRUfrWRPbQ-bZJQvXK zxaa!nj2JHmezVyg8$vDAbuu?B4OST}=ftv?Jw~qyc~IGJ+tCY6rf$w%!IpSKQl;D< ze)_~!y&Uf9&Oc7FXd_#h06Ht!Ye+b zzf`F@k!fmZSoDk>3aC1%2JWmJF0lzir#If+DVvAD1$_E%K%lTSc{6Y_+b z3n2ty5NxiEAOcQA-WcT?*Vcb_tN3y~+rPb&%-EU1dq%!N$uO+eI1_S9p(C*i-;u7c z@;S-G6UMpJ@5_{8wpu}dv^cVTSF79m+&+@mbv-(;b^#sE?AxT^SuAY5B?jfRHiFs&&CmfTJlULhB%E~0`4z0!eCIp}=VmF+Q83t4wu7~jR`dZbDRN=ZXEDUrR543y3=t?#%o;bMFrvnVNci>{ zsyOj1a#rH+gIH}O?$obfq}%l=>3eL)yvHXVR2)6Qi@P`H%39BkekN4JajG!NTakvK zb!&ax%gJ3}snVXo>bLfBGyH$K(J}jAuIl)<+qIZ7{j(#<_-*fG?d2(lMxUO^s49)C zv-z~;^L5L^-YF6_&vXtJ@fD+8mpD0SQ!7m0EMVB}j@DX6Ek>n>1OSl>XbRg7WZ*0G zVeZ-W+j!q$4IVTP>w%JW%#~=VtnFkom&YuF=UBuQZp`t}LoSqCrQ|noIEry4Q4wC?o%(_MpnJ|@#v0x9Z7p5 znVx+e>{}RC#Q{D)P?iTDqr-kl>jDAWs~^+=X?#?2JD-G*C`Dhe4fx9VZ2Tcb}=iG{f@ z59!})xwLWgX7hlunnM5Gq_0u~&j-K6fl* z9W{8$$hW~yl|>!r?^IcIGA(zz!DD#}iuH%^QpC9mM?x7Lwu82Ay$$;j9TLBid3jHk zVh*eXmixq+|G2gqLnz(c07uqW>*gwY`CbEyDA3VajUIT5#i_7t(J>4~S}Hi}Sf;DU zdfa^45`cFZ@f(W6nj9TGt@0keB<3|+VnA@NR@FwhlV*6&=aHy}c1vUzh(Ew+uU$mD z_0pZ4cJP2P(q5V5+CSRNz*v+l=~!P3qaz-QTdf#5Y^b6`ubs~DD6F-*7$Nv0_H8gB zqI1GFqxxN7g|7`MhZi8&qs>Pd2Rfg7vhX|l|O`S!nkQAmwEMSI-!f! zs6ZGf*Y{d#1s3ImR4nJ@rHhSXkr+~Wim=jrAxYq$K1`kQMCv2*P~It$ScEI;hnGDW zc*e_v1!3e@pYM&_hWX}APR1|n%c<=oCMWKv7i4lts%31I44{vdG5VK2*GiBh^m!O= z3~h{6j2^4|cr0^`*6yFP?afS2Ir+q1*{Pqil>~ke{Tvq6na*I;OX|(^L%UZ>WnL}||137;x8!h==lA=R)hd9dVN3UcYMa2@c>aPZ;25-eAr0M(nd(zG;(hxF#Q-R`mtcu&Bt!Mpbn4-DSg9AckpZ*rEYgd)biqLT13LvG)^jP86fAmmaQ zIStpSfX{9b<32T{No==x*@%}Zc})E(50AD!DLYn}s;zqzTTwYJj`T7c;?@KaM{f80S;5;MJfb~j zSKfT8_Gs}=Au%^ImnCS^`^b&l{}Fd6DP}fv#zI>}%xaeQkfxm-MS4acZA!D19qRMTmWCiEgx@)*y5@aATJEcz@a$S$!c!igio%_r`f{znK z!8c4(e6K`DJ!K3$%g~FFG(?1V!R^qk0eTDGHWL0@Gu7iQrXuWvAyv^*?{c;^(kN=O znZ;?EnbmPc^)&AH#C^w*&TTIG)lU-cZZF)yW+-pFs96H*CDmtDsYOl^*CZcRrO%77 z#ml8#eqwKH@YXgZX7Jf`O&1Nh6&@F0Byy>caYlbo;b(3}^dPf2E7K?l{~B9lhjg16 zoqelUl}#WW`f~MfZzEQxI#@nM=W$g5X2Ht<)l;xth&@+FH-u&2O-CE&^^F8B8&yw_ zzFFXG+iFIq)2jIsF21|tlqIz*$yx6`t$5i3->?vQ7o6Nq6D@fSf+@lnEx*UlEhYOw zD6UCr>J_nwJp#?UO?OqBmqKy6IW8}TU0a}h)EigKWew9_km+*1bv4AE>aQOps2y!Q zu8>GeYw<`&n)ltGLoX6iP!cy(T6L>t-j#th%|Qs88*0ZBEoF{}H$3mzO56nU#vT=3 z#Ja&T8r-hby*0bd(z=N}?7J?Yv6OySQ6!+vME~uwsqs(nMM<&2gN3hf%5c6}*avp2 zmzhrO&?ao8CA+mtB=W0L%?GUgmUk|d^n1pD*J)mPDdT3_3gg;MQ&7xUpp)j?MM>Yl&~u}8vVLt%St zZ~53q|Ax(&4ev}*yFL|cy;zbdm#UM(D59hRGEVu!=VZDOhiv5LjX4A==k+|v*gOhn zNm5GSZVNnG=Kp-e!}4<))MSrN;vzAL=B1>$k!+0f&%XLxpkXAO#uZpbI7!a3$wyb& z-ACf!ml@L89Tu7q!;$b*DpSc~Z{rG_1*1s_AE#z5NXRHQ_ZiNnnOO!Wa0ijN(Y8}a zIRVz4KtMm#ZN3#d`2&p@;|{jJM(a05Dotk)v7A7kv7JD1_%rMLfr@K2mfe)@qQbNp^jI3kEJ_j643pU))!@%~CugV1BnS^g_~4}MsmUq6@0 zLrzVe_}MAJUha>#sZYu^$~k*c{BR9_i%sT4a2W>xtI^eK7oGXzZ8-qvqG%lF{dKrM zzu*2IK*;nDbgFLr@izH+00$!uulRMWKL-fR`FV)ts=?Hsr_6CbNWFym%E+H*^#5CxdL|ol585)zSqSQb2Vr-sl z@ELY2)3nNcLbPgXnz6WBF~U1BIv37=sl2Y=mKG8m7zmky-f zMFFIv&R;%v=(IXe-QA^>Ulsm2gCKsA(`T7@Bk1_~T{!fz@!=S!K_2AxQk?X`^kpJh zlYo?s&>xTXI!{Jb%vFDYjb>+O*9P+Y;3PJs?VJdDe#HU10p$vs?x-IhWmV@12Ay|j zU)`9y7MQD$HRj9mC@!33$MKC?d+^_@m&3nu7+%NC((~46kLl~FpQDm_N5i4Pafy-s zCa(c)p#3+2yw4<8e;~ytubV1Y1BvQm@HeWT_$F}k^LgMqHrww$WBV#aPmSsQ+)%-> z0J_SMW!iSQbN$CF)Qw4A(UK?XUMXVyxi~*J)_?c(FQWLDc!K|H#C}zi|5dBaS{}3g zM^7U|a&7^86(>?0m#s{{LM9XZ=uB+ujjo*ShAfRLmw^*k%*H5Rs^40vqj~tZIrzD2 zuEnIrUqALfs?eygvb-#5SJ_`^EGhHw%Pkjj3#DA}XsLnm4qnZo_v0A>uja@T{?ATi zjftH2#Z>&*+wmsBfrq=7tCpiyT@G8#rs&5Rs@9}xeOe_wRbyF>4+5{(fGFoitb2t7 zT#nDi36UIrQ!V~`&;NBrJw`;(;Z0O^NTDG+y|W$f>UEJ@Cw?YiTY7D-+_O0A@xJxP z=OIT=^K^gi;~y(P<{V6E!W*YZ(?YVtaoN~7cnEH%^h{i6QtMM&jnk^{x$l)Mu|I?p z#>9N7(l^hahu79LDF^eZBRy2AoX7n#sZhn=*l0JnrPKZb{C-UJ-_IEH1s*t>f@1q& zJbuXZUsDOV$ok)}eizt(#q>ij{;LvyZSwy`l_1-+Uv~RD-uNMUAY-^53@3=R=cBBL zOWDGWZf0feny0zW^mG*A_a_HeZK|AC-#;e~S4ev?bYY;ZsvP{a(|@{SH{w;l$^Au8 zEKuE{=QDI`$;-=A_IVgaOiHTw`PIc7tH|%QW%GQ4n{toLJU;y30sxVuOWJy0&UGyE zIOuzdo(l!P5qJJ%Vd7;Ph^nAC&$d$Y z7NbE-mJuU;$rQNs=}k*?o~y-TmiWPQV@`$24M?C|dX_9DB;YX-i}`GGE>fPhu# ziL6Yg+BwP}X830gL(PF`c=MsfvZWi}m*YzMuEO-?k$<$H?Ui>Bxn&4Ye{YVq@bhrwWAna?DBR@DSf3*R_ac;z65cPxQv`?w`zp0>F;s>3H>TWCYyq zskIs(Ex!!2-0wDW!SQ~BMN4=t)d29L=Dk`p-ymTyD{TSq*{gkS8VmqU@hFBphv9PV zQ_Yoz%aNDF^=`aoOyQ^Qlxm&cPRy)omo-wmB`V7OWGHYYMyr7KhGexz_eC-vRR_&5 zq~~1W)hXLjbK4`DOpV^at&P{eNqkB#6R3#0v^dZhS(*Yhq^bMhR6k+sy1R$#EqFIW zHJDNzP;;x~Y8(P>VzT@Vj9^YL!@`j{@)bpn;>B=BBZ;{iY`#V%USqvxwndtugaOF6 z+~uLYXIs`Il(MfrvG`cxWqZD`(-|B0ZJwmEV*$!qu9~roLUv=S2_G-M6n3b3{2`1a zz4~#}-i61I#MD01m}zOiZqHp1z+%&y$V!mW!7du^-f z#;{9Ghd$f!kJ-YjBc)&Agza~%rDZ|qZL%^Q7yzXaOWQJ3{9-6A2ZDpgRc$pRxwSrK zB58CvN+@vNgCiC0B!F;%7$ied@;BomBS!xbl=Es>a#v?}k>z+oc=v5#!n5@J%7?B1 zy#?Adgu&C0C63gXw;v_wcllei~$7BKP@~qvvFHwne@+%2x!DYwDu(N>5Z_ zJp-^D#|v|pf43aexXvc@xP;3><>0wbB6CZPXn`W|tqU139|xY34y$#e-#@rhak6bp z_K>~CPP@u$H8yo%lhnb-(Z=SZ;7605L$~=}W4k#wd|=&1L5hxHta?eTqm2vUd5YmS zohZoJ7M1>?T&}l(T2eMwaSPqh_7*4M^cfHk@&w* z(fut@4U$nkNkPnO=)3TSlzqvf=F#p7jV(&N{cupV(V$|{Il(LXcqPlv87SY66_9K% zNo3knc)Uq6@l`HdTNy##L>j$tMcjgp!Bk}XG*D}4GbzR^3sMVW-QDJm?^S#eks9wo z!f{&;Fb`hG59UUQC-B12N`24+A&hO$`6^WwMqdqKM=Y1hHO4CY>|M1W_5W!XH3$#A z-TUfhbk8~FHM3BsN)`V7)v^9)>sJ1Ny^jWWdbv*CoKtN^k=tI#EMr8{BG1u7z(ho@bWzP`YqntE)uubsr?VAoYsv16o7Em2JB;%7B<4tn z0G+Ca005)KR;_aW7ShX%8JyA69@|}~pF|iqdIXzzd1>P#y+ZD8C(&DM&A775=26Pq zT(TM0ACc)5M^HWhRlIURQsctd5aZz_&s^zw64^U+loF1WCCTl zOULvrq{oy=zAIVC@PQdDHxi#5E_h77Lb-#<82N3KnW^MJ*_C%R!N6;Janp~~*t;>L zA+^hceyNB+;lV$9%GkKaH!3y!?80bE$IZHc`~*kAC(_<24yjKhZs4eRZ;^zYa@BiH z{_C{=#_E)m0lcx?LX7`6oB2JBI>{x&aVgW}Y}23N$Z`NARaQ=1|0BVWIuigoPwsub z#q+!307%7uU;NKp_rIe4r-EkwzmzNMquA*Eh_C)I!(=B@xBBlG{Kr)w8S!Lt$kV4crw(>qi9?Qd=w&mej7|HR zpSnyh=!J(%&y!0D)kD{;r>=EFV2JbK=d-} zZ+kla5i9Gx#%$%J>v!+gSm-wAQ3E!d;<+xAOHomAbnkBJe?o{SiHS77f2AS&iPmrP z{yvO&O29jmtxCFF{Nrs}eZai5YF+tBX82Xw-Qls1Kmf>;;bv8UZN$WZL>Ax_vJHUUcB%z z9{u*2=R4ZO{_9}=8i6tKX(|>rC$-&%4v8AtRwA6oLabmut2RWNYSL`aAN)Kd;F$c< z@53d2FQbKv(kKN4NO z8WiLi?8_1$CzZApHN2V@p6x*3$TzM{-ab{_sEgvWmo%~owRfJ}4NraZhBN#e%Ntk{ zU&mqeF=$={oO;Y^+{O+Nx~0i}Q+1h4>fonwO9{7WmSVrjQz@-|jtITt?NAGMCHUB4 z>o9Pk*^q~fwAX9pRTGEY_2f%f-`5n=kWXJ6_x$}?tkE*xLpUdFZfE6v7K!Wf=N6L; zmI6L@8q&~f+&zh=WP|1F$7L4>Jkn9o_aJWgMXO^)^JGNGVX|Gxwk$rlH!+@CS- zPj5nsu=b{7qpCsbE4bZN+usK?mVDtwgc#q`n^DNyfpEV@At8^`zMFtoDo$s&{)5jV zGB({@+r1{M-!u#}k1$kKrrf!Grvbt*!{N25BK8NUkh+8jCk(yV*2$25SUM1pmmmSC zyUTWhYvbRl_}3LsoVYMy&MrKyw>3p-%Vm=i)!Pol(5Acl-BnM18#+fGkt1beC%0vY zRVM0%r=t=7++0#ezQS%TSgvtxAMZN#`wO~>5k&@z@5Bx^6YL+(Vk0Y4pT-k9*lBq% zCerlLzv<-EgRzZ#;+IkZgRquXOcHb;1d?<+w6f+vV#V-gmZ|stmn!4m$CU&iynI|M z)fI&p@=vc{6OGP9mffTVZSlS2{)0fNTe+RD?Y-T&EP8jj16#j5QmQ}Xx%7B!zveLl zScVStcH6+8b{Waieq&FWiQ3)EH<#;o)w~z-XKI)`A}mdU=9(;zC8o)L3xmE}rq=&S z$$^{yOI-^mvRIE#{~a#Zc{#5Yq56f-V=94jmB zYK9csKtX87q$16A4i1h{Z;!5fy)VO@4! z-poFDg#O{VdR2tuDEsP}GuPf-OWM5i?%lhkD+yeoiua+Qz5;D!+fju3NOb$iA!+lG ze{*wl`{(TIXDBHXKtlEkS`pKIwf5GYls}0`f4h%{BtSZ!dq*n0A?4_?7P;)2tjy5+ z5lPUP^G!wEUoLli$pRn^wa)2xCI|PqY{s;Q76Ga+L+@hziy`>r%2#s%zgg3K-3rwr zM@-Qu2g=It#teUI(z(T<>TP~wrPws&R_nYFVG6(o-`#S2z2A9WGF&hBqge*r{OiqZ zMT@3F6PFrCejej$IgmWu&CFCm;Y9$S0-sU!WJpVac0udguWVBQXJ~kTYuVxrvvlD2 z1w{pgc)G3>MF2XRT$}1B^KaqPNBF_P%W#7)qh8Mhw>FXnxQ|7CUo`asqMVGe`1zGX z*EMeJ_Faxmm#nTX>65EAZ!w>YdzaDHcM+1UJQL-zo?zO$K2DX_ z)vtc*;Zof?!cX?Y%q5DeNVj=b*+yBu17&5ExM$(f?Ew1Kae*=n=|0Dy!XV$S1Jy{~ z#9jew)$bg)p5M$Y_qbiGTil-%o!y5co|k7a}k2Jv7yV;wru>~&;x*GLBqG~D)-%D zRoM?$;`GZjq7_BmK>${%h(tgwCUzK&W-S(TbVuVIBs}J=AGXT-6q2X{+#mTWpFlz_ zRda6pi08J&TyGuJw$MB7<@t%JLtMB;?sRs8Um`mIMz4YU&Ol}?|bIc zG4ma-_p7;!JFg{>b0Q zD%>ugq0gv)UQ*(Q2KNe=iCBieC-gl?fuHhBG0SZ&u6eg75aCWt5pm7P2-11{y+lo} z>Tws=&X&5`e}+%@q75pUmv*?sI|V}RH+xhHAhSnRQ6f|jhSw3%o~r&nn=#!?xEV%r z{y$jUoU08F**bHI=t7R#|7fVUhBySAC!ot846Qpql>Lvt>}jI|&F(WDt6eY0hzB_FZ^_ zn1L%LBWx{&@W@Yw+1l6M|-9_ zJ&W~Y_EtCa9Uqw&vAj8a_l%SZ$=Q%5xKQrgC)e}6Rjcuyd|7m<&=JF&*WwU8k0BRw z;}B-uqX+^eOMJ=o@?1vi=dBBO9d%Y5DklfXBOJ4|NpuQQFWNCnx%8`Cb{s8=#Go)Q zKQ0mtX)=)E+^l}3I{dYv#<)*%kwkDZy&te@Y}|!uL5Mpq(}6;Pn!~HUdMBrscG%&~ zCxjt5Dw0EPV_iqK!eUP8vZPDp@Kap#g6q(2wZr-CvWI!~Q7Ce;v`nts3^9k3@}r&! z9wW)xte_J%7X#`1(nQbC|7nz!}6BLZK}-W(dr>;$PRhZ6&&F_=k5S1;OP~g=u20I#5P?bx>^s3MBMi`yt3i@!sEl=f{;F5Gv?B zvgJ=1wn}TvO|_d!&C{p0Nr^s?hph5k0a*4rDYl^_y@b=%_rSGPmy9Bo(Jw{X1fX1< z*m*mz1S7FrCQz@H#57%hVPE&Bn-$2#&&ow|d}8ca2V=9~1n91{#PAS-j%;WZ=oEN; z`myiU5@x=}J2o|Sg6mk0`*}5WFKXe{dk~`aVx~4!0}Ew&n;JWTZtcKZS9kKkXx8OYdFJ@jn*_@YDWDyR;#k~sfz`o1 z8Llmp3{!cBa3_Hh^%iv{Yo8o}pz?h;XK%>E$@!(E9v9AkMPE3Jv2DNSCca!*`XR?| zfXh4lII`Zr+1CEdQ1m;aO0UOETuRI7c#Cq?w5{hs2t*c1GTkNme(+G$VM>|#seJ6V zkz5Uin9IJ0DPhTKl4)=D!3THTGFfvprj zWrMzkXv$w>6zU&Uo1K9ummgs*-AL)5Los&s07YL$)n3RznX%pzkYL2${`7FSN2|jFLlpeCFp|&H*z=93D2|R3x=qpy^znmHT zq}xh6M3}uoH+pGpC`GeulpJy?7rDFSA+a~41y+Lv^%Ch*40qykt#C6A@?-qde9l&A zwWleeNn*HoDygrgWs$>y%V~~G_tYTok>@io>Szzit-`ps75u)Veo7ZqrzJ51M~4F< zBj`wVI=UTyqs1dp6H~JLv*la2G0|>>XMgwm{ae88Mc<`bX8?IyW(NmKpEEc!5!Jj? z8RG6-zM2#gQ0*eDDtvWsB#W)Z;%lrW-*uRCiB;r0=mFh?TuBzjpUGm1DN2O}akk6% z_15NBgmG4=v2f@hnzEd_JEXZ#{ni)goHb*>;NCN|)c%un2-m^l9cfR(mcDw$@z_=U zDj$cDtCKuIodG>OHBjiRnCWoj2mI-)rQ+_`ZV6q?{c@qR4onS>M&eV1CgYO%dj{rT zi3SE3I1u)z_OzyCXk4ZHJ|F&4HsUBz`gLIrW1yamXh|T8?xxN7M->+4-r@=fnxYsq za5zXiLE(dkY>4yn@H(!81t+kg*R{Flc%H-4!l4X(u&b}qFu8U(D}J5LIS14Qinq~M zB&WY%G6N1L&gJw+*Do(g^alzigyirs=`r0W9EUwRT%oCmnp+NBcQd7o`uZQ<&rk?R z*`BC_8W83fM{JdE^x0lGk)%C7P0ID5X9IpJcXsGBJzCgtWIlHt^if+4^A?r?pu8KK z4lY_EI2&n~w-m2<8esEfrbZdv$=YO~ffN#VbRiXlqddPgREPqXEd0_fB}h-fQd6I| z5{CEm#iWYS{a%N$GPOJwoMS7C-(Ah7&wNG2T;g~vs&nnV&J0%QqA*m%gp{Pl zl(FY5^Wf+v(x%PjrQO%NzL8&|7I9VhJSeCieYrfpm)Rco)&d`K{h%@!O;3` z>C`0;QOB&5+`;509vKlF;LFW+w zv<&D88B5>~FU@{tNm+JFaoWhW1SWX# z7jH4rf2Jxjra=9i?wfB3=Z86lWGo*HCYACncXVhd#QoZ}$*Cb=dsT#(m|G{0bcBGt566l>afX%_ss>ZJ51uilU|MVe)mHC$Ql8+AFB96h+lAipUB~CIB?TTBaM?9jr=$#i zx#Bv*?_rkg(7ieg>zM?x@mqU3s>u2`a-DWa`i9L2fa%SX4S2N)PM!EjD#tbxavK#RS?9yhy{b931V zo(TJr3bgFA`tK~tY z-7mQTzV<+Ogxf%f4%Eh;GYYMwj`Q{gfZ3vJZgGyO0xC_$DH9H61)(1htFIzI_+q+8 zRNL&yc8nU1M*D%RS@8by>~`s`6aRFg!4Pih>~Ym2lA*PY+q)7}ZGk2}jn<6kI(w;G zbNxV>7hWu87oE3}O$-gWD5wtOs(nbwFq`aO)pQ*#fXrW2ICM`ytd7rt{hQ)$(zrPQo}-uRG~>K@NN{E2 zi>kqM9a~IM^|r5z-_G7VL1OHE=#UmN`nc&}!l{)eD5)uingd2e<#+sacLlE-aX`Il zCo{Z1_F--IP7k40EeG;cy!!{(8$hk_3;c7?D`Auv){vfNFxiXFS?kD)W3fn1K>s3QGFE6KX>k5V>@{jsK!y7 zoO<^6%YM0P@B|Q-I#}+_f2!JFRTUz+AoW|Ub*E4Nkp4ep60{6}b^g{WYXA7_50MZ* z2W+{FALUu1f0k(einT|9!%OzMlhgkw4Kgyo`v&bWTq6GCl`qwA^ETQ~&fqz{N;^#6pGOt7a zt&{QG!5bTHV$7~`7icc9bFx49#HAH8bRM3gXu+1B8MysG!t*G^Z)MdDGr%h{dfmPJ zxW?gIE-%17m@{@nmzVZK4Q?)9eQOhw}yF1&p>xCM6Z+afb zpXw3@Iehy@b6reGn=4Ew=7o429R74jbYgQ~<eSE;N8?-R# z3^tXN`_<~bL-AC3Z2&Jqkf)QnwBeRQ_LI-IzMn`jgs|oPP6?=XzgE`sE;W{iTc_xa zia|x&8#ei=PoWVEeqnYEdb-6TNTjs zb9byQ$0rUDLiXrRgah{dyPDlCT5O4>eO=FtUzXie`x>f+<&BWNNb9GZ4)!~DGzA1_ zJNoU5_9 z4`5Bp>5|#<72Tm*Jt@C#4E1VYtK|?numF}FEWl3v;be4ZFr$c50sus|gD79W!qM>> z-&m*TOFZ)5|K1}vaI2)+5J)ArmT>$XI)$;yNwJefZE@MaaXz0VH$|8;vKn=&!-5(c z8!yn(P9E_nB2yD@RH)SGhX`Ill|SN}e@k^mlmh^ma_g;HvtNG0??Sm&VEi*}!`=}9 z8ITX)XFb)Z^$}83R8D9D@XYoBdqZuLw_&fKf;`-XOn2Sxdn}J|`+qNX8X#UCI-WG? zPP9bccks<$yhLA~UhgcswJ6QekyxxXbG>RNO0kclENg#NPy&-Kxo^=)$yR&rD!bSN z$9ngW=?pt749~mR?D3UxwwB3nb1*|uzm~;{O7e!do8K&_0Wv2nEXoM!me_b|PZ4f6 zAn2zO$JP?nQN5Y6;VM78-3zf6nYsTD3}Tc5Fo`3n%-^MHuQOJzv&AIkci)j&bar(59ME7eTD!&r^R=2;6un9PmLW0 z0f(%?$VK-#9U-Hc>25X{;eB(u7^Y~ctMbvzo@S;1T}Qvd>;nZcsXaS>=c&1oWm=xT zf}ZB7uT=~&j)Ud9jQ1LuBs`G>U~G|vdv?%;A=ph_j#+Rt^T4ZGAK*+5XXp)Z3|?0P zYWnCa_3{&q*mV|37q6m@!=AuVxB3cn`{%?iSJKEWpR%wJ;Z2NOP3mI24sY%pttF#@ zpWk~e2b~9KhvDnWf}+fx-)t_6Y@MbL@d5QPid$Ei8+sP1O+M~aB)dlO ztI_iMPT_C=6!9-LCOI>lpJ3IMESN|o{Z7C4DoLHOET#ftm>)~gt(y8G&PO~{SWu8~ z%w3ROJeqw~Lx>V}w-{)k*BtTM_ovK>&2iUK7K5PtfebfA$P_u#jWd)E7t9_mOmGE| zoh0+GEjiUXIe^;f(edl^sY$I?mfMW=5A`DRilPUeO*2 zpXkj6Z4*!ux~w+K+Wni?d~I)4dXApdW%phgGqaZYzvyMAJ}S3}s9X3AhazmOPz9*_ zHK(l3%@o-|OLd~KH&_r$Y1)xh&MF4UVUK{*DT+1yo=ws-EMcod^Yb~^a&$&&cbBy0 z0NSbB@(@-)! zo;f#L0xFp#eA*!}H#fVz^xM}x8n(?%@dA5M8{VZ!#h2hgg;OTWDP_-Xow3&i`rhEa zaQ~y6wGs`4bAiKi7lGKP@&*YH^#Q`6H%N+4YpBd0se1LW-9cDGUL^CQNqtcY!!T0b zQ-pWx`Dr@?ghSF9&_$aB>#cV{39tQy**%M+_v}U2c)4k5b@PQwOfnX3@hSC~pEMtC zE9~V2Ja5OVJua3pu=yvFW@p~34Bb^`@ScXG)t;U==a6)tU&jS`Q?A_Dmu zIb|vB1Y7L_EqCMz183dXt(8;TmfBsm$F@T~Z9ByJwN`12%S;t2@vRRdvtw0pD)p(x z6YWZjn3HKwkVcpVMsbT63NcApYXc+xr!z}<$MthP;z8XWl|vo`t}|y|#(sRrjN_l- zi@!uEzntm^G4L#@DQ8PCM^k(0B!lPLW!+8pgJqjK`NA8LuUQbhbVh2y@HZb;!yQm*kO4hhi4+!3S*WlW4MtN)?Me&v`WWn-CGVG zb?^rMnTKq2A=qSZr@Hkzx5eSM(2(gnPfoG!JZ1lEd!zpk+N6S(Imp)A+GJ=H}IGD^y_ts zdMbzzRrd{t71LY{G{dMEQz7dN)yh!q5|X*{zI6r;tdEyYTT+3xb=*xA4zEmx4gDC0 z0oz!3yvbH+dS)+CP5hum1*|lWo!!UW9Z8f$H0B{q#t2Nhv^XaA~bBfq5eUobhN@s0BN8&=FYQX7NZ89dP1 z%gNG8j(;F3P?kk;A1dTDbf1pbsOZ}W+da2et|Maub%97n1N+i^N44*3+mZ0e&OPPxm3y>HSpd-qM&7#ETm}kqVXoqNkRS7(yrba5?~##9tZ4FdM|nQ z#IKz7D=ehw#8BJUpO#He%vyKJ3)oQHS_VjjG~3&ZUq)|0-Jn(U$E)>K3PW?JFE_b6 z)PzPP%?aZ2#TViEdKXhvq9vJ`KBPo6x4#&_=ay_oN`EejNm9c&(EioIXhgAsguj9X z%0UD?xFCh9P%Gx1ITw4Da;gsn*SMv&zJS0POC#O)Ro_)Ozro$ceDMd4cBIGw)FBC2 zojw2Q1%5&5{)?1CJ7w;tcxh|02f}quVoUwe@iWua2J4$2!XmUcYBxtn`yOJhl$-V! zVZNA2oE{bRiQhm|%B%^etl5u))~!@>{E>yWNbhKX0+PVfKfJTD|1e=Y1B%SNnaqWNnSz)MXvy57sH- z&+u(hoLxXw(r4)q_j3+4uIse5pc8VA&~K)THSdKvvbof9S?fWQ`Abj5QEGH{29f3| zpkgwHCZPY7odec4~y{IMsQ(68IIm6^ki6Wi_41b2oK1Y2ZL(%Kur`5<}!LJ;6+J1xp)ElZU z-7eP4cP_Ga5~L<3ZcNh3PD(W-({XYF+^Pv$%*J^5pFVzkd42{%=x{No2@oZ{_d3M7 z@b`=R#kmk94u^^7$M0SoWMgH&MLn7TO|BFDco45so&LDxkq4{4&zySKcnmZMUE{gA z+zM1m&PxoX2rv{j_uedqKHT3(oSQBmoF{jgji>Y%PZVma0|+t-_Jb^dIFQk7p0@XX zzI=5)?i8yxU)d;2YF%oyL>FWN&XK)vIcUKjh~=HC1(o7+Ig2Q_kJZWIMe1C+ zG^?~~P_>N(z`TqCgmwTFlv25&ERaRaWDR}ecyoFuDt%^?Fq-bp$5rUol`q_ZfC*I9 zP2SW>Xf|6#B?pGs^s!Z#%u8`Qd}e5E)f{>N>RCUn#?o2y-uQkrU9=A+oUfAtuz7Rw zdmgzAIyGx|bQ&RC=&wm`reo=}^WH~)o~lp{7451q$V}vj0}2c!jg2SiM5L^PKF`$2 zh7yFusvv(PGo6S$(B+YK+xPiX&-}L1y?J7*}1t6*eoi!^Z+Hv8f{9~gBz*Y^0N3))3eHP+uEcUbgD^h zysVY++(*0G+(9E%LBT*M;d(pHdU*vVKT9aZ&3*Hr5h$H}50o-H@oONi=eVDJdBU-6 zPiB_`V0=;#06;TgG3>d&Tv!jSZjwvmTYac7{I;aNJK48u(exT9?;Jh){tb~$;OYva zc0V@h(@EBs>zf}~Ri9^8>WQGP9|!TkJNnKN(j<2$6_vE1TG?j0l4}vy9uDUy z_*`tyom12Mi4*RpH=iI8T((zvTB=d)xC-VIr_~mVYM*?tUnG}Qo$zJ2Snj<^rH?k!z|MRix-nJmt97CITJNN^4jW~H7d11N4lp4em{ zyOb$_Z?D_%e(}h38Q_m4V!&f{MJiNL6z)RN`jJqy>8>BbUSnAX5K$l8R-m1?!ZwlZ z&sH5f)BVMO_>>%ZzyiWOj}8iyV(((;o850Vr@<#H9(uQrdnxcbF_$q9Vd-!+#5{p; zw2-9Kc{PXHb8KG>^^+=0lCoG*lLq!i-k38T+?Ld2dJ|`q z9m4f)Jn}a8xispuEWPkKN_ID2ZtAnRZUVb5Epu&l+bp`wJTzsLWytPDB@sissNsTa zbn)v>DBsM=%K91T-C65TPlAg@|4~}yqX*5Tjb(wK--RioW)^}meF?qwn`>Vr+?TJT zI>Rc{0x+f*hr|J9?Nmoc$L^*qpR$kJn~~Nxwow_x_?|$OYn%&Ke~in

fs(cR~Ai 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 ALjV8( literal 0 HcmV?d00001 diff --git a/assets/interchain-unlock.png b/assets/interchain-unlock.png new file mode 100644 index 0000000000000000000000000000000000000000..bea573b9addede7f2617eeedc6d4980ee59d207c GIT binary patch literal 80521 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 literal 0 HcmV?d00001 diff --git a/assets/interchain-withdraw-funds.png b/assets/interchain-withdraw-funds.png new file mode 100644 index 0000000000000000000000000000000000000000..b571b5c1ec4e3f2990ff5d24f86f379c7d55dfaf GIT binary patch literal 65482 zcmeFZXH=6}7d8w?5hJ1?FrWe!M7mN%s(@4x=_T|kC4lsfNK+B%ARr*pdk?*dC=fz# zNvK0F0i+~AfP6RejEpnmzi+*3z3*D%56pr)=RRkjeRjF_wa@FPPvj`hF`OeHAfQl? zmr)}iARZ$iAVQxd1%7kTkI(`BBX*Wj&^QbH^Eqq&ihzKbKtblAhKJ$W1es5)X5F{% zteHgVL6hDanqFj=h#y^2<*YBZaCJr3727`_Egzk1HzdqDH{hKBJkco4d_5c|%KG>` z+d1Ofd@)nC2@##$A{)Ys-cAx}*yF|6kIs5QFA0dwGQarOj|7&;)pGIn(2J4;gv9^) z(L#Q)^n2*PzXgmWNlScoto+3{xl6zLCbRLZ!>vDAVvBI3rcMwM*&I`yw9 z{xNOci_FF_B1Wc1zZ&($i}2=O%pQ2OoLRCZoME|7=M z4FHB$wn&LIjXZtT2bB<(-IbB1%+&Kt!e0%h>a`=34)nEn7)4CVzkE%Ft2HVmlnlKn zdaRXv!>Z>)=ayIv;g8KXCNle~>~zs}bx0BWq1~TDL+)a4WWL6S$NIm2m+Q7o7R# zSC-G1A{~SldGtAz;)^bO--pGpR(rpyq<2YefyI~gq>4;UOfgP&#j|m7B@4w-wTE?L z14Ft}#c-L9=~Bg$dIANj)u|L}$oB`K?Mm^m=y{G?w{L6ZYvkWa5vi(YmVw1_$jhz@ zwS%GdG0q3XdbMgKqnHNIts2x-F*p6}$H{CWZs_=+AYwM%Pc@aJZd!M=M;ax3N3IIH ztpyF`8MJX-Iy!EM*!{-%E`mwoI*Sr*g1FEGb5xWkpWRKKQw62tFgD^hR1{`SDmBP5 z?Nu!+D_i&ML~7n)?-Cm|u(L+xx3?R1x&AmyEi^PWYcC*e*>DE=M+)1v3`Q4=J@u?+ zhCjE%%qIZ-d)h3_r44<1o?cLq(Q|1M^q|xd?$voE@v1n2(O!_R=XjNE_V$zEr^Th9 z)`M}Mz@X6JPO_usNKX)LXT3wi@7z}eEMNL;?F$W*`P$}blr>!K%PMeaI5OEfUx&gy zI|`j29-SIKnvv3f7We-Bok6RfC|&1;PXFV5A?>NWrDt@dS;ZnnJ0Gv#G~ZLn$Wr>| z7A;{UPA}+G$!S!pm+F0>N=~kj{WdHt;hIRbO>)>u+sr+BLDR~hkRZkC;*_~H#C^~0 z6w3MOnVSRo?E=L-+q9hVBo9EF5_k`-H#8h8`IYk&w57UBV4X`E^9s| zrM!~&(6mw%vw38l+_7;gFALD)!Nm;VV0E4GuR}qwCi6pi&pem$lhLHRD~iocLa^HH zfDc7F9^d1H%(1DB+{E5Ow!mA5@TKylwh^{=$WkM8nSNrJ<(q?hj(xZ)n*H zm7bKi^B~_SC07Y}v@CKDm8)fVY3`3ImDEmiK3**gB(#eki-{1=tQd?=c!nedWqf-Q z+jCHd<|_9>d+jexjAm+u+pIGdfdv!DV{$rqeV^7JjU>-Emo&{7;

!>+#uK1un}g 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 update_ibc_proposal_status(deps, info, proposal_id, status), + ExecuteMsg::RemoveOutpostVotes { proposal_id } => { + remove_outpost_votes(deps, env, info, proposal_id) + } } } @@ -226,7 +235,7 @@ pub fn submit_proposal( // Check that controller exists and it supports this channel if let Some(ibc_channel) = &ibc_channel { if let Some(ibc_controller) = &config.ibc_controller { - check_controller_supports_channel(deps.querier, ibc_controller, ibc_channel)?; + check_contract_supports_channel(deps.querier, ibc_controller, ibc_channel)?; } else { return Err(ContractError::MissingIBCController {}); } @@ -237,7 +246,9 @@ pub fn submit_proposal( submitter: sender.clone(), status: ProposalStatus::Active, for_power: Uint128::zero(), + outpost_for_power: Uint128::zero(), against_power: Uint128::zero(), + outpost_against_power: Uint128::zero(), for_voters: Vec::new(), against_voters: Vec::new(), start_block: env.block.height, @@ -390,17 +401,34 @@ pub fn cast_outpost_vote( // Voting power provided is used as is from the Hub. Validation of the voting // power is done by the Hub contract with the Outpost. + // We track voting power from Outposts separately as well so as to have a + // way to cancel votes should a vulnerability be found in IBC or the Hub/Outpost + // implementation match vote_option { ProposalVoteOption::For => { proposal.for_power = proposal.for_power.checked_add(voting_power)?; + proposal.outpost_for_power = proposal.outpost_for_power.checked_add(voting_power)?; proposal.for_voters.push(voter.clone()); } ProposalVoteOption::Against => { proposal.against_power = proposal.against_power.checked_add(voting_power)?; + proposal.outpost_against_power = + proposal.outpost_against_power.checked_add(voting_power)?; proposal.against_voters.push(voter.clone()); } }; + // Assert that the total amount of power from Outposts is not greater than the + // total amount of power that was available at the time of proposal creation + let current_outpost_power = proposal + .outpost_for_power + .checked_add(proposal.outpost_against_power)?; + let max_outpost_power = + get_total_outpost_voting_power_at(deps.querier, &hub, proposal.start_time)?; + if current_outpost_power > max_outpost_power { + return Err(ContractError::InvalidVotingPower {}); + } + PROPOSALS.save(deps.storage, proposal_id, &proposal)?; Ok(Response::new().add_attributes(vec![ @@ -571,7 +599,7 @@ pub fn submit_execute_emissions_proposal( // Check that controller exists and it supports this channel if let Some(ibc_channel) = &ibc_channel { if let Some(ibc_controller) = &config.ibc_controller { - check_controller_supports_channel(deps.querier, ibc_controller, ibc_channel)?; + check_contract_supports_channel(deps.querier, ibc_controller, ibc_channel)?; } else { return Err(ContractError::MissingIBCController {}); } @@ -587,7 +615,9 @@ pub fn submit_execute_emissions_proposal( submitter: info.sender, status: ProposalStatus::Passed, for_power: Uint128::zero(), + outpost_for_power: Uint128::zero(), against_power: Uint128::zero(), + outpost_against_power: Uint128::zero(), for_voters: vec![generator_controller.to_string()], against_voters: Vec::new(), start_block: env.block.height, @@ -742,6 +772,10 @@ pub fn update_config( } } + if let Some(guardian_addr) = updated_config.guardian_addr { + config.guardian_addr = Some(deps.api.addr_validate(&guardian_addr)?); + } + config.validate()?; CONFIG.save(deps.storage, &config)?; @@ -786,6 +820,90 @@ fn update_ibc_proposal_status( } } +/// Remove all votes cast from all Outposts in case of a vulnerability +/// in IBC or the contracts that allow manipulation of governance. This is the +/// last line of defence against a malicious actor. +/// +/// This can only be called by the guardian. +fn remove_outpost_votes( + deps: DepsMut, + env: Env, + info: MessageInfo, + proposal_id: u64, +) -> Result { + let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?; + + let config = CONFIG.load(deps.storage)?; + + // Only the guardian may execute this + if Some(info.sender) != config.guardian_addr { + return Err(ContractError::Unauthorized {}); + } + + // EndProposal must be called first to return xASTRO to the proposer + if proposal.status == ProposalStatus::Active { + return Err(ContractError::ProposalNotInDelayPeriod {}); + } + + // This may only be called during the "delay" period for a proposal. That is, + // the config.proposal_effective_delay blocks between when the voting period + // ends and the proposal can be executed. If we allow the removal of votes during + // the voting period, we can end up in a battle with the attacker where we + // remove the votes and they exploit and vote again. + if env.block.height <= proposal.end_block || env.block.height > proposal.delayed_end_block { + return Err(ContractError::ProposalNotInDelayPeriod {}); + } + + // Remove the voting power from Outposts + let new_for_power = proposal + .for_power + .saturating_sub(proposal.outpost_for_power); + let new_against_power = proposal + .against_power + .saturating_sub(proposal.outpost_against_power); + + proposal.for_power = new_for_power; + proposal.against_power = new_against_power; + + // Zero out the Outpost voting power after removal + proposal.outpost_for_power = Uint128::zero(); + proposal.outpost_against_power = Uint128::zero(); + + let total_votes = proposal.for_power.saturating_add(proposal.against_power); + let total_voting_power = calc_total_voting_power_at(deps.as_ref(), &proposal)?; + + // Recalculate proposal state + let mut proposal_quorum: Decimal = Decimal::zero(); + let mut proposal_threshold: Decimal = Decimal::zero(); + + if !total_voting_power.is_zero() { + proposal_quorum = Decimal::from_ratio(total_votes, total_voting_power); + } + + if !total_votes.is_zero() { + proposal_threshold = Decimal::from_ratio(proposal.for_power, total_votes); + } + + // Determine the proposal result + proposal.status = if proposal_quorum >= config.proposal_required_quorum + && proposal_threshold > config.proposal_required_threshold + { + ProposalStatus::Passed + } else { + ProposalStatus::Rejected + }; + + PROPOSALS.save(deps.storage, proposal_id, &proposal)?; + + let response = Response::new().add_attributes(vec![ + attr("action", "remove_outpost_votes"), + attr("proposal_id", proposal_id.to_string()), + attr("proposal_result", proposal.status.to_string()), + ]); + + Ok(response) +} + /// Expose available contract queries. /// /// ## Queries diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index d6cec35e..3a51707e 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -41,6 +41,9 @@ pub enum ContractError { #[error("Proposal delay not ended!")] ProposalDelayNotEnded {}, + #[error("Proposal not in delay period!")] + ProposalNotInDelayPeriod {}, + #[error("Contract can't be migrated!")] MigrationError {}, @@ -76,6 +79,9 @@ pub enum ContractError { #[error("The proposal has no messages to execute")] InvalidProposalMessages {}, + + #[error("Voting power exceeds maximum Outpost power")] + InvalidVotingPower {}, } impl From for ContractError { diff --git a/contracts/assembly/src/migration.rs b/contracts/assembly/src/migration.rs index a8b43384..58e213e5 100644 --- a/contracts/assembly/src/migration.rs +++ b/contracts/assembly/src/migration.rs @@ -102,7 +102,9 @@ pub(crate) fn migrate_proposals_to_v160(deps: DepsMut, cfg: &Config) -> StdResul submitter: proposal.submitter, status: proposal.status, for_power: proposal.for_power, + outpost_for_power: Uint128::zero(), against_power: proposal.against_power, + outpost_against_power: Uint128::zero(), for_voters: proposal .for_voters .into_iter() @@ -155,6 +157,7 @@ pub(crate) fn migrate_config_to_160(deps: DepsMut, msg: MigrateMsg) -> StdResult proposal_required_quorum: cfg_v130.proposal_required_quorum, proposal_required_threshold: cfg_v130.proposal_required_threshold, whitelisted_links: cfg_v130.whitelisted_links, + guardian_addr: None, }; CONFIG.save(deps.storage, &cfg)?; diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index cd976fdf..51e395c8 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -15,6 +15,8 @@ use astroport_governance::voting_escrow_lite::{ Cw20HookMsg as VXAstroCw20HookMsg, InstantiateMsg as VXAstroInstantiateMsg, }; +use astroport_governance::hub::InstantiateMsg as HubInstantiateMsg; + use astroport_governance::builder_unlock::msg::{ InstantiateMsg as BuilderUnlockInstantiateMsg, ReceiveMsg as BuilderUnlockReceiveMsg, }; @@ -217,7 +219,7 @@ fn test_proposal_submitting() { let owner = Addr::unchecked("owner"); let user = Addr::unchecked("user1"); - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); let proposals: ProposalListResponse = app @@ -497,6 +499,7 @@ fn test_proposal_submitting() { proposal_required_threshold: None, whitelist_add: None, whitelist_remove: None, + guardian_addr: None, }))) .unwrap(), funds: vec![], @@ -548,6 +551,7 @@ fn test_proposal_submitting() { proposal_required_threshold: None, whitelist_add: None, whitelist_remove: None, + guardian_addr: None, }))) .unwrap(), funds: vec![], @@ -574,6 +578,7 @@ fn test_successful_proposal() { builder_unlock_addr, assembly_addr, _, + _, ) = instantiate_contracts(&mut app, owner, false, false); // Init voting power for users @@ -692,6 +697,7 @@ fn test_successful_proposal() { "https://some2.link/".to_string(), ]), whitelist_remove: Some(vec!["https://some.link/".to_string()]), + guardian_addr: None, }))) .unwrap(), funds: vec![], @@ -966,7 +972,7 @@ fn test_successful_emissions_proposal() { let mut app = mock_app(); let owner = Addr::unchecked("generator_controller"); - let (_, _, _, _, _, assembly_addr, _) = instantiate_contracts(&mut app, owner, true, false); + let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); // Provide some funds to the Assembly contract to use in the proposal messages app.init_modules(|router, _, storage| { @@ -1011,7 +1017,7 @@ fn test_no_generator_controller_emissions_proposal() { let mut app = mock_app(); let owner = Addr::unchecked("generator_controller"); - let (_, _, _, _, _, assembly_addr, _) = instantiate_contracts(&mut app, owner, false, false); + let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { title: "Emissions Test title!".to_string(), description: "Emissions Test description!".to_string(), @@ -1040,7 +1046,7 @@ fn test_empty_messages_emissions_proposal() { let mut app = mock_app(); let owner = Addr::unchecked("generator_controller"); - let (_, _, _, _, _, assembly_addr, _) = instantiate_contracts(&mut app, owner, true, false); + let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { title: "Emissions Test title!".to_string(), description: "Emissions Test description!".to_string(), @@ -1071,7 +1077,7 @@ fn test_unauthorised_emissions_proposal() { let mut app = mock_app(); let owner = Addr::unchecked("generator_controller"); - let (_, _, _, _, _, assembly_addr, _) = instantiate_contracts(&mut app, owner, true, false); + let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { title: "Emissions Test title!".to_string(), description: "Emissions Test description!".to_string(), @@ -1101,7 +1107,7 @@ fn test_voting_power_changes() { let owner = Addr::unchecked("owner"); - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); // Mint tokens for submitting proposal @@ -1151,6 +1157,7 @@ fn test_voting_power_changes() { proposal_required_threshold: None, whitelist_add: None, whitelist_remove: None, + guardian_addr: None, }))) .unwrap(), funds: vec![], @@ -1234,7 +1241,7 @@ fn test_fail_outpost_vote_without_hub() { let owner = Addr::unchecked("owner"); - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); // Mint tokens for submitting proposal @@ -1284,6 +1291,7 @@ fn test_fail_outpost_vote_without_hub() { proposal_required_threshold: None, whitelist_add: None, whitelist_remove: None, + guardian_addr: None, }))) .unwrap(), funds: vec![], @@ -1324,13 +1332,15 @@ fn test_outpost_vote() { let owner = Addr::unchecked("owner"); - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + let (astro_token, staking_instance, xastro_addr, _, _, assembly_addr, _, hub_addr) = instantiate_contracts(&mut app, owner.clone(), false, true); let user1_voting_power = 10_000_000_000; let user2_voting_power = 5_000_000_000; let remote_user1_voting_power = 80_000_000_000u128; - let remote_user2_voting_power = 3_000_000_000u128; + // let remote_user2_voting_power = 3_000_000_000u128; + + let hub_addr = hub_addr.unwrap(); // Mint tokens for submitting proposal mint_tokens( @@ -1359,6 +1369,15 @@ fn test_outpost_vote() { user2_voting_power, ); + // Mint ASTRO to stake + mint_tokens( + &mut app, + &owner, + &astro_token, + &Addr::unchecked("cw20ics20"), + 1_000_000_000_000u128, + ); + app.update_block(|mut block| { block.time = block.time.plus_seconds(WEEK); block.height += WEEK / 5; @@ -1388,6 +1407,7 @@ fn test_outpost_vote() { proposal_required_threshold: None, whitelist_add: None, whitelist_remove: None, + guardian_addr: None, }))) .unwrap(), funds: vec![], @@ -1409,107 +1429,58 @@ fn test_outpost_vote() { .unwrap_err(); assert_eq!(err.root_cause().to_string(), "Unauthorized"); - cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - owner.clone(), - Addr::unchecked("remote1"), - ProposalVoteOption::For, - Uint128::from(remote_user1_voting_power), - ) - .unwrap(); - - cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - owner.clone(), - Addr::unchecked("remote2"), - ProposalVoteOption::Against, - Uint128::from(remote_user2_voting_power), - ) - .unwrap(); - + // Attempts to vote with no xASTRO minted on Outposts let err = cast_outpost_vote( &mut app, assembly_addr.clone(), 1, - owner, + hub_addr, Addr::unchecked("remote1"), ProposalVoteOption::For, - Uint128::from(remote_user2_voting_power), - ) - .unwrap_err(); - assert_eq!(err.root_cause().to_string(), "User already voted!"); - - // user1 can vote as he had voting power before the proposal submitting. - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user1"), - ProposalVoteOption::For, - ) - .unwrap(); - - let err = cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user1"), - ProposalVoteOption::For, + Uint128::from(remote_user1_voting_power), ) .unwrap_err(); - assert_eq!(err.root_cause().to_string(), "User already voted!"); - - // user2 can vote as he had voting power before the proposal submitting. - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user2"), - ProposalVoteOption::Against, - ) - .unwrap(); - - app.update_block(next_block); - - // Skip voting period and delay - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi - .time - .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - // End proposal - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - // Check proposal votes, Outpost and Hub votes should be counted - let total_for_voting_power = user1_voting_power + remote_user1_voting_power; - let total_against_voting_power = user2_voting_power + remote_user2_voting_power; - assert_eq!(proposal.for_power, Uint128::from(total_for_voting_power)); assert_eq!( - proposal.against_power, - Uint128::from(total_against_voting_power) + err.root_cause().to_string(), + "Voting power exceeds maximum Outpost power" ); - // Should be passed - assert_eq!(proposal.status, ProposalStatus::Passed); + + // Note: Due to cw-multitest not supporting IBC messages we can no longer + // test voting with Outpost voting power + + // app.execute_contract( + // owner, + // hub_addr.clone(), + // &astroport_governance::hub::ExecuteMsg::AddOutpost { + // outpost_addr: "outpost1".to_string(), + // outpost_channel: "channel-3".to_string(), + // cw20_ics20_channel: "channel-1".to_string(), + // }, + // &[], + // ) + // .unwrap_err(); + + // Stake some ASTRO from an Outpost + // stake_remote_astro( + // &mut app, + // Addr::unchecked("cw20ics20".to_string()), + // hub_addr.clone(), + // astro_token, + // Uint128::from(remote_user1_voting_power), + // ) + // .unwrap_err(); + + // Continue normally + // cast_outpost_vote( + // &mut app, + // assembly_addr.clone(), + // 1, + // hub_addr.clone(), + // Addr::unchecked("remote1"), + // ProposalVoteOption::For, + // Uint128::from(remote_user1_voting_power), + // ) + // .unwrap(); } #[cfg(not(feature = "testnet"))] @@ -1523,7 +1494,7 @@ fn test_block_height_selection() { let user2 = Addr::unchecked("user2"); let user3 = Addr::unchecked("user3"); - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); // Mint tokens for submitting proposal @@ -1643,7 +1614,7 @@ fn test_unsuccessful_proposal() { let owner = Addr::unchecked("owner"); - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = + let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); // Init voting power for users @@ -1782,7 +1753,7 @@ fn test_unsuccessful_proposal() { fn test_check_messages() { let mut app = mock_app(); let owner = Addr::unchecked("owner"); - let (_, _, _, vxastro_addr, _, assembly_addr, _) = + let (_, _, _, vxastro_addr, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); change_owner(&mut app, &vxastro_addr, &assembly_addr); @@ -1863,7 +1834,16 @@ fn instantiate_contracts( owner: Addr, with_generator_controller: bool, with_hub: bool, -) -> (Addr, Addr, Addr, Addr, Addr, Addr, Option) { +) -> ( + Addr, + Addr, + Addr, + Addr, + Addr, + Addr, + Option, + Option, +) { let token_addr = instantiate_astro_token(router, &owner); let (staking_addr, xastro_token_addr) = instantiate_xastro_token(router, &owner, &token_addr); let vxastro_token_addr = instantiate_vxastro_token(router, &owner, &xastro_token_addr); @@ -1883,7 +1863,12 @@ fn instantiate_contracts( let mut hub_addr = None; if with_hub { - hub_addr = Some(owner.to_string()); + hub_addr = Some(instantiate_hub( + router, + &owner, + &Addr::unchecked("contract6".to_string()), + &staking_addr, + )); } let assembly_addr = instantiate_assembly_contract( @@ -1894,7 +1879,7 @@ fn instantiate_contracts( &builder_unlock_addr, None, generator_controller_addr, - hub_addr, + hub_addr.clone(), ); ( @@ -1905,6 +1890,7 @@ fn instantiate_contracts( builder_unlock_addr, assembly_addr, None, + hub_addr, ) } @@ -2020,6 +2006,44 @@ fn instantiate_vxastro_token(router: &mut App, owner: &Addr, xastro: &Addr) -> A .unwrap() } +fn instantiate_hub( + router: &mut App, + owner: &Addr, + assembly_addr: &Addr, + staking_addr: &Addr, +) -> Addr { + let hub_contract = Box::new( + ContractWrapper::new_with_empty( + astroport_hub::execute::execute, + astroport_hub::contract::instantiate, + astroport_hub::query::query, + ) + .with_reply(astroport_hub::reply::reply), + ); + + let hub_code_id = router.store_code(hub_contract); + + let msg = HubInstantiateMsg { + owner: owner.to_string(), + assembly_addr: assembly_addr.to_string(), + cw20_ics20_addr: "cw20ics20".to_string(), + generator_controller_addr: "unknown".to_string(), + ibc_timeout_seconds: 60, + staking_addr: staking_addr.to_string(), + }; + + router + .instantiate_contract( + hub_code_id, + owner.clone(), + &msg, + &[], + String::from("Hub"), + None, + ) + .unwrap() +} + fn instantiate_builder_unlock_contract(router: &mut App, owner: &Addr, astro_token: &Addr) -> Addr { let builder_unlock_contract = Box::new(ContractWrapper::new_with_empty( builder_unlock::contract::execute, @@ -2056,7 +2080,7 @@ fn instantiate_assembly_contract( builder: &Addr, delegator: Option, generator_controller_addr: Option, - hub_addr: Option, + hub_addr: Option, ) -> Addr { let assembly_contract = Box::new(ContractWrapper::new_with_empty( astro_assembly::contract::execute, @@ -2066,13 +2090,15 @@ fn instantiate_assembly_contract( let assembly_code = router.store_code(assembly_contract); + let hub: Option = hub_addr.as_ref().map(|s| s.to_string()); + let msg = InstantiateMsg { xastro_token_addr: xastro.to_string(), vxastro_token_addr: Some(vxastro.to_string()), voting_escrow_delegator_addr: delegator, ibc_controller: None, generator_controller_addr, - hub_addr, + hub_addr: hub, builder_unlock_addr: builder.to_string(), proposal_voting_period: PROPOSAL_VOTING_PERIOD, proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, @@ -2259,6 +2285,31 @@ fn cast_outpost_vote( ) } +// Add back once cw-multitest supports IBC +// fn stake_remote_astro( +// app: &mut App, +// sender: Addr, +// hub: Addr, +// astro_token: Addr, +// amount: Uint128, +// ) -> anyhow::Result { +// let cw20_msg = to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { +// channel: "channel-1".to_string(), +// sender: "remoteuser1".to_string(), +// receiver: hub.to_string(), +// memo: "{\"stake\":{}}".to_string(), +// }) +// .unwrap(); + +// let msg = Cw20ExecuteMsg::Send { +// contract: hub.to_string(), +// amount, +// msg: cw20_msg, +// }; + +// app.execute_contract(sender, astro_token, &msg, &[]) +// } + fn change_owner(app: &mut App, contract: &Addr, assembly: &Addr) { let msg = astroport_governance::voting_escrow_lite::ExecuteMsg::ProposeNewOwner { new_owner: assembly.to_string(), diff --git a/contracts/generator_controller_lite/src/contract.rs b/contracts/generator_controller_lite/src/contract.rs index d9fdc9e8..0814344c 100644 --- a/contracts/generator_controller_lite/src/contract.rs +++ b/contracts/generator_controller_lite/src/contract.rs @@ -20,7 +20,7 @@ use astroport_governance::generator_controller_lite::{ ConfigResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, NetworkInfo, QueryMsg, UserInfoResponse, VOTERS_MAX_LIMIT, }; -use astroport_governance::utils::{check_controller_supports_channel, get_lite_period}; +use astroport_governance::utils::{check_contract_supports_channel, get_lite_period}; use astroport_governance::voting_escrow_lite::QueryMsg::CheckVotersAreBlacklisted; use astroport_governance::voting_escrow_lite::{ get_emissions_voting_power, get_lock_info, BlacklistedVotersResponse, @@ -291,7 +291,7 @@ fn update_networks( if let Some(ibc_channel) = network_info.ibc_channel.clone() { match &assembly_config.ibc_controller { Some(ibc_controller) => { - check_controller_supports_channel( + check_contract_supports_channel( deps.querier, ibc_controller, &ibc_channel, diff --git a/contracts/hub/.cargo/config b/contracts/hub/.cargo/config new file mode 100644 index 00000000..f1bf3f59 --- /dev/null +++ b/contracts/hub/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +wasm-debug = "build --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --bin schema" \ No newline at end of file diff --git a/contracts/hub/Cargo.toml b/contracts/hub/Cargo.toml new file mode 100644 index 00000000..887be453 --- /dev/null +++ b/contracts/hub/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "astroport-hub" +version = "1.0.0" +authors = ["Astroport"] +edition = "2021" +description = "Handles interchain actions from Astroport outposts" +license = "GPL-3.0" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cw2 = "1.0.1" +cw20 = "0.15" +cosmwasm-schema = "1.1.0" +cosmwasm-std = { version = "1.1", features = ["iterator", "ibc3"] } +cw-storage-plus = "0.15" +schemars = "0.8.12" +serde = { version = "1.0.164", default-features = false, features = ["derive"] } +thiserror = "1.0.40" +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-governance = { path = "../../packages/astroport-governance" } +serde-json-wasm = "0.5.1" + +[dev-dependencies] +cw-multi-test = "0.16.5" +anyhow = "1.0" diff --git a/contracts/hub/README.md b/contracts/hub/README.md new file mode 100644 index 00000000..38520790 --- /dev/null +++ b/contracts/hub/README.md @@ -0,0 +1,139 @@ +# Hub + +The Hub contract enables staking, unstaking, voting in governance as well as voting on vxASTRO emissions from any chain where the Outpost contract is deployed on. The Hub and Outpost contracts are designed to work together, connected over IBC channels. + +The Hub defines the following messages that can be received over IBC: + +```rust +/// Hub defines the messages that can be sent from an Outpost to the Hub +pub enum Hub { + /// Queries the Assembly for a proposal by ID via the Hub + QueryProposal { + /// The ID of the proposal to query + id: u64, + }, + /// Cast a vote on an Assembly proposal + CastAssemblyVote { + /// The ID of the proposal to vote on + proposal_id: u64, + /// The address of the voter + voter: Addr, + /// The vote choice + vote_option: ProposalVoteOption, + /// The voting power held by the voter, in this case xASTRO holdings + voting_power: Uint128, + }, + /// Cast a vote during an emissions voting period + CastEmissionsVote { + /// The address of the voter + voter: Addr, + /// The voting power held by the voter, in this case vxASTRO lite holdings + voting_power: Uint128, + /// The votes in the format (pool address, percent of voting power) + votes: Vec<(String, u16)>, + }, + /// Stake ASTRO tokens for xASTRO + Stake {}, + /// Unstake xASTRO tokens for ASTRO + Unstake { + // The user requesting the unstake and that should receive it + receiver: String, + /// The amount of xASTRO to unstake + amount: Uint128, + }, + /// Kick an unlocked voter's voting power from the Generator Controller lite + KickUnlockedVoter { + /// The address of the voter to kick + voter: Addr, + }, + /// Withdraw stuck funds from the Hub in case of specific IBC failures + WithdrawFunds { + /// The address of the user to withdraw funds for + user: Addr, + }, +} +``` + +The Hub is unable to verify the information it receives, such as xASTRO holdings on an Outpost. To prevent invalid data reaching the Hub, it is only allowed to receive messages from the Outpost contract which verifies the data before sending it. + +The Hub defines the following execute messages: + +```rust +pub enum ExecuteMsg { + /// Receive a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// Update parameters in the Hub contract. Only the owner is allowed to + /// update the config + UpdateConfig { + /// The new address of the Assembly on the Hub, ignored if None + assembly_addr: Option, + /// The new address of the CW20-ICS20 contract on the Hub that + /// supports memo handling, ignored if None + cw20_ics20_addr: Option, + }, + /// Add a new Outpost to the Hub. Only allowed Outposts can send IBC messages + AddOutpost { + /// The remote contract address of the Outpost to add + outpost_addr: String, + /// The channel to use for CW20-ICS20 IBC transfers + cw20_ics20_channel: String, + }, + /// Remove an Outpost from the Hub + RemoveOutpost { + /// The remote contract address of the Outpost to remove + outpost_addr: String, + }, +} +``` + +## Message details + +**Receive ASTRO via a Cw20HookMsg message containing an OutpostMemo** + +To stake ASTRO from an Outpost a user needs to send the ASTRO over IBC (via the CW20-ICS20 contract) to the Hub. Together with these tokens they need to provide a valid JSON memo indicating the action to take. Currently, only staking is supported. + +Using a chain's CLI, the command looks as follows + +```bash +wasmd tx ibc-transfer transfer transfer channel-1 cw20_ics20_contract address 2000ibc/81A0618D89A81E830D4D670650E674770DEFFE344DCE3EDF3F62A9E3A506C0B4 -- --from user --memo '{"stake": {}}' +``` + +Once the memo is interpreted and executed, the xASTRO is minted to the user on the Outpost. + +**Update Config** + +Update config allows the owner to set a new address for the Assembly and the CW20-ICS20 contracts + +```json +{ + "update_config": { + "assembly_addr": "wasm123...", + "cw20_ics20_addr": "wasm456..." + } +} +``` + +**Adding an Outpost** + +Only Outposts listed in the contract are allowed to open IBC channels and send messages. + +```json +{ + "add_outpost":{ + "outpost_addr": "wasm123...", + "cw20_ics20_channel": "ASTRO transfer channel in CW20-ICS20 contract" + } +} +``` + +**Remove an Outpost** + +Removing an Outpost will not close the IBC channels, but will block new messages sent from the Outpost + +```json +{ + "remove_outpost":{ + "outpost_addr": "wasm123..." + } +} +``` \ No newline at end of file diff --git a/contracts/hub/src/contract.rs b/contracts/hub/src/contract.rs new file mode 100644 index 00000000..3681c5f4 --- /dev/null +++ b/contracts/hub/src/contract.rs @@ -0,0 +1,133 @@ +use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response}; +use cw2::set_contract_version; + +use astroport::staking::{ConfigResponse, QueryMsg}; +use astroport_governance::{ + hub::{Config, InstantiateMsg, MigrateMsg}, + interchain::{MAX_IBC_TIMEOUT_SECONDS, MIN_IBC_TIMEOUT_SECONDS}, +}; + +use crate::{error::ContractError, state::CONFIG}; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "astroport-hub"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Instantiates the contract, storing the config and querying the staking contract. +/// Returns a `Response` object on successful execution or a `ContractError` on failure. +#[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)?; + + // Query staking contract for ASTRO and xASTRO address + let staking_config: ConfigResponse = deps + .querier + .query_wasm_smart(&msg.staking_addr, &QueryMsg::Config {})?; + + if !(MIN_IBC_TIMEOUT_SECONDS..=MAX_IBC_TIMEOUT_SECONDS).contains(&msg.ibc_timeout_seconds) { + return Err(ContractError::InvalidIBCTimeout { + timeout: msg.ibc_timeout_seconds, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS, + }); + } + + let config = Config { + owner: deps.api.addr_validate(&msg.owner)?, + assembly_addr: deps.api.addr_validate(&msg.assembly_addr)?, + cw20_ics20_addr: deps.api.addr_validate(&msg.cw20_ics20_addr)?, + staking_addr: deps.api.addr_validate(&msg.staking_addr)?, + token_addr: staking_config.deposit_token_addr, + xtoken_addr: staking_config.share_token_addr, + generator_controller_addr: deps.api.addr_validate(&msg.generator_controller_addr)?, + ibc_timeout_seconds: msg.ibc_timeout_seconds, + }; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default()) +} + +/// Migrates the contract to a new version. +#[entry_point] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Err(ContractError::MigrationError {}) +} + +#[cfg(test)] +mod tests { + + use super::*; + + use crate::{ + contract::instantiate, + mock::{mock_all, ASSEMBLY, CW20ICS20, GENERATOR_CONTROLLER, OWNER, STAKING}, + }; + + // Test Cases: + // + // Expect Success + // - Invalid IBC timeouts are rejected + // + #[test] + fn invalid_ibc_timeout() { + let (mut deps, env, info) = mock_all(OWNER); + + // Test MAX + 1 + let ibc_timeout_seconds = MAX_IBC_TIMEOUT_SECONDS + 1; + let err = instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::InvalidIBCTimeout { + timeout: ibc_timeout_seconds, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS + } + ); + + // Test MIN - 1 + let ibc_timeout_seconds = MIN_IBC_TIMEOUT_SECONDS - 1; + let err = instantiate( + deps.as_mut(), + env, + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::InvalidIBCTimeout { + timeout: ibc_timeout_seconds, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS + } + ); + } +} diff --git a/contracts/hub/src/error.rs b/contracts/hub/src/error.rs new file mode 100644 index 00000000..b1bb819b --- /dev/null +++ b/contracts/hub/src/error.rs @@ -0,0 +1,70 @@ +use cosmwasm_std::{OverflowError, StdError}; +use serde_json_wasm::de::Error as SerdeError; +use thiserror::Error; + +/// This enum describes Hub's contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unable to parse: {0}")] + ParseError(#[from] std::num::ParseIntError), + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("You can not send 0 tokens")] + ZeroAmount {}, + + #[error("Insufficient funds held for the action")] + InsufficientFunds {}, + + #[error("The provided address does not have any funds")] + NoFunds {}, + + #[error("Voting power exceeds channel balance")] + InvalidVotingPower {}, + + #[error("The action {} is not allowed via an IBC memo", action)] + NotMemoAction { action: String }, + + #[error( + "The action {} is not allowed via IBC and must be actioned via a tranfer memo", + action + )] + NotIBCAction { action: String }, + + #[error("Memo does not conform to the expected format: {}", reason)] + InvalidMemo { reason: SerdeError }, + + #[error("Memo was not intended for the hook contract")] + InvalidDestination {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Invalid submessage {0}", reason)] + InvalidSubmessage { reason: String }, + + #[error("Outpost already added, remove it first: {0}", address)] + OutpostAlreadyAdded { address: String }, + + #[error("No Outpost found that matches the message channels")] + UnknownOutpost {}, + + #[error("Invalid IBC timeout: {timeout}, must be between {min} and {max} seconds")] + InvalidIBCTimeout { timeout: u64, min: u64, max: u64 }, + + #[error("Channel already established: {channel_id}")] + ChannelAlreadyEstablished { channel_id: String }, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} diff --git a/contracts/hub/src/execute.rs b/contracts/hub/src/execute.rs new file mode 100644 index 00000000..e0685ff1 --- /dev/null +++ b/contracts/hub/src/execute.rs @@ -0,0 +1,1602 @@ +use cosmwasm_std::{ + entry_point, from_binary, to_binary, Addr, DepsMut, Env, MessageInfo, Response, StdError, + StdResult, SubMsg, WasmMsg, +}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; + +use astroport::{ + common::{claim_ownership, drop_ownership_proposal, propose_new_owner}, + querier::query_token_balance, +}; +use astroport_governance::{ + hub::{Config, Cw20HookMsg, ExecuteMsg}, + interchain::{Hub, MAX_IBC_TIMEOUT_SECONDS, MIN_IBC_TIMEOUT_SECONDS}, + utils::check_contract_supports_channel, +}; + +use crate::{ + error::ContractError, + reply::STAKE_ID, + state::{ + OutpostChannels, ReplyData, CONFIG, OUTPOSTS, OWNERSHIP_PROPOSAL, REPLY_DATA, USER_FUNDS, + }, +}; + +/// Exposes all the execute functions available in the contract. +/// +/// ## Execute messages +/// * **ExecuteMsg::Receive(msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes +/// it depending on the received template. +/// +/// * **ExecuteMsg::UpdateConfig { ibc_timeout_seconds }** Update parameters in the Hub contract. Only the owner is allowed to +/// update the config +/// +/// * **ExecuteMsg::AddOutpost { outpost_addr, cw20_ics20_channel }** Add an Outpost to the contract, +/// allowing new IBC connections and IBC messages +/// +/// * **ExecuteMsg::RemoveOutpost { outpost_addr }** Removes an Outpost from the contract, +/// blocking new IBC connections as well as any IBC messages +/// +/// * **ExecuteMsg::ProposeNewOwner { new_owner, expires_in }** Creates a new request to change +/// contract ownership. +/// +/// * **ExecuteMsg::DropOwnershipProposal {}** Removes a request to change contract ownership. +/// +/// * **ExecuteMsg::ClaimOwnership {}** Claims contract ownership. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::UpdateConfig { + ibc_timeout_seconds, + } => update_config(deps, info, ibc_timeout_seconds), + ExecuteMsg::AddOutpost { + outpost_addr, + outpost_channel, + cw20_ics20_channel, + } => add_outpost( + deps, + env, + info, + outpost_addr, + outpost_channel, + cw20_ics20_channel, + ), + ExecuteMsg::RemoveOutpost { outpost_addr } => remove_outpost(deps, info, outpost_addr), + ExecuteMsg::ProposeNewOwner { + new_owner, + expires_in, + } => { + let config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + new_owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config: Config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on +/// the received template +/// +/// Funds received here must be from the CW20-ICS20 contract and is used for +/// actions initiated from an Outpost that require ASTRO tokens +/// +/// * **cw20_msg** CW20 message to process +fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // We only allow ASTRO tokens to be sent here + if info.sender != config.token_addr { + return Err(ContractError::Unauthorized {}); + } + + // The sender of the ASTRO tokens must be the CW20-ICS20 contract + if cw20_msg.sender != config.cw20_ics20_addr { + return Err(ContractError::Unauthorized {}); + } + + // We can't do anything with no tokens + if cw20_msg.amount.is_zero() { + return Err(ContractError::ZeroAmount {}); + } + + // Match the CW20 template + match from_binary(&cw20_msg.msg)? { + Cw20HookMsg::OutpostMemo { + channel, + sender, + receiver, + memo, + } => handle_outpost_memo(deps, env, cw20_msg, channel, sender, receiver, memo), + Cw20HookMsg::TransferFailure { receiver } => { + handle_transfer_failure(deps, info, cw20_msg, receiver) + } + } +} + +/// Handle the JSON memo from an Outpost by matching against the available +/// actions. +/// +/// If the memo is not in a valid format for the actions it is +/// considered invalid. +/// +/// If the memo wasn't intended for us we forward it to the original +/// intended receiver +fn handle_outpost_memo( + deps: DepsMut, + env: Env, + msg: Cw20ReceiveMsg, + receiving_channel: String, + original_sender: String, + original_receiver: String, + memo: String, +) -> Result { + // If the receiver is not our contract we assume this is incorrect and fail + // the transfer, causing the funds to be returned to the sender on the + // original chain + if env.contract.address != original_receiver { + return Err(ContractError::InvalidDestination {}); + } + + // But if this was intended for us, parse and handle the memo + let sub_msg: SubMsg = match serde_json_wasm::from_str::(memo.as_str()) { + Ok(hub) => match hub { + Hub::Stake {} => handle_stake_instruction( + deps, + env, + msg, + receiving_channel, + original_sender.clone(), + )?, + _ => { + return Err(ContractError::NotMemoAction { + action: hub.to_string(), + }) + } + }, + Err(reason) => { + // This memo doesn't match any of our action formats + // In case the receiver is set to our handler contract we + // assume the funds were intended to have a valid action but + // are invalid, thus we need to fail the transaction and return + // the funds + return Err(ContractError::InvalidMemo { reason }); + } + }; + + Ok(Response::default() + .add_submessage(sub_msg) + .add_attribute("hub", "handle_memo") + .add_attribute("memo_type", "instruction") + .add_attribute("sender", original_sender)) +} + +/// Handle a stake instruction sent via memo from an Outpost +/// +/// The full amount is staked and the resulting xASTRO is sent to the +/// original sender on the Outpost +fn handle_stake_instruction( + deps: DepsMut, + env: Env, + msg: Cw20ReceiveMsg, + receiving_channel: String, + original_sender: String, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Stake all the received ASTRO tokens + // We need a SubMessage here to ensure we only mint the actual + // amount of ASTRO that was staked, which *might* not the full amount sent + let enter_msg = astroport::staking::Cw20HookMsg::Enter {}; + let send_msg = Cw20ExecuteMsg::Send { + contract: config.staking_addr.to_string(), + amount: msg.amount, + msg: to_binary(&enter_msg)?, + }; + + // Execute the message, we're using a CW20, so no funds added here + let stake_msg = WasmMsg::Execute { + contract_addr: config.token_addr.to_string(), + msg: to_binary(&send_msg)?, + funds: vec![], + }; + + let current_xastro_balance = query_token_balance( + &deps.querier, + config.xtoken_addr.to_string(), + env.contract.address, + )?; + + // Temporarily save the data needed for the SubMessage reply + let reply_data = ReplyData { + receiver: original_sender, + receiving_channel, + value: current_xastro_balance, + original_value: msg.amount, + }; + REPLY_DATA.save(deps.storage, &reply_data)?; + + Ok(SubMsg::reply_on_success(stake_msg, STAKE_ID)) +} + +/// Update the Hub config +fn update_config( + deps: DepsMut, + info: MessageInfo, + ibc_timeout_seconds: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Only owner can update the config + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + if let Some(ibc_timeout_seconds) = ibc_timeout_seconds { + if !(MIN_IBC_TIMEOUT_SECONDS..=MAX_IBC_TIMEOUT_SECONDS).contains(&ibc_timeout_seconds) { + return Err(ContractError::InvalidIBCTimeout { + timeout: ibc_timeout_seconds, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS, + }); + } + config.ibc_timeout_seconds = ibc_timeout_seconds; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default()) +} + +/// Add an Outpost to the Hub +/// +/// Adding an Outpost requires the Outpost address and the CW20-ICS20 channel +/// where funds will be sent through. Adding an Outpost will allow a new IBC +/// channel to be established with the Outpost and the Hub +fn add_outpost( + deps: DepsMut, + env: Env, + info: MessageInfo, + outpost_addr: String, + outpost_channel: String, + cw20_ics20_channel: String, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Only owner can add Outposts + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + if OUTPOSTS.has(deps.storage, &outpost_addr) { + return Err(ContractError::OutpostAlreadyAdded { + address: outpost_addr, + }); + } + + // Check if the channel is supported in the CW20-ICS20 contract + check_contract_supports_channel(deps.querier, &config.cw20_ics20_addr, &cw20_ics20_channel)?; + // Check that the Hub supports the Outpost channel + check_contract_supports_channel(deps.querier, &env.contract.address, &outpost_channel)?; + + let outpost = OutpostChannels { + outpost: outpost_channel, + cw20_ics20: cw20_ics20_channel.clone(), + }; + + // Store the CW20-ICS20 transfer channel for the Outpost + OUTPOSTS.save(deps.storage, &outpost_addr, &outpost)?; + + Ok(Response::default() + .add_attribute("action", "add_outpost") + .add_attribute("address", outpost_addr) + .add_attribute("cw20_ics20_channel", cw20_ics20_channel)) +} + +/// Remove an Outpost from the Hub +/// +/// Removing an Outpost will block new IBC channels to be established between the +/// Hub and the provided Outpost. All IBC messages will also fail +/// +/// IMPORTANT: This does not close any existing IBC channels +fn remove_outpost( + deps: DepsMut, + info: MessageInfo, + outpost_addr: String, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Only owner can remove Outposts + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + OUTPOSTS.remove(deps.storage, &outpost_addr); + + Ok(Response::default() + .add_attribute("action", "remove_outpost") + .add_attribute("address", outpost_addr)) +} + +/// Handle failed CW20-ICS20 IBC transfers +/// +/// If a CW20-ICS20 IBC transfer fails that we initiated, we receive the original +/// tokens back and need to store them for the user to retrieve manually +/// +/// Once funds are held here, the original user will need to issue a withdraw +/// transaction on the Outpost to retrieve their funds. +fn handle_transfer_failure( + deps: DepsMut, + info: MessageInfo, + msg: Cw20ReceiveMsg, + receiver: String, +) -> Result { + let user_addr = Addr::unchecked(&receiver); + USER_FUNDS.update(deps.storage, &user_addr, |balance| -> StdResult<_> { + Ok(balance.unwrap_or_default().checked_add(msg.amount)?) + })?; + + Ok(Response::default() + .add_attribute("outpost_handler", "handle_transfer_failure") + .add_attribute("sender", info.sender) + .add_attribute("og_receiver", receiver)) +} + +#[cfg(test)] +mod tests { + use super::*; + use astroport_governance::{hub::HubBalance, interchain::Outpost}; + use cosmwasm_std::{ + testing::{mock_info, MOCK_CONTRACT_ADDR}, + IbcEndpoint, IbcMsg, IbcPacket, IbcPacketTimeoutMsg, Reply, ReplyOn, SubMsgResponse, + SubMsgResult, Uint128, Uint64, + }; + use serde_json_wasm::de::Error as SerdeError; + + use crate::{ + contract::instantiate, + execute::execute, + ibc::ibc_packet_timeout, + mock::{ + mock_all, setup_channel, ASSEMBLY, ASTRO_TOKEN, CW20ICS20, GENERATOR_CONTROLLER, OWNER, + STAKING, XASTRO_TOKEN, + }, + query::query, + reply::{reply, UNSTAKE_ID}, + }; + + // Test Cases: + // + // Expect Success + // - Adding and removing Outposts work correctly + // + // Expect Error + // - Adding an Outpost with duplicate address + // - Adding an Outpost when not the owner + // - Removing an Outpost when not the owner + // + #[test] + fn add_remove_outpost() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "wasm1contractaddress1".to_string(), + outpost_channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "wasm1contractaddress2".to_string(), + outpost_channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-2".to_string(), + }, + ) + .unwrap(); + + // Test paging, should return a single result + let outposts = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::Outposts { + start_after: None, + limit: Some(1), + }, + ) + .unwrap(); + + assert_eq!( + outposts, + to_binary(&vec![astroport_governance::hub::OutpostConfig { + address: "wasm1contractaddress1".to_string(), + channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + },]) + .unwrap() + ); + + // Test paging, should return a single result of the second item + let outposts = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::Outposts { + start_after: Some("wasm1contractaddress1".to_string()), + limit: Some(1), + }, + ) + .unwrap(); + + assert_eq!( + outposts, + to_binary(&vec![astroport_governance::hub::OutpostConfig { + address: "wasm1contractaddress2".to_string(), + channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-2".to_string(), + },]) + .unwrap() + ); + + // Get all + let outposts = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::Outposts { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!( + outposts, + to_binary(&vec![ + astroport_governance::hub::OutpostConfig { + address: "wasm1contractaddress1".to_string(), + channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + astroport_governance::hub::OutpostConfig { + address: "wasm1contractaddress2".to_string(), + channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-2".to_string(), + } + ]) + .unwrap() + ); + + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::RemoveOutpost { + outpost_addr: "wasm1contractaddress1".to_string(), + }, + ) + .unwrap(); + + let outposts = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::Outposts { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!( + outposts, + to_binary(&vec![astroport_governance::hub::OutpostConfig { + address: "wasm1contractaddress2".to_string(), + channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-2".to_string(), + },]) + .unwrap() + ); + + // Must not allow duplicate Outpost addresses + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "wasm1contractaddress2".to_string(), + outpost_channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-2".to_string(), + }, + ) + .unwrap_err(); + assert!(matches!( + err, + ContractError::OutpostAlreadyAdded { address: _ } + )); + + // Must not allow adding if not the owner + let err = execute( + deps.as_mut(), + env.clone(), + mock_info("not_owner", &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "wasm1contractaddress3".to_string(), + outpost_channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-4".to_string(), + }, + ) + .unwrap_err(); + assert!(matches!(err, ContractError::Unauthorized {})); + + // Must not allow removing if not the owner + let err = execute( + deps.as_mut(), + env, + mock_info("not_owner", &[]), + astroport_governance::hub::ExecuteMsg::RemoveOutpost { + outpost_addr: "wasm1contractaddress2".to_string(), + }, + ) + .unwrap_err(); + assert!(matches!(err, ContractError::Unauthorized {})); + } + + // Test Cases: + // + // Expect Success + // - Updating config works + // + // Expect Error + // - Updating config with invalid addresses + // - Updating config when not the owner + // + #[test] + fn update_config() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + let config = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::Config {}, + ) + .unwrap(); + + // Ensure the config set during instantiation is correct + assert_eq!( + config, + to_binary(&astroport_governance::hub::Config { + owner: Addr::unchecked(OWNER), + assembly_addr: Addr::unchecked(ASSEMBLY), + cw20_ics20_addr: Addr::unchecked(CW20ICS20), + staking_addr: Addr::unchecked(STAKING), + token_addr: Addr::unchecked(ASTRO_TOKEN), + xtoken_addr: Addr::unchecked(XASTRO_TOKEN), + generator_controller_addr: Addr::unchecked(GENERATOR_CONTROLLER), + ibc_timeout_seconds: 10, + }) + .unwrap() + ); + + // Update the IBC timeout to a value below min + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::UpdateConfig { + ibc_timeout_seconds: Some(MIN_IBC_TIMEOUT_SECONDS - 1), + }, + ) + .unwrap_err(); + assert!(matches!( + err, + ContractError::InvalidIBCTimeout { + timeout: _, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS + } + )); + + // Update the IBC timeout to a value below max + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::UpdateConfig { + ibc_timeout_seconds: Some(MAX_IBC_TIMEOUT_SECONDS + 1), + }, + ) + .unwrap_err(); + assert!(matches!( + err, + ContractError::InvalidIBCTimeout { + timeout: _, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS + } + )); + + // Update the IBC timeout to a correct value + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::UpdateConfig { + ibc_timeout_seconds: Some(50), + }, + ) + .unwrap(); + // Query the new config + let config = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::Config {}, + ) + .unwrap(); + + assert_eq!( + config, + to_binary(&astroport_governance::hub::Config { + owner: Addr::unchecked(OWNER), + assembly_addr: Addr::unchecked(ASSEMBLY), + cw20_ics20_addr: Addr::unchecked(CW20ICS20), + staking_addr: Addr::unchecked(STAKING), + token_addr: Addr::unchecked(ASTRO_TOKEN), + xtoken_addr: Addr::unchecked(XASTRO_TOKEN), + generator_controller_addr: Addr::unchecked(GENERATOR_CONTROLLER), + ibc_timeout_seconds: 50, + }) + .unwrap() + ); + + // Must not allow updating if not the owner + let err = execute( + deps.as_mut(), + env, + mock_info("not_owner", &[]), + astroport_governance::hub::ExecuteMsg::UpdateConfig { + ibc_timeout_seconds: Some(200), + }, + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::Unauthorized {})); + } + + // Test Cases: + // + // Expect Success + // - Sending the funds results in correct balances + // + // Expect Error + // - When not sent by the CW20-ICS20 contract + // - When tokens are not ASTRO + // - When amount is zero + // + // This tests that balances are correctly tracked by the contract in case of + // IBC failures that result in funds getting stuck on the Hub + #[test] + fn cw20_ics20_transfer_failure() { + let (mut deps, env, info) = mock_all(OWNER); + + let user1 = "user1"; + let user2 = "user2"; + let user1_funds = Uint128::from(100u128); + let user2_funds = Uint128::from(300u128); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Add an allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-2".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Transfer failures are only allowed to be recorded when sent by the + // CW20-ICS20 contract and if the tokens are ASTRO + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: "not_cw20_ics20".to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + receiver: user1.to_owned(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::Unauthorized {})); + + // Transfer failures must only accept ASTRO tokens + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(CW20ICS20, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + receiver: user1.to_owned(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::Unauthorized {})); + + // Transfer failures will must not accept zero amounts + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: Uint128::zero(), + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + receiver: user1.to_owned(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::ZeroAmount {})); + + // Add a valid failure for user + execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + receiver: user1.to_owned(), + }) + .unwrap(), + }), + ) + .unwrap(); + + // Verify that the amount was added to the user's balance + let balance = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::UserFunds { + user: Addr::unchecked(user1), + }, + ) + .unwrap(); + + assert_eq!( + balance, + to_binary(&HubBalance { + balance: user1_funds + }) + .unwrap() + ); + + execute( + deps.as_mut(), + env, + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user2_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + receiver: user2.to_owned(), + }) + .unwrap(), + }), + ) + .unwrap(); + + // Verify that the amount was added to the user's balance + let stuck_funds = USER_FUNDS + .load(&deps.storage, &Addr::unchecked(user2)) + .unwrap(); + + assert_eq!(stuck_funds, user2_funds); + } + + // Test Cases: + // + // Expect Success + // - Memo is sent from authorised CW20-ICS20 contract + // + // Expect Error + // - Memo's sent from anywhere other than the CW20-ICS20 contract + // - Memo's sent with tokens other than ASTRO + // - Memo's sent with no funds + #[test] + fn receive_memo_auth_checks() { + let (mut deps, env, info) = mock_all(OWNER); + + let user1 = "user1"; + let user1_funds = Uint128::from(100u128); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up a valid IBC connection + setup_channel(deps.as_mut(), env.clone()); + + // Add an allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Memo's can only be handled when sent by the CW20-ICS20 contract + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_string(), + memo: "{\"stake\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::Unauthorized {})); + + // Memo's must only accept ASTRO tokens + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(CW20ICS20, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_string(), + memo: "{\"stake\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::Unauthorized {})); + + // Memo will must not accept zero amounts + let err = execute( + deps.as_mut(), + env, + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: Uint128::zero(), + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_string(), + memo: "{\"stake\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::ZeroAmount {})); + } + + // Test Cases: + // + // Expect Success + // - Invalid memo is received and handled + // + // Expect Error + // - Calling this from an unauthorised contract must fail + #[test] + fn receive_invalid_memo() { + let (mut deps, env, info) = mock_all(OWNER); + + let user1 = "user1"; + let user1_funds = Uint128::from(100u128); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up a valid IBC connection + setup_channel(deps.as_mut(), env.clone()); + + // Add an allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Send an invalid memo / broken JSON sent to us must fail + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_string(), + memo: "{\"stak}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!( + err, + ContractError::InvalidMemo { + reason: SerdeError::EofWhileParsingString + } + )); + + // Send an unknown memo action + let err = execute( + deps.as_mut(), + env, + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_string(), + memo: "{\"staking\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!( + err, + ContractError::InvalidMemo { + reason: SerdeError::Custom(_) + } + )); + } + + // Test Cases: + // + // Expect Success + // - Memo wasn't intended for us, forward funds + #[test] + fn receive_standard_transfer_memo() { + let (mut deps, env, info) = mock_all(OWNER); + + let user1 = "user1"; + let user1_funds = Uint128::from(100u128); + let receiving_user = "user2"; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up a valid IBC connection + setup_channel(deps.as_mut(), env.clone()); + + // Add an allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Send an unknown memo action + let err = execute( + deps.as_mut(), + env, + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: receiving_user.to_string(), + memo: "Hello fren, have some ASTRO".to_string(), + }) + .unwrap(), + }), + ) + .unwrap_err(); + + assert!(matches!(err, ContractError::InvalidDestination {})); + } + + // Test Cases: + // + // Expect Success + // - Memo was a staking instruction, stake funds + #[test] + fn receive_stake_memo() { + let (mut deps, env, info) = mock_all(OWNER); + + let user1 = "user1"; + let user1_funds = Uint128::from(100u128); + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up a valid IBC connection + setup_channel(deps.as_mut(), env.clone()); + + // Add an allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Send a valid stake memo + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_owned(), + memo: "{\"stake\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap(); + + // Verify that the stake message matches the expected message + let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: STAKING.to_string(), + amount: user1_funds, + msg: stake_msg, + }) + .unwrap(); + + // Verify that we see a stake message reply + assert_eq!( + res.messages[0], + SubMsg { + id: 9000, + gas_limit: None, + reply_on: ReplyOn::Success, + msg: WasmMsg::Execute { + contract_addr: ASTRO_TOKEN.to_string(), + msg: send_msg, + funds: vec![], + } + .into(), + } + ); + + // Construct the reply from the staking contract that will be returned + // to the contract + let stake_reply = Reply { + id: STAKE_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }; + + let res = reply(deps.as_mut(), env.clone(), stake_reply).unwrap(); + + // We must have one IBC message + assert_eq!(res.messages.len(), 1); + + // Once staked, we mint the xASTRO on the remote chain + let mint_msg = to_binary(&Outpost::MintXAstro { + receiver: user1.to_string(), + amount: user1_funds, + }) + .unwrap(); + + // We should see the IBC message + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: mint_msg, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + + // At this point the channel must have a balance that matches the amount + let balances = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::ChannelBalanceAt { + channel: "channel-3".to_string(), + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let expected = HubBalance { + balance: user1_funds, + }; + + assert_eq!(balances, to_binary(&expected).unwrap()); + + // At this point the total channel balance must have a balance that matches the amount + let total_balance = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::TotalChannelBalancesAt { + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let total_expected = HubBalance { + balance: user1_funds, + }; + + assert_eq!(total_balance, to_binary(&total_expected).unwrap()); + } + + // Test Cases: + // + // Expect Success + // - Memo was a staking instruction, stake funds + #[test] + fn receive_stake_xastro_mint_timeout() { + let (mut deps, env, info) = mock_all(OWNER); + + let user1 = "user1"; + let user1_funds = Uint128::from(100u128); + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up a valid IBC connection + setup_channel(deps.as_mut(), env.clone()); + + // Add an allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Mint some xASTRO that we can trigger a timeout for + // Send a valid stake memo + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_owned(), + memo: "{\"stake\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap(); + + // Verify that the stake message matches the expected message + let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: STAKING.to_string(), + amount: user1_funds, + msg: stake_msg, + }) + .unwrap(); + + // Verify that we see a stake message reply + assert_eq!( + res.messages[0], + SubMsg { + id: 9000, + gas_limit: None, + reply_on: ReplyOn::Success, + msg: WasmMsg::Execute { + contract_addr: ASTRO_TOKEN.to_string(), + msg: send_msg, + funds: vec![], + } + .into(), + } + ); + + // Construct the reply from the staking contract that will be returned + // to the contract + let stake_reply = Reply { + id: STAKE_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }; + + let res = reply(deps.as_mut(), env.clone(), stake_reply).unwrap(); + + // We must have one IBC message + assert_eq!(res.messages.len(), 1); + + // At this point the channel must hold user1_funds of value + let balances = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::ChannelBalanceAt { + channel: "channel-3".to_string(), + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let expected = HubBalance { + balance: user1_funds, + }; + + assert_eq!(balances, to_binary(&expected).unwrap()); + + // At this point the total channel balance must have a balance that matches the amount + let total_balance = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::TotalChannelBalancesAt { + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let total_expected = HubBalance { + balance: user1_funds, + }; + + assert_eq!(total_balance, to_binary(&total_expected).unwrap()); + + // Trigger a timeout on minting xASTRO remotely + let mint_msg = to_binary(&Outpost::MintXAstro { + receiver: user1.to_owned(), + amount: user1_funds, + }) + .unwrap(); + let packet = IbcPacket::new( + mint_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: "wasm.outpost".to_string(), + channel_id: "channel-5".to_string(), + }, + 3, + env.block.time.plus_seconds(ibc_timeout_seconds).into(), + ); + + // When the timeout occurs, we should see an unstake message to return the ASTRO to the user + let timeout_packet = IbcPacketTimeoutMsg::new(packet, Addr::unchecked("relayer")); + let res = ibc_packet_timeout(deps.as_mut(), env.clone(), timeout_packet).unwrap(); + + // Should have exactly one message + assert_eq!(res.messages.len(), 1); + + // Verify that the unstake message matches the expected message + let unstake_msg = to_binary(&astroport::staking::Cw20HookMsg::Leave {}).unwrap(); + let send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: STAKING.to_string(), + amount: user1_funds, + msg: unstake_msg, + }) + .unwrap(); + + // We should see the unstake SubMessagge + assert_eq!( + res.messages[0], + SubMsg { + id: 9001, + gas_limit: None, + reply_on: ReplyOn::Success, + msg: WasmMsg::Execute { + contract_addr: XASTRO_TOKEN.to_string(), + msg: send_msg, + funds: vec![], + } + .into(), + } + ); + + // At this point the channel must still hold the tokens + let balances = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::ChannelBalanceAt { + channel: "channel-3".to_string(), + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let expected = HubBalance { + balance: user1_funds, + }; + + assert_eq!(balances, to_binary(&expected).unwrap()); + + // And the total must still match + let total_balance = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::TotalChannelBalancesAt { + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let total_expected = HubBalance { + balance: user1_funds, + }; + + assert_eq!(total_balance, to_binary(&total_expected).unwrap()); + + // Construct the reply from the staking contract that will be returned + // to the contract + let unstake_reply = Reply { + id: UNSTAKE_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }; + + let res = reply(deps.as_mut(), env.clone(), unstake_reply).unwrap(); + + // We must have one CW20-ICS20 transfer message + assert_eq!(res.messages.len(), 1); + + // At this point the channel must have a zero balance after minting remotely + // failed and the tokens were unstaked + let balances = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::ChannelBalanceAt { + channel: "channel-3".to_string(), + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let expected = HubBalance { + balance: Uint128::zero(), + }; + + assert_eq!(balances, to_binary(&expected).unwrap()); + + // And now it shoul be zero + let total_balance = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::TotalChannelBalancesAt { + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let total_expected = HubBalance { + balance: Uint128::zero(), + }; + + assert_eq!(total_balance, to_binary(&total_expected).unwrap()); + + // The rest of the unstaking flow is covered in ibc_staking tests + } +} diff --git a/contracts/hub/src/ibc.rs b/contracts/hub/src/ibc.rs new file mode 100644 index 00000000..fcbf9883 --- /dev/null +++ b/contracts/hub/src/ibc.rs @@ -0,0 +1,678 @@ +use astroport::querier::query_token_balance; +use cosmwasm_std::{ + entry_point, from_binary, to_binary, Deps, DepsMut, Env, Ibc3ChannelOpenResponse, + IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcChannelOpenResponse, IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, + IbcReceiveResponse, Never, StdError, StdResult, SubMsg, +}; + +use astroport_governance::interchain::{get_contract_from_ibc_port, Hub, Outpost, Response}; + +use crate::{ + error::ContractError, + ibc_governance::{ + handle_ibc_blacklisted, handle_ibc_cast_assembly_vote, handle_ibc_cast_emissions_vote, + handle_ibc_unlock, + }, + ibc_misc::handle_ibc_withdraw_stuck_funds, + ibc_query::handle_ibc_query_proposal, + ibc_staking::{construct_unstake_msg, handle_ibc_unstake}, + reply::UNSTAKE_ID, + state::{ReplyData, CONFIG, OUTPOSTS, REPLY_DATA}, +}; + +pub const IBC_APP_VERSION: &str = "astroport-outpost-v1"; +pub const IBC_ORDERING: IbcOrder = IbcOrder::Unordered; + +/// Handle the opening of a new IBC channel +/// +/// We verify that the connection is using the correct configuration +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_open( + _deps: DepsMut, + _env: Env, + msg: IbcChannelOpenMsg, +) -> Result { + let channel = msg.channel(); + + if channel.order != IBC_ORDERING { + return Err(ContractError::Std(StdError::generic_err( + "Ordering is invalid. The channel must be unordered".to_string(), + ))); + } + if channel.version != IBC_APP_VERSION { + return Err(ContractError::Std(StdError::generic_err(format!( + "Must set version to `{IBC_APP_VERSION}`" + )))); + } + + if let Some(counter_version) = msg.counterparty_version() { + if counter_version != IBC_APP_VERSION { + return Err(ContractError::Std(StdError::generic_err(format!( + "Counterparty version must be `{IBC_APP_VERSION}`" + )))); + } + } + + Ok(Some(Ibc3ChannelOpenResponse { + version: IBC_APP_VERSION.to_string(), + })) +} + +/// Handle the connection of a new IBC channel +/// +/// We verify that the connection is being made to an allowed Outpost and +/// if the channel has not been set, add it +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_connect( + deps: DepsMut, + _env: Env, + msg: IbcChannelConnectMsg, +) -> Result { + let channel = msg.channel(); + + if let Some(counter_version) = msg.counterparty_version() { + if counter_version != IBC_APP_VERSION { + return Err(ContractError::Std(StdError::generic_err(format!( + "Counterparty version must be `{IBC_APP_VERSION}`" + )))); + } + } + + // We allow any contract with any channel to connect, but we will only + // allow messages from whitelisted Outposts to be accepted + // If a channel has already been established, we will reject the connection + let counterparty_port = + get_contract_from_ibc_port(channel.counterparty_endpoint.port_id.as_str()); + if let Some(channels) = OUTPOSTS.may_load(deps.storage, counterparty_port)? { + return Err(ContractError::ChannelAlreadyEstablished { + channel_id: channels.outpost, + }); + } + + Ok(IbcBasicResponse::new() + .add_attribute("action", "ibc_connect") + .add_attribute("channel_id", &channel.endpoint.channel_id)) +} + +/// Handle the receiving the packets while wrapping the actual call to provide +/// returning errors as an acknowledgement. +/// +/// This allows the original caller from another chain to handle the failure +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_receive( + deps: DepsMut, + env: Env, + msg: IbcPacketReceiveMsg, +) -> Result { + do_packet_receive(deps, env, msg).or_else(|err| { + // Construct an error acknowledgement that can be handled on the Outpost + let ack_data = to_binary(&Response::new_error(err.to_string())).unwrap(); + + Ok(IbcReceiveResponse::new() + .add_attribute("action", "ibc_packet_receive") + .add_attribute("error", err.to_string()) + .set_ack(ack_data)) + }) +} + +/// Process the received packet and return the response +/// +/// Packets are expected to be wrapped in the Hub format, if it doesn't conform +/// it will be failed. +/// +/// If a ContractError is returned, it will be wrapped into a Response +/// containing the error to be handled on the Outpost +fn do_packet_receive( + deps: DepsMut, + env: Env, + msg: IbcPacketReceiveMsg, +) -> Result { + block_unauthorized_packets( + deps.as_ref(), + msg.packet.src.port_id.clone(), + msg.packet.dest.channel_id.to_string(), + )?; + + // Parse the packet data into a Hub message + let outpost_msg: Hub = from_binary(&msg.packet.data)?; + match outpost_msg { + Hub::QueryProposal { id } => handle_ibc_query_proposal(deps, id), + Hub::CastAssemblyVote { + proposal_id, + voter, + vote_option, + voting_power, + } => handle_ibc_cast_assembly_vote( + deps, + msg.packet.dest.channel_id, + proposal_id, + voter, + vote_option, + voting_power, + ), + Hub::CastEmissionsVote { + voter, + voting_power, + votes, + } => handle_ibc_cast_emissions_vote( + deps, + env, + msg.packet.dest.channel_id, + voter, + voting_power, + votes, + ), + Hub::Unstake { receiver, amount } => { + handle_ibc_unstake(deps, env, msg.packet.dest.channel_id, receiver, amount) + } + Hub::KickUnlockedVoter { voter } => handle_ibc_unlock(deps, voter), + Hub::KickBlacklistedVoter { voter } => handle_ibc_blacklisted(deps, voter), + Hub::WithdrawFunds { user } => { + handle_ibc_withdraw_stuck_funds(deps, msg.packet.dest.channel_id, user) + } + _ => Err(ContractError::NotIBCAction { + action: outpost_msg.to_string(), + }), + } +} + +/// Handle IBC packet timeouts for messages we sent +/// +/// Timeouts will cause certain actions to be reversed and, when applicable, return +/// funds to the user +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_timeout( + deps: DepsMut, + env: Env, + msg: IbcPacketTimeoutMsg, +) -> Result { + let failed_msg: Outpost = from_binary(&msg.packet.data)?; + match failed_msg { + Outpost::MintXAstro { receiver, amount } => { + let config = CONFIG.load(deps.storage)?; + + // If we get a timeout on a packet to mint remote xASTRO + // we need to undo the transaction and return the original ASTRO + // If we get another timeout returning the original ASTRO the funds + // will be held in this contract to withdraw later + let wasm_msg = construct_unstake_msg( + deps.storage, + deps.querier, + env.clone(), + msg.packet.src.channel_id.clone(), + receiver.clone(), + amount, + )?; + let sub_msg = SubMsg::reply_on_success(wasm_msg, UNSTAKE_ID); + + // We don't decrease the channel balance here, but only after unstaking + let current_astro_balance = query_token_balance( + &deps.querier, + config.token_addr.to_string(), + env.contract.address, + )?; + + // Temporarily save the data needed for the SubMessage reply + let reply_data = ReplyData { + receiver: receiver.clone(), + receiving_channel: msg.packet.src.channel_id, + value: current_astro_balance, + original_value: amount, + }; + REPLY_DATA.save(deps.storage, &reply_data)?; + + Ok(IbcBasicResponse::new() + .add_attribute("action", "ibc_packet_timeout") + .add_submessage(sub_msg) + .add_attribute("original_action", "mint_remote_xastro") + .add_attribute("original_receiver", receiver) + .add_attribute("original_amount", amount.to_string())) + } + } +} + +/// Handle IBC packet acknowledgements for messages we sent +/// +/// We don't need acks for now, we handle failures instead +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_ack( + _deps: DepsMut, + _env: Env, + _msg: IbcPacketAckMsg, +) -> Result { + Ok(IbcBasicResponse::new().add_attribute("action", "ibc_packet_ack")) +} + +/// Handle the closing of IBC channels, which we don't allow +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_close( + _deps: DepsMut, + _env: Env, + _channel: IbcChannelCloseMsg, +) -> StdResult { + Err(StdError::generic_err("Closing channel is not allowed")) +} + +/// Checks the provided port against the Outpost list. +/// +/// If the port doesn't exist or the channel doesn't match, this function will +/// return an error, effectively blocking the packet. +fn block_unauthorized_packets( + deps: Deps, + source_port_id: String, + destination_channel_id: String, +) -> Result<(), ContractError> { + let counterparty_port = get_contract_from_ibc_port(source_port_id.as_str()); + + let outpost_channels = OUTPOSTS.load(deps.storage, counterparty_port)?; + if outpost_channels.outpost != destination_channel_id { + return Err(ContractError::Unauthorized {}); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::{ + testing::{mock_info, MOCK_CONTRACT_ADDR}, + Addr, IbcAcknowledgement, IbcEndpoint, IbcPacket, Uint128, + }; + + use super::*; + use crate::{ + contract::instantiate, + execute::execute, + mock::{ + mock_all, mock_channel, mock_ibc_packet, setup_channel, ASSEMBLY, CW20ICS20, + GENERATOR_CONTROLLER, OWNER, STAKING, + }, + }; + + // Test Cases: + // + // Expect Success + // - Creating a channel with correct settings + // + // Expect Error + // - Attempt to create a channel with an invalid version + // - Attempt to create a channel with an invalid ordering + // - Attempt to create a channel before registering an Outpost + // - Attempt to create a channel with an unauthorize Outpost address + #[test] + fn ibc_open_channel() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // A connection with invalid ordering is not allowed + let channel = mock_channel( + "wasm.hub", + "channel-2", + "wasm.unknown_contract", + "channel-7", + IbcOrder::Ordered, + "non-astroport-v1", + ); + let open_msg = IbcChannelOpenMsg::new_init(channel); + let err = ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err( + "Ordering is invalid. The channel must be unordered" + )) + ); + + // A connection with invalid version is not allowed + let channel = mock_channel( + "wasm.hub", + "channel-2", + "wasm.unknown_contract", + "channel-7", + IbcOrder::Unordered, + "non-astroport-v1", + ); + let open_msg = IbcChannelOpenMsg::new_init(channel); + let err = ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err( + "Must set version to `astroport-outpost-v1`" + )) + ); + + // A connection with correct settings is allowed + let channel = mock_channel( + "wasm.hub", + "channel-2", + "wasm.unknown_contract", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + let open_msg = IbcChannelOpenMsg::new_init(channel); + ibc_channel_open(deps.as_mut(), env, open_msg).unwrap(); + + // let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + // ibc_channel_connect(deps.as_mut(), env.clone(), connect_msg).unwrap(); + } + + // Test Cases: + // + // Expect Success + // - Creating a channel with an allowed Outpost + // + // Expect Error + // - Attempt to connect a channel with an invalid version + // - Attempt to connect a channel before registering an Outpost + // - Attempt to connect a channel with an unauthorize Outpost address + #[test] + fn ibc_connect_channel() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Opening a connection with unknown contracts is allowed + let channel = mock_channel( + "wasm.hub", + "channel-2", + "wasm.unknown_contract", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + // This should pass + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + ibc_channel_connect(deps.as_mut(), env.clone(), connect_msg).unwrap(); + + // Now set the allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Attempting to connect again should now fail + let channel = mock_channel( + "wasm.hub", + "channel-3", + "wasm.outpost", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + let err = ibc_channel_connect(deps.as_mut(), env, connect_msg).unwrap_err(); + + assert_eq!( + err, + ContractError::ChannelAlreadyEstablished { + channel_id: "channel-3".to_string() + } + ); + } + + // Test Cases: + // + // Expect Success + // - Packets are acknowledged without error + #[test] + fn ibc_ack_packet() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // The Hub doesn't do anything with acks, we just check that + // it doesn't fail + let ack = IbcAcknowledgement::new( + to_binary(&Response::Result { + action: None, + address: None, + error: None, + }) + .unwrap(), + ); + let mint_msg = to_binary(&Outpost::MintXAstro { + receiver: "user".to_owned(), + amount: Uint128::one(), + }) + .unwrap(); + let original_packet = IbcPacket::new( + mint_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: "wasm.outpost".to_string(), + channel_id: "channel-3".to_string(), + }, + 3, + env.block.time.plus_seconds(10).into(), + ); + + let ack_msg = IbcPacketAckMsg::new(ack, original_packet, Addr::unchecked("relayer")); + ibc_packet_ack(deps.as_mut(), env, ack_msg).unwrap(); + } + + // Test Cases: + // + // Expect Success + // - Creating a channel with an allowed Outpost + // + // Expect Error + // - Attempt to connect a channel with an invalid version + // - Attempt to connect a channel before registering an Outpost + // - Attempt to connect a channel with an unauthorize Outpost address + #[test] + fn ibc_close_channel() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up a valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add an allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + let channel = mock_channel( + "wasm.hub", + "channel-3", + "wasm.outpost", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + + let close_msg = IbcChannelCloseMsg::new_init(channel); + let err = ibc_channel_close(deps.as_mut(), env, close_msg).unwrap_err(); + + assert_eq!(err, StdError::generic_err("Closing channel is not allowed")); + } + + // Test Cases: + // + // Expect Success + // - Only packets from the whitelisted Outpost contract and channel are allowed + // + // Expect Error + // - Attempt to send a packet from an invalid counterparty port + // - Attempt to send a packet from a valid port but invalid channel + #[test] + fn ibc_check_receive_auth() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Create a random channel + // Creating an unauthorised channel is allowed + let channel = mock_channel( + "wasm.hub", + "channel-100", + "wasm.outpost", + "channel-150", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + ibc_channel_connect(deps.as_mut(), env.clone(), connect_msg).unwrap(); + + // Attempt to unstake via the unauthorised channel + // This must always fail as the port and channel is not whitelisted + // We don't need to test every type of Hub message as the safety check + // happens in do_packet_receive which is the entrypoint for all messages + // being received + let ibc_unstake_msg = to_binary(&Hub::Unstake { + receiver: "unstaker".to_string(), + amount: Uint128::from(100u128), + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-100", ibc_unstake_msg.clone()); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); + let ack: Response = from_binary(&res.acknowledgement).unwrap(); + match ack { + Response::Result { error, .. } => { + assert!( + error == Some("astroport_hub::state::OutpostChannels not found".to_string()) + ); + } + _ => panic!("Wrong response type"), + } + + // Whitelist the Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-100".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Attempt to unstake again via an unauthorised Outpost + let recv_packet = mock_ibc_packet("channel-55", ibc_unstake_msg.clone()); + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); + let ack: Response = from_binary(&res.acknowledgement).unwrap(); + match ack { + Response::Result { error, .. } => { + assert!(error == Some("Unauthorized".to_string())); + } + _ => panic!("Wrong response type"), + } + + // Attempt to unstake via the authorised Outpost + let recv_packet = mock_ibc_packet("channel-100", ibc_unstake_msg); + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + } +} diff --git a/contracts/hub/src/ibc_governance.rs b/contracts/hub/src/ibc_governance.rs new file mode 100644 index 00000000..29b6e0cd --- /dev/null +++ b/contracts/hub/src/ibc_governance.rs @@ -0,0 +1,727 @@ +use cosmwasm_std::{to_binary, Addr, DepsMut, Env, IbcReceiveResponse, Uint128, WasmMsg}; + +use astroport_governance::{ + assembly::{Proposal, ProposalVoteOption}, + generator_controller_lite, + interchain::Response, +}; + +use crate::{ + error::ContractError, + state::{channel_balance_at, CONFIG}, +}; + +/// Handle an IBC message to cast a vote on an Assembly proposal from an Outpost +/// and return an IBC acknowledgement +/// +/// The Outpost is responsible for checking and sending the voting power of the +/// voter, we add an additional check to make sure that the voting power is not +/// more than the xASTRO minted remotely via this channel +pub fn handle_ibc_cast_assembly_vote( + deps: DepsMut, + outpost_channel: String, + proposal_id: u64, + voter: Addr, + vote_option: ProposalVoteOption, + voting_power: Uint128, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Cast the vote in the Assembly + let vote_msg = astroport_governance::assembly::ExecuteMsg::CastOutpostVote { + proposal_id, + voter: voter.to_string(), + vote: vote_option, + voting_power, + }; + let wasm_msg = WasmMsg::Execute { + contract_addr: config.assembly_addr.to_string(), + msg: to_binary(&vote_msg)?, + funds: vec![], + }; + + // Assert that the voting power does not exceed the xASTRO minted via this channel + // at the time the proposal was created + let proposal: Proposal = deps.querier.query_wasm_smart( + config.assembly_addr, + &astroport_governance::assembly::QueryMsg::Proposal { proposal_id }, + )?; + + let xastro_balance = channel_balance_at(deps.storage, &outpost_channel, proposal.start_time)?; + + if voting_power > xastro_balance { + return Err(ContractError::InvalidVotingPower {}); + } + + // If the vote succeeds, the ack will be sent back to the Outpost + let ack_data = to_binary(&Response::new_success( + "cast_assembly_vote".to_owned(), + voter.to_string(), + ))?; + + Ok(IbcReceiveResponse::new() + .add_message(wasm_msg) + .set_ack(ack_data)) +} + +/// Handle an IBC message to cast a vote on emissions during a voting period +/// from an Outpost and return an IBC acknowledgement +/// +/// The Outpost is responsible for checking and sending the voting power of the +/// voter, we add an additional check to make sure that the voting power is not +/// more than the xASTRO minted remotely via this channel. vxASTRO lite does +/// not boost voting power and must be equal to the deposit +pub fn handle_ibc_cast_emissions_vote( + deps: DepsMut, + env: Env, + outpost_channel: String, + voter: Addr, + voting_power: Uint128, + votes: Vec<(String, u16)>, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Cast the emissions vote + let vote_msg = generator_controller_lite::ExecuteMsg::OutpostVote { + voter: voter.to_string(), + votes, + voting_power, + }; + let msg = WasmMsg::Execute { + contract_addr: config.generator_controller_addr.to_string(), + msg: to_binary(&vote_msg)?, + funds: vec![], + }; + + // Assert that the voting power does not exceed the xASTRO minted via this channel at the current block + let xastro_balance = + channel_balance_at(deps.storage, &outpost_channel, env.block.time.seconds())?; + if voting_power > xastro_balance { + return Err(ContractError::InvalidVotingPower {}); + } + + // If the vote succeeds, the ack will be sent back to the Outpost + let ack_data = to_binary(&Response::new_success( + "cast_emissions_vote".to_owned(), + voter.to_string(), + ))?; + + Ok(IbcReceiveResponse::new().add_message(msg).set_ack(ack_data)) +} + +/// Handle an IBC message to kick an unlocked voter from the Outpost. +/// +/// We rely on the Outpost to verify the unlock before sending it here. If this +/// transaction succeeds, the voting power will be removed immediately +pub fn handle_ibc_unlock(deps: DepsMut, user: Addr) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Remove the vxASTRO voter's voting power + let unlock_msg = generator_controller_lite::ExecuteMsg::KickUnlockedOutpostVoter { + unlocked_voter: user.to_string(), + }; + let msg = WasmMsg::Execute { + contract_addr: config.generator_controller_addr.to_string(), + msg: to_binary(&unlock_msg)?, + funds: vec![], + }; + + // If the unlock succeeds, the ack will be sent back to the Outpost + let ack_data = to_binary(&Response::new_success( + "unlock".to_owned(), + user.to_string(), + ))?; + + Ok(IbcReceiveResponse::new().add_message(msg).set_ack(ack_data)) +} + +/// Handle an IBC message to kick a blacklisted voter from the Outpost. +/// +/// We rely on the Outpost to verify the blacklist before sending it here. If this +/// transaction succeeds, the voting power will be removed immediately +pub fn handle_ibc_blacklisted( + deps: DepsMut, + user: Addr, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Remove the vxASTRO voter's voting power + let blacklist_msg = generator_controller_lite::ExecuteMsg::KickBlacklistedVoters { + blacklisted_voters: vec![user.to_string()], + }; + let msg = WasmMsg::Execute { + contract_addr: config.generator_controller_addr.to_string(), + msg: to_binary(&blacklist_msg)?, + funds: vec![], + }; + + // If the vote succeeds, the ack will be sent back to the Outpost + let ack_data = to_binary(&Response::new_success( + "kick_blacklisted".to_owned(), + user.to_string(), + ))?; + + Ok(IbcReceiveResponse::new().add_message(msg).set_ack(ack_data)) +} + +#[cfg(test)] +mod tests { + use astroport_governance::interchain::Hub; + use cosmwasm_std::{ + from_binary, + testing::{mock_info, MOCK_CONTRACT_ADDR}, + IbcPacketReceiveMsg, Reply, ReplyOn, SubMsg, SubMsgResponse, SubMsgResult, + }; + use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; + + use super::*; + use crate::{ + contract::instantiate, + execute::execute, + ibc::ibc_packet_receive, + mock::{ + mock_all, mock_ibc_packet, setup_channel, ASSEMBLY, ASTRO_TOKEN, CW20ICS20, + GENERATOR_CONTROLLER, OWNER, STAKING, + }, + reply::{reply, STAKE_ID}, + }; + + // Test Cases: + // + // Expect Success + // - Submitting the vote results in an Assembly message + // + // Expect Error + // - An error is returned instead + #[test] + fn ibc_assembly_vote() { + let (mut deps, env, info) = mock_all(OWNER); + + let voter = "voter1234"; + let voting_power = Uint128::from(100u128); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Stake tokens to ensure the channel has a non-zero balance + let user1 = "user1"; + let user1_funds = Uint128::from(100u128); + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_owned(), + memo: "{\"stake\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap(); + + // Verify that the stake message matches the expected message + let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: STAKING.to_string(), + amount: user1_funds, + msg: stake_msg, + }) + .unwrap(); + + // Verify that we see a stake message reply + assert_eq!( + res.messages[0], + SubMsg { + id: 9000, + gas_limit: None, + reply_on: ReplyOn::Success, + msg: WasmMsg::Execute { + contract_addr: ASTRO_TOKEN.to_string(), + msg: send_msg, + funds: vec![], + } + .into(), + } + ); + + // Construct the reply from the staking contract that will be returned + // to the contract + let stake_reply = Reply { + id: STAKE_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }; + + let res = reply(deps.as_mut(), env.clone(), stake_reply).unwrap(); + + // We must have one IBC message + assert_eq!(res.messages.len(), 1); + + // At this point we now have 100 staked tokens + // We can test that voting power may not exceed this + let proposal_id = 1u64; + let vote_option = ProposalVoteOption::For; + + // Attempt a vote with double the voting power + let ibc_vote = to_binary(&Hub::CastAssemblyVote { + proposal_id, + voter: Addr::unchecked(voter), + vote_option: vote_option.clone(), + voting_power: voting_power.checked_add(Uint128::from(100u128)).unwrap(), + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_vote); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); + + let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + match hub_respone { + Response::Result { error, .. } => { + assert_eq!( + error, + Some("Voting power exceeds channel balance".to_string()) + ); + } + _ => panic!("Wrong response type"), + } + + // Attempt a vote with the correct voting power + let ibc_vote = to_binary(&Hub::CastAssemblyVote { + proposal_id, + voter: Addr::unchecked(voter), + vote_option, + voting_power, + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_vote); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + + let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + match hub_respone { + Response::Result { error, .. } => { + assert!(error.is_none()); + } + _ => panic!("Wrong response type"), + } + + assert_eq!(res.messages.len(), 1); + + let assembly_msg = to_binary( + &astroport_governance::assembly::ExecuteMsg::CastOutpostVote { + proposal_id, + vote: ProposalVoteOption::For, + voter: voter.to_string(), + voting_power, + }, + ) + .unwrap(); + + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: ASSEMBLY.to_string(), + msg: assembly_msg, + funds: vec![], + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - Submitting the vote results in a Generator controller message + // + // Expect Error + // - An error is returned instead + #[test] + fn ibc_emissions_vote() { + let (mut deps, env, info) = mock_all(OWNER); + + let voter = "voter1234"; + let voting_power = Uint128::from(100u128); + let votes = vec![("pooladdress".to_string(), 10000)]; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Voting must fail if the channel balance in insufficient + let ibc_unstake = to_binary(&Hub::CastEmissionsVote { + voter: Addr::unchecked(voter), + voting_power, + votes: votes.clone(), + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_unstake); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); + + let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + match hub_respone { + Response::Result { error, .. } => { + assert_eq!( + error.unwrap(), + "Voting power exceeds channel balance".to_string() + ); + } + _ => panic!("Wrong response type"), + } + + // Stake some ASTRO remotely + // Stake tokens to ensure the channel has a non-zero balance + let user1 = "user1"; + let user1_funds = Uint128::from(100u128); + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: user1_funds, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: user1.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_owned(), + memo: "{\"stake\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap(); + + // Verify that the stake message matches the expected message + let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: STAKING.to_string(), + amount: user1_funds, + msg: stake_msg, + }) + .unwrap(); + + // Verify that we see a stake message reply + assert_eq!( + res.messages[0], + SubMsg { + id: 9000, + gas_limit: None, + reply_on: ReplyOn::Success, + msg: WasmMsg::Execute { + contract_addr: ASTRO_TOKEN.to_string(), + msg: send_msg, + funds: vec![], + } + .into(), + } + ); + + // Construct the reply from the staking contract that will be returned + // to the contract + let stake_reply = Reply { + id: STAKE_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }; + + let res = reply(deps.as_mut(), env.clone(), stake_reply).unwrap(); + + // We must have one IBC message + assert_eq!(res.messages.len(), 1); + + let ibc_vote = to_binary(&Hub::CastEmissionsVote { + voter: Addr::unchecked(voter), + voting_power, + votes: votes.clone(), + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_vote); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + + let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + match hub_respone { + Response::Result { error, .. } => { + assert!(error.is_none(),); + } + _ => panic!("Wrong response type"), + } + + assert_eq!(res.messages.len(), 1); + + let generator_controller_msg = to_binary( + &astroport_governance::generator_controller_lite::ExecuteMsg::OutpostVote { + voter: voter.to_string(), + voting_power, + votes, + }, + ) + .unwrap(); + + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: GENERATOR_CONTROLLER.to_string(), + msg: generator_controller_msg, + funds: vec![], + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - Kicking the user results in a Generator controller message + // + // Expect Error + // - An error is returned instead + #[test] + fn ibc_kick_unlocked() { + let (mut deps, env, info) = mock_all(OWNER); + + let voter = "voter1234"; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add an allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Kick the voter + let ibc_kick_unlocked = to_binary(&Hub::KickUnlockedVoter { + voter: Addr::unchecked(voter), + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_kick_unlocked); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + + let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + match hub_respone { + Response::Result { error, .. } => { + assert!(error.is_none()); + } + _ => panic!("Wrong response type"), + } + + // We must have one message + assert_eq!(res.messages.len(), 1); + + // Verify that the message matches the expected message + let controller_msg = to_binary( + &astroport_governance::generator_controller_lite::ExecuteMsg::KickUnlockedOutpostVoter { + unlocked_voter:voter.to_string(), + }, + ) + .unwrap(); + + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: GENERATOR_CONTROLLER.to_string(), + msg: controller_msg, + funds: vec![], + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - Kicking the user results in a Generator controller message + // + // Expect Error + // - An error is returned instead + #[test] + fn ibc_kick_blacklisted() { + let (mut deps, env, info) = mock_all(OWNER); + + let voter = "voter1234"; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Kick the voter + let ibc_kick_blacklisted = to_binary(&Hub::KickBlacklistedVoter { + voter: Addr::unchecked(voter), + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_kick_blacklisted); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + + let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + match hub_respone { + Response::Result { error, .. } => { + assert!(error.is_none()); + } + _ => panic!("Wrong response type"), + } + + // We must have one message + assert_eq!(res.messages.len(), 1); + + // Verify that the message matches the expected message + let controller_msg = to_binary( + &astroport_governance::generator_controller_lite::ExecuteMsg::KickBlacklistedVoters { + blacklisted_voters: vec![voter.to_string()], + }, + ) + .unwrap(); + + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: GENERATOR_CONTROLLER.to_string(), + msg: controller_msg, + funds: vec![], + } + .into(), + } + ); + } +} diff --git a/contracts/hub/src/ibc_misc.rs b/contracts/hub/src/ibc_misc.rs new file mode 100644 index 00000000..1f933e27 --- /dev/null +++ b/contracts/hub/src/ibc_misc.rs @@ -0,0 +1,230 @@ +use astroport::cw20_ics20::TransferMsg; +use cosmwasm_std::{to_binary, Addr, DepsMut, IbcReceiveResponse, WasmMsg}; +use cw20::Cw20ExecuteMsg; + +use astroport_governance::interchain::Response; + +use crate::{ + error::ContractError, + state::{get_transfer_channel_from_outpost_channel, CONFIG, USER_FUNDS}, +}; + +/// Handle an IBC message to withdraw funds stuck on the Hub +/// +/// In some cases where the CW20-ICS20 IBC transfer to the Outpost user fails +/// (due to timeout or otherwise), the funds will be stuck on the Hub chain. In +/// such a case the CW20-ICS20 contract will send the funds back here and this +/// function will attempt to send them back to the user. +pub fn handle_ibc_withdraw_stuck_funds( + deps: DepsMut, + receive_channel: String, + user: Addr, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Check if this user has any funds stuck on the Hub chain + let balance = USER_FUNDS.load(deps.storage, &user)?; + if balance.is_zero() { + return Err(ContractError::NoFunds {}); + } + + // Map the channel the request was received on to the channel used in the + // CW20-ICS20 transfer + // We can use the request channel safely as the Outpost contract enforces the + // address, we can't receive a request for funds for a different address from an + // incorrect Outpost + // Example, an Injective address can't request funds from a Neutron channel + let outpost_channels = + get_transfer_channel_from_outpost_channel(deps.as_ref(), &receive_channel)?; + + // User has funds, try to send it back to them + let transfer_msg = TransferMsg { + channel: outpost_channels.cw20_ics20, + remote_address: user.to_string(), + timeout: Some(config.ibc_timeout_seconds), + memo: None, + }; + + let send_msg = Cw20ExecuteMsg::Send { + contract: config.cw20_ics20_addr.to_string(), + amount: balance, + msg: to_binary(&transfer_msg)?, + }; + + let msg = WasmMsg::Execute { + contract_addr: config.token_addr.to_string(), + msg: to_binary(&send_msg)?, + funds: vec![], + }; + + // This acknowledgement only indicates that the withdraw was processed without + // error, not that the funds were successfully transferred over IBC to the user + let ack_data = to_binary(&Response::new_success( + "withdraw_funds".to_owned(), + user.to_string(), + ))?; + + // We're sending everything back to the user, so we can delete their balance + // If this fails again, the balance will be re-added from the CW20-ICS20 contract + USER_FUNDS.remove(deps.storage, &user); + + Ok(IbcReceiveResponse::new().set_ack(ack_data).add_message(msg)) +} + +#[cfg(test)] +mod tests { + use super::*; + use astroport_governance::interchain::{self, Hub}; + use cosmwasm_std::{ + from_binary, testing::mock_info, IbcPacketReceiveMsg, ReplyOn, SubMsg, Uint128, + }; + use cw20::Cw20ReceiveMsg; + + use crate::{ + contract::instantiate, + execute::execute, + ibc::ibc_packet_receive, + mock::{ + mock_all, mock_ibc_packet, setup_channel, ASSEMBLY, ASTRO_TOKEN, CW20ICS20, + GENERATOR_CONTROLLER, OWNER, STAKING, + }, + }; + + // Test Cases: + // + // Expect Success + // - Withdrawing stuck funds results in IBC message + // + // Expect Error + // - When address has no funds stuck + // + // This tests that balances are correctly tracked by the contract in case of + // IBC failures that result in funds getting stuck on the Hub + #[test] + fn ibc_withdraw_stuck_funds() { + let (mut deps, env, info) = mock_all(OWNER); + + let stuck_amount = Uint128::from(100u128); + let user = "user1"; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Add a valid failure + execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: stuck_amount, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + receiver: user.to_owned(), + }) + .unwrap(), + }), + ) + .unwrap(); + + // Withdraw must fail if the user has no funds stuck + let ibc_withdraw = to_binary(&Hub::WithdrawFunds { + user: Addr::unchecked("not_user"), + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_withdraw); + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); + + let hub_respone: interchain::Response = from_binary(&res.acknowledgement).unwrap(); + match hub_respone { + interchain::Response::Result { error, .. } => { + assert!(error.is_some()); + assert_eq!( + error.unwrap(), + "cosmwasm_std::math::uint128::Uint128 not found" + ); + } + _ => panic!("Wrong response type"), + } + + // Our user has funds stuck, so withdrawal must succeed + let ibc_withdraw = to_binary(&Hub::WithdrawFunds { + user: Addr::unchecked(user), + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_withdraw); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + + let hub_respone: interchain::Response = from_binary(&res.acknowledgement).unwrap(); + match hub_respone { + interchain::Response::Result { address, error, .. } => { + assert!(error.is_none()); + assert_eq!(address.unwrap(), user); + } + _ => panic!("Wrong response type"), + } + + // We must see one message being emitted from the withdraw + assert_eq!(res.messages.len(), 1); + + // It must be a CW20-ICS20 transfer message + let ibc_transfer_msg = to_binary(&TransferMsg { + remote_address: user.to_string(), + channel: "channel-1".to_string(), + timeout: Some(10), + memo: None, + }) + .unwrap(); + let cw_send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: CW20ICS20.to_string(), + amount: stuck_amount, + msg: ibc_transfer_msg, + }) + .unwrap(); + + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: "astro".to_string(), + msg: cw_send_msg, + funds: vec![], + } + .into(), + } + ); + } +} diff --git a/contracts/hub/src/ibc_query.rs b/contracts/hub/src/ibc_query.rs new file mode 100644 index 00000000..1010bc3b --- /dev/null +++ b/contracts/hub/src/ibc_query.rs @@ -0,0 +1,170 @@ +use cosmwasm_std::{to_binary, DepsMut, IbcReceiveResponse}; + +use astroport_governance::{ + assembly::Proposal, + assembly::QueryMsg, + interchain::{ProposalSnapshot, Response}, +}; + +use crate::{error::ContractError, state::CONFIG}; + +/// Query the Assembly for a proposal and return the result in an +/// IBC acknowledgement +/// +/// If the proposal doesn't exist, the Outpost will see a generic ABCI error +/// and not "proposal not found" due to limitations in wasmd +pub fn handle_ibc_query_proposal( + deps: DepsMut, + id: u64, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + let proposal: Proposal = deps.querier.query_wasm_smart( + config.assembly_addr, + &QueryMsg::Proposal { proposal_id: id }, + )?; + + let proposal_snapshot = ProposalSnapshot { + id: proposal.proposal_id, + start_time: proposal.start_time, + }; + + let ack_data = to_binary(&Response::QueryProposal(proposal_snapshot))?; + Ok(IbcReceiveResponse::new() + .set_ack(ack_data) + .add_attribute("query", "proposal") + .add_attribute("proposal_id", id.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use astroport_governance::interchain::Hub; + use cosmwasm_std::{from_binary, testing::mock_info, Addr, IbcPacketReceiveMsg, Uint64}; + + use crate::{ + contract::instantiate, + execute::execute, + ibc::ibc_packet_receive, + mock::{ + mock_all, mock_ibc_packet, setup_channel, ASSEMBLY, CW20ICS20, GENERATOR_CONTROLLER, + OWNER, STAKING, + }, + }; + + // Test Cases: + // + // Expect Success + // - Proposal should not be queried without Assembly + + #[test] + fn query_proposal_fails_invalid_assembly() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: "invalid".to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + let ibc_query_proposal = to_binary(&Hub::QueryProposal { id: 1 }).unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_query_proposal); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + + let ack: Response = from_binary(&res.acknowledgement).unwrap(); + match ack { + Response::Result { error, .. } => { + assert!(error.is_some()); + } + _ => panic!("Wrong response type"), + } + + // No messages should be emitted + assert_eq!(res.messages.len(), 0); + } + + // Test Cases: + // + // Expect Success + // - An IBC ack contains the correct information + + #[test] + fn query_proposal() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + let ibc_query_proposal = to_binary(&Hub::QueryProposal { id: 1 }).unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_query_proposal); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + + let ack: Response = from_binary(&res.acknowledgement).unwrap(); + match ack { + Response::QueryProposal(proposal) => { + assert_eq!(proposal.id, Uint64::from(1u64)); + } + _ => panic!("Wrong response type"), + } + + // No message must be emitted, the ack contains the data + assert_eq!(res.messages.len(), 0); + } +} diff --git a/contracts/hub/src/ibc_staking.rs b/contracts/hub/src/ibc_staking.rs new file mode 100644 index 00000000..d74b01f4 --- /dev/null +++ b/contracts/hub/src/ibc_staking.rs @@ -0,0 +1,329 @@ +use astroport::querier::query_token_balance; +use cosmwasm_std::{ + to_binary, DepsMut, Env, IbcReceiveResponse, QuerierWrapper, Storage, SubMsg, Uint128, WasmMsg, +}; +use cw20::Cw20ExecuteMsg; + +use astroport_governance::interchain::Response; + +use crate::{ + error::ContractError, + reply::UNSTAKE_ID, + state::{ReplyData, CONFIG, REPLY_DATA}, +}; + +/// Handle an unstake command from an Outpost +/// +/// Once the xASTRO has been unstaked, the resulting ASTRO will be sent back +/// to the user on the Outpost +pub fn handle_ibc_unstake( + deps: DepsMut, + env: Env, + receive_channel: String, + receiver: String, + amount: Uint128, +) -> Result { + let msg = construct_unstake_msg( + deps.storage, + deps.querier, + env, + receive_channel, + receiver.clone(), + amount, + )?; + // Add to SubMessage to handle the reply + let sub_msg = SubMsg::reply_on_success(msg, UNSTAKE_ID); + + // Set the acknowledgement. This is only to indicate that the unstake + // was processed without error, not that the funds were successfully + let ack_data = to_binary(&Response::new_success("unstake".to_owned(), receiver))?; + + Ok(IbcReceiveResponse::new() + .set_ack(ack_data) + .add_submessage(sub_msg)) +} + +/// Create the messages and state to correctly handle the unstaking of xASTRO +pub fn construct_unstake_msg( + storage: &mut dyn Storage, + querier: QuerierWrapper, + env: Env, + receiving_channel: String, + receiver: String, + amount: Uint128, +) -> Result { + let config = CONFIG.load(storage)?; + + // Unstake the received xASTRO amount + // We need a SubMessage here to ensure that we send the correct amount + // of ASTRO to the receiver as the ratio isn't 1:1 + let leave_msg = astroport::staking::Cw20HookMsg::Leave {}; + let send_msg = Cw20ExecuteMsg::Send { + contract: config.staking_addr.to_string(), + amount, + msg: to_binary(&leave_msg)?, + }; + + // Send the xASTRO held in the contract to the Staking contract + let msg = WasmMsg::Execute { + contract_addr: config.xtoken_addr.to_string(), + msg: to_binary(&send_msg)?, + funds: vec![], + }; + + // Log the amount of ASTRO we currently hold + let current_astro_balance = query_token_balance( + &querier, + config.token_addr.to_string(), + env.contract.address, + )?; + + // Temporarily save the data needed for the SubMessage reply + let reply_data = ReplyData { + receiver, + receiving_channel, + value: current_astro_balance, + original_value: amount, + }; + REPLY_DATA.save(storage, &reply_data)?; + + Ok(msg) +} + +#[cfg(test)] +mod tests { + use super::*; + use astroport::cw20_ics20::TransferMsg; + use astroport_governance::{hub::HubBalance, interchain::Hub}; + use cosmwasm_std::{ + from_binary, + testing::{mock_info, MOCK_CONTRACT_ADDR}, + Addr, IbcPacketReceiveMsg, Reply, ReplyOn, SubMsgResponse, SubMsgResult, Uint64, + }; + use cw20::Cw20ReceiveMsg; + + use crate::{ + contract::instantiate, + execute::execute, + ibc::ibc_packet_receive, + mock::{ + mock_all, mock_ibc_packet, setup_channel, ASSEMBLY, ASTRO_TOKEN, CW20ICS20, + GENERATOR_CONTROLLER, OWNER, STAKING, XASTRO_TOKEN, + }, + query::query, + reply::{reply, STAKE_ID}, + }; + + // Test Cases: + // + // Expect Success + // - Unstaked tokens must be returned to the user + + #[test] + fn ibc_unstake() { + let (mut deps, env, info) = mock_all(OWNER); + + let unstaker = "unstaker"; + let unstake_amount = Uint128::from(100u128); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::hub::InstantiateMsg { + owner: OWNER.to_string(), + assembly_addr: ASSEMBLY.to_string(), + cw20_ics20_addr: CW20ICS20.to_string(), + staking_addr: STAKING.to_string(), + generator_controller_addr: GENERATOR_CONTROLLER.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid IBC channel + setup_channel(deps.as_mut(), env.clone()); + + // Add allowed Outpost + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::hub::ExecuteMsg::AddOutpost { + outpost_addr: "outpost".to_string(), + outpost_channel: "channel-3".to_string(), + cw20_ics20_channel: "channel-1".to_string(), + }, + ) + .unwrap(); + + // Send a valid stake memo so we have something to unstake + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(ASTRO_TOKEN, &[]), + astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: CW20ICS20.to_string(), + amount: unstake_amount, + msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + channel: "channel-1".to_string(), + sender: unstaker.to_string(), + receiver: MOCK_CONTRACT_ADDR.to_owned(), + memo: "{\"stake\":{}}".to_string(), + }) + .unwrap(), + }), + ) + .unwrap(); + + // Verify that the stake message matches the expected message + let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: STAKING.to_string(), + amount: unstake_amount, + msg: stake_msg, + }) + .unwrap(); + + // Verify that we see a stake message reply + assert_eq!( + res.messages[0], + SubMsg { + id: 9000, + gas_limit: None, + reply_on: ReplyOn::Success, + msg: WasmMsg::Execute { + contract_addr: ASTRO_TOKEN.to_string(), + msg: send_msg, + funds: vec![], + } + .into(), + } + ); + + // Construct the reply from the staking contract that will be returned + // to the contract + let stake_reply = Reply { + id: STAKE_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }; + + let res = reply(deps.as_mut(), env.clone(), stake_reply).unwrap(); + + // We must have one IBC message + assert_eq!(res.messages.len(), 1); + + let ibc_unstake = to_binary(&Hub::Unstake { + receiver: unstaker.to_owned(), + amount: unstake_amount, + }) + .unwrap(); + let recv_packet = mock_ibc_packet("channel-3", ibc_unstake); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); + + let ack: Response = from_binary(&res.acknowledgement).unwrap(); + match ack { + Response::Result { error, .. } => { + assert!(error.is_none()); + } + _ => panic!("Wrong response type"), + } + + // Should have exactly one message + assert_eq!(res.messages.len(), 1); + + // Verify that the unstake message matches the expected message + let unstake_msg = to_binary(&astroport::staking::Cw20HookMsg::Leave {}).unwrap(); + let send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: STAKING.to_string(), + amount: unstake_amount, + msg: unstake_msg, + }) + .unwrap(); + + // We should see the unstake SubMessage + assert_eq!( + res.messages[0], + SubMsg { + id: 9001, + gas_limit: None, + reply_on: ReplyOn::Success, + msg: WasmMsg::Execute { + contract_addr: XASTRO_TOKEN.to_string(), + msg: send_msg, + funds: vec![], + } + .into(), + } + ); + + // Construct the reply from the staking contract that will be returned + // to the contract + let unstake_reply = Reply { + id: UNSTAKE_ID, + result: SubMsgResult::Ok(SubMsgResponse { + events: vec![], + data: None, + }), + }; + + let res = reply(deps.as_mut(), env.clone(), unstake_reply).unwrap(); + + // We must have one CW20-ICS20 transfer message + assert_eq!(res.messages.len(), 1); + + // Contruct the CW20-ICS20 ASTRO token transfer we expect to see + let transfer_msg = to_binary(&TransferMsg { + channel: "channel-1".to_string(), + remote_address: unstaker.to_string(), + timeout: Some(10), + memo: None, + }) + .unwrap(); + let send_msg = to_binary(&Cw20ExecuteMsg::Send { + contract: CW20ICS20.to_string(), + amount: unstake_amount, + msg: transfer_msg, + }) + .unwrap(); + + // We should see the ASTRO token transfer + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: ASTRO_TOKEN.to_string(), + msg: send_msg, + funds: vec![], + } + .into(), + } + ); + + // At this point the channel must have a zero balance as everything + // has been unstaked + let balances = query( + deps.as_ref(), + env.clone(), + astroport_governance::hub::QueryMsg::ChannelBalanceAt { + channel: "channel-3".to_string(), + timestamp: Uint64::from(env.block.time.seconds()), + }, + ) + .unwrap(); + + let expected = HubBalance { + balance: Uint128::zero(), + }; + + assert_eq!(balances, to_binary(&expected).unwrap()); + } +} diff --git a/contracts/hub/src/lib.rs b/contracts/hub/src/lib.rs new file mode 100644 index 00000000..692993b9 --- /dev/null +++ b/contracts/hub/src/lib.rs @@ -0,0 +1,14 @@ +pub mod contract; +pub mod error; +pub mod execute; +pub mod ibc; +pub mod ibc_governance; +pub mod ibc_misc; +pub mod ibc_query; +pub mod ibc_staking; +pub mod query; +pub mod reply; +pub mod state; + +#[cfg(test)] +mod mock; diff --git a/contracts/hub/src/mock.rs b/contracts/hub/src/mock.rs new file mode 100644 index 00000000..b5ae25a9 --- /dev/null +++ b/contracts/hub/src/mock.rs @@ -0,0 +1,298 @@ +use std::cell::Cell; + +#[cfg(test)] +use cosmwasm_std::{from_binary, Uint64}; +use cosmwasm_std::{ + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + to_binary, Addr, Binary, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, ListChannelsResponse, MessageInfo, OwnedDeps, + Timestamp, Uint128, +}; + +use cosmwasm_std::testing::MOCK_CONTRACT_ADDR; +use cosmwasm_std::{ + from_slice, Empty, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, +}; +use cw20::BalanceResponse as Cw20BalanceResponse; + +use crate::ibc::{ibc_channel_connect, ibc_channel_open, IBC_APP_VERSION}; + +pub const CONTRACT_PORT: &str = "ibc:wasm1234567890abcdef"; +pub const REMOTE_PORT: &str = "wasm.outpost"; +pub const CONNECTION_ID: &str = "connection-2"; +pub const OWNER: &str = "owner"; +pub const ASSEMBLY: &str = "assembly"; +pub const CW20ICS20: &str = "cw20_ics20"; +pub const GENERATOR_CONTROLLER: &str = "generator_controller"; +pub const STAKING: &str = "staking"; +pub const ASTRO_TOKEN: &str = "astro"; +pub const XASTRO_TOKEN: &str = "xastro"; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies. +/// This uses the Astroport CustomQuerier. +#[cfg(test)] +pub fn mock_dependencies() -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, &[])])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +/// WasmMockQuerier will respond to requests from the custom querier, +/// providing responses to the contracts +pub struct WasmMockQuerier { + base: MockQuerier, + xastro_balance: Cell, + astro_balance: Cell, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + if contract_addr == STAKING { + match from_binary(msg).unwrap() { + astroport::staking::QueryMsg::Config {} => { + let config = astroport::staking::ConfigResponse { + deposit_token_addr: Addr::unchecked("astro"), + share_token_addr: Addr::unchecked("xastro"), + }; + + SystemResult::Ok(to_binary(&config).into()) + } + _ => { + panic!("DO NOT ENTER HERE") + } + } + } else { + if contract_addr == ASTRO_TOKEN { + // Manually increase the ASTRO balance every query + // to help tests + let response = Cw20BalanceResponse { + balance: self.astro_balance.get(), + }; + self.astro_balance.set( + self.astro_balance + .get() + .checked_add(Uint128::from(100u128)) + .unwrap(), + ); + return SystemResult::Ok(to_binary(&response).into()); + } + if contract_addr == XASTRO_TOKEN { + // Manually increase the ASTRO balance every query + // to help tests + let response = Cw20BalanceResponse { + balance: self.xastro_balance.get(), + }; + self.xastro_balance.set( + self.xastro_balance + .get() + .checked_add(Uint128::from(100u128)) + .unwrap(), + ); + return SystemResult::Ok(to_binary(&response).into()); + } + if contract_addr != ASSEMBLY { + return SystemResult::Err(SystemError::Unknown {}); + } + match from_binary(msg).unwrap() { + astroport_governance::assembly::QueryMsg::Proposal { proposal_id } => { + let proposal = astroport_governance::assembly::Proposal { + proposal_id: Uint64::from(proposal_id), + submitter: Addr::unchecked("submitter"), + status: astroport_governance::assembly::ProposalStatus::Active, + for_power: Uint128::zero(), + outpost_against_power: Uint128::zero(), + against_power: Uint128::zero(), + outpost_for_power: Uint128::zero(), + for_voters: vec![], + against_voters: vec![], + start_block: 1, + start_time: 1571797419, + end_block: 5, + delayed_end_block: 10, + expiration_block: 15, + title: "Test title".to_string(), + description: "Test description".to_string(), + link: None, + messages: None, + deposit_amount: Uint128::one(), + ibc_channel: None, + }; + SystemResult::Ok(to_binary(&proposal).into()) + } + _ => { + panic!("DO NOT ENTER HERE") + } + } + } + } + QueryRequest::Ibc(IbcQuery::ListChannels { .. }) => { + let response = ListChannelsResponse { + channels: vec![ + IbcChannel::new( + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcOrder::Unordered, + "version", + "connection-1", + ), + IbcChannel::new( + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-2".to_string(), + }, + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-2".to_string(), + }, + IbcOrder::Unordered, + "version", + "connection-1", + ), + IbcChannel::new( + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcOrder::Unordered, + "version", + "connection-1", + ), + IbcChannel::new( + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-100".to_string(), + }, + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcOrder::Unordered, + "version", + "connection-1", + ), + ], + }; + SystemResult::Ok(to_binary(&response).into()) + // if contract_addr != "cw20_ics20" { + // return SystemResult::Err(SystemError::Unknown {}); + // } + } + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { + base, + xastro_balance: Cell::new(Uint128::zero()), + astro_balance: Cell::new(Uint128::zero()), + } + } +} + +/// Mock the dependencies for unit tests +pub fn mock_all( + sender: &str, +) -> ( + OwnedDeps, + Env, + MessageInfo, +) { + let deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info(sender, &[]); + (deps, env, info) +} + +/// Mock an IBC channel +pub fn mock_channel( + our_port: &str, + our_channel_id: &str, + counter_port: &str, + counter_channel: &str, + ibc_order: IbcOrder, + ibc_version: &str, +) -> IbcChannel { + IbcChannel::new( + IbcEndpoint { + port_id: our_port.into(), + channel_id: our_channel_id.into(), + }, + IbcEndpoint { + port_id: counter_port.into(), + channel_id: counter_channel.into(), + }, + ibc_order, + ibc_version.to_string(), + CONNECTION_ID, + ) +} + +/// Set up a valid channel for use in tests +pub fn setup_channel(mut deps: DepsMut, env: Env) { + let channel = mock_channel( + "wasm.hub", + "channel-3", + "wasm.outpost", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.branch(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + ibc_channel_connect(deps, env, connect_msg).unwrap(); +} + +/// Construct a mock IBC packet +pub fn mock_ibc_packet(my_channel: &str, data: Binary) -> IbcPacket { + IbcPacket::new( + data, + IbcEndpoint { + port_id: REMOTE_PORT.to_string(), + channel_id: "channel-7".to_string(), + }, + IbcEndpoint { + port_id: CONTRACT_PORT.to_string(), + channel_id: my_channel.to_string(), + }, + 3, + Timestamp::from_seconds(1665321069).into(), + ) +} diff --git a/contracts/hub/src/query.rs b/contracts/hub/src/query.rs new file mode 100644 index 00000000..3474f837 --- /dev/null +++ b/contracts/hub/src/query.rs @@ -0,0 +1,72 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_binary, Addr, Binary, Deps, Env, Order, StdResult, Uint128}; +use cw_storage_plus::Bound; + +use crate::state::{channel_balance_at, total_balance_at, CONFIG, OUTPOSTS, USER_FUNDS}; +use astroport_governance::{ + hub::{HubBalance, OutpostConfig, QueryMsg}, + DEFAULT_LIMIT, MAX_LIMIT, +}; + +/// Expose available contract queries. +/// +/// ## Queries +/// * **QueryMsg::Config {}** Returns core contract settings stored in the [`Config`] structure. +/// +/// * **QueryMsg::UserFunds { }** Returns a [`HubBalance`] containing the amount of ASTRO this address has held on the Hub due to IBC failures +/// +/// * **QueryMsg::Outposts { }** Returns a [`Vec`] containing the active Outposts +/// +/// * **QueryMsg::ChannelBalanceAt { channel, timestamp }** Returns a [`HubBalance`] containing the amount of xASTRO minted on the specified channel at the specified timestamp +/// +/// * **QueryMsg::TotalChannelBalancesAt { }** Returns a [`HubBalance`] containing the total amount of xASTRO minted across all channels at a specified time +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), + QueryMsg::UserFunds { user } => query_user_funds(deps, user), + QueryMsg::Outposts { start_after, limit } => query_outposts(deps, start_after, limit), + QueryMsg::ChannelBalanceAt { channel, timestamp } => to_binary(&HubBalance { + balance: channel_balance_at(deps.storage, &channel, timestamp.u64())?, + }), + QueryMsg::TotalChannelBalancesAt { timestamp } => to_binary(&HubBalance { + balance: total_balance_at(deps.storage, timestamp.u64())?, + }), + } +} + +/// Return a list of Outpost in the format of `OutpostConfig` +/// Paged by address and will only return limit at a time +fn query_outposts( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.as_deref().map(Bound::exclusive); + + let outposts: Vec = OUTPOSTS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (key, value) = item.unwrap(); + OutpostConfig { + address: key, + channel: value.outpost, + cw20_ics20_channel: value.cw20_ics20, + } + }) + .collect(); + to_binary(&outposts) +} + +/// Return the amount of ASTRO this address has held on the Hub due to IBC +/// failures +fn query_user_funds(deps: Deps, user: Addr) -> StdResult { + let funds = USER_FUNDS + .load(deps.storage, &user) + .unwrap_or(Uint128::zero()); + + to_binary(&HubBalance { balance: funds }) +} diff --git a/contracts/hub/src/reply.rs b/contracts/hub/src/reply.rs new file mode 100644 index 00000000..e03307f8 --- /dev/null +++ b/contracts/hub/src/reply.rs @@ -0,0 +1,161 @@ +use astroport::{cw20_ics20::TransferMsg, querier::query_token_balance}; +use cosmwasm_std::{ + entry_point, to_binary, CosmosMsg, DepsMut, Env, IbcMsg, Reply, Response, SubMsgResult, WasmMsg, +}; +use cw20::Cw20ExecuteMsg; + +use astroport_governance::interchain::Outpost; + +use crate::{ + error::ContractError, + state::{ + decrease_channel_balance, get_outpost_from_cw20ics20_channel, + get_transfer_channel_from_outpost_channel, increase_channel_balance, CONFIG, REPLY_DATA, + }, +}; + +/// Reply ID when staking tokens +pub const STAKE_ID: u64 = 9000; +/// Reply ID when unstaking tokens +pub const UNSTAKE_ID: u64 = 9001; + +/// Handle SubMessage replies +/// +/// To correctly handle staking and unstaking amount we execute the calls using +/// SubMessages and the replies are handled here +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, reply: Reply) -> Result { + match reply.id { + STAKE_ID => handle_stake_reply(deps, env, reply), + UNSTAKE_ID => handle_unstake_reply(deps, env, reply), + _ => Err(ContractError::UnknownReplyId { id: reply.id }), + } +} + +/// Handle the reply from a staking transaction +fn handle_stake_reply(deps: DepsMut, env: Env, reply: Reply) -> Result { + match reply.result { + SubMsgResult::Ok(..) => { + let config = CONFIG.load(deps.storage)?; + + // Load the temporary data stored before the SubMessage was executed + let reply_data = REPLY_DATA.load(deps.storage)?; + + // Determine the actual amount of xASTRO we received from staking + // and mint on the Outpost + let current_x_astro_balance = + query_token_balance(&deps.querier, config.xtoken_addr, env.contract.address)?; + let xastro_received = current_x_astro_balance.checked_sub(reply_data.value)?; + + // The channel we received the ASTRO to stake on was the CW20-ICS20 + // channel, we need to determine the channel to use for minting the + // xASTRO be checking the known Outposts + let outpost_channels = + get_outpost_from_cw20ics20_channel(deps.as_ref(), &reply_data.receiving_channel)?; + + // Submit an IBC transaction to mint the same amount of xASTRO + // we received from staking on the Outpost + let mint_remote = Outpost::MintXAstro { + amount: xastro_received, + receiver: reply_data.receiver.clone(), + }; + let msg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: outpost_channels.outpost.clone(), + data: to_binary(&mint_remote)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + + // Keep track of the amount of xASTRO minted on the related Outpost + increase_channel_balance( + deps.storage, + env.block.time.seconds(), + &outpost_channels.outpost, + xastro_received, + )?; + + Ok(Response::new() + .add_message(msg) + .add_attribute("action", "mint_remote_xastro") + .add_attribute("amount", xastro_received) + .add_attribute("channel", outpost_channels.outpost) + .add_attribute("receiver", reply_data.receiver)) + } + // In the case where staking fails, the funds will either automatically be returned + // through the CW20-ICS20 contract or the user will need to manually withdraw them + // from this contract. In either case, we don't need to do anything here as the + // original staking memo is already a SubMessage in the CW20-ICS20 contract + SubMsgResult::Err(err) => Err(ContractError::InvalidSubmessage { reason: err }), + } +} + +/// Handle the reply from an unstaking transaction +fn handle_unstake_reply(deps: DepsMut, env: Env, reply: Reply) -> Result { + match reply.result { + SubMsgResult::Ok(..) => { + let config = CONFIG.load(deps.storage)?; + + // Load the temporary data stored before the SubMessage was executed + let reply_data = REPLY_DATA.load(deps.storage)?; + + // Determine the actual amount of ASTRO we received from unstaking + // to determine how much to send back to the user + let current_astro_balance = query_token_balance( + &deps.querier, + config.token_addr.clone(), + env.contract.address, + )?; + let astro_received = current_astro_balance.checked_sub(reply_data.value)?; + + // The channel we received the unstaking from was the Outpost contract + // channel, we need to determine the channel to use for sending the + // ASTRO back using the CW20-ICS20 contract + let outpost_channels = get_transfer_channel_from_outpost_channel( + deps.as_ref(), + &reply_data.receiving_channel, + )?; + + // Send the ASTRO back to the unstaking user on the Outpost chain + // via the CW20-ICS20 contract + let transfer_msg = TransferMsg { + channel: outpost_channels.cw20_ics20.clone(), + remote_address: reply_data.receiver.clone(), + timeout: Some(config.ibc_timeout_seconds), + memo: None, + }; + + let transfer = Cw20ExecuteMsg::Send { + contract: config.cw20_ics20_addr.to_string(), + amount: astro_received, + msg: to_binary(&transfer_msg)?, + }; + + let wasm_msg = WasmMsg::Execute { + contract_addr: config.token_addr.to_string(), + msg: to_binary(&transfer)?, + funds: vec![], + }; + + // Decrease the amount of xASTRO minted via this Outpost + decrease_channel_balance( + deps.storage, + env.block.time.seconds(), + &outpost_channels.outpost, + reply_data.original_value, + )?; + + Ok(Response::new() + .add_message(wasm_msg) + .add_attribute("action", "return_unstaked_astro") + .add_attribute("amount", astro_received) + .add_attribute("channel", outpost_channels.cw20_ics20) + .add_attribute("receiver", reply_data.receiver)) + } + // If unstaking fails the error will be returned to the Outpost that would undo + // the burning of xASTRO and return the tokens to the user + SubMsgResult::Err(err) => Err(ContractError::InvalidSubmessage { reason: err }), + } +} diff --git a/contracts/hub/src/state.rs b/contracts/hub/src/state.rs new file mode 100644 index 00000000..60802008 --- /dev/null +++ b/contracts/hub/src/state.rs @@ -0,0 +1,169 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Deps, Order, StdError, StdResult, Storage, Uint128}; +use cw_storage_plus::{Bound, Item, Map}; + +use astroport::common::OwnershipProposal; +use astroport_governance::hub::Config; + +use crate::error::ContractError; + +/// Holds temporary data used in the staking/unstaking replies +#[cw_serde] +pub struct ReplyData { + /// The address that should receive the staked/unstaked tokens + pub receiver: String, + /// The IBC channel the original request was received on + pub receiving_channel: String, + /// A generic value to store balances + pub value: Uint128, + /// The original value of a request + pub original_value: Uint128, +} + +/// Holds the IBC channels that are allowed to communicate with the Hub +#[cw_serde] +pub struct OutpostChannels { + /// The channel of the Outpost contract on the remote chain + pub outpost: String, + /// The channel to send ASTRO CW20-ICS20 tokens through + pub cw20_ics20: String, +} + +/// Stores the contract config +pub const CONFIG: Item = Item::new("config"); + +/// Stores data for reply endpoint. +pub const REPLY_DATA: Item = Item::new("reply_data"); + +/// Stores funds that got stuck on the Hub chain due to IBC transfer failures +/// when using cross-chain actions +pub const USER_FUNDS: Map<&Addr, Uint128> = Map::new("user_funds"); + +/// Contains a proposal to change contract ownership +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); + +/// Contains a map of outpost addresses to their IBC channels that are allowed +/// to communicate with the Hub over IBC +pub const OUTPOSTS: Map<&str, OutpostChannels> = Map::new("channel_map"); + +/// Contains a map of Outpost channels to their balances at timestamps. That is, the amount +/// of xASTRO minted via an Outpost at a specific time +pub const OUTPOST_CHANNEL_BALANCES: Map<(&str, u64), Uint128> = + Map::new("outpost_channel_balances"); + +pub const TOTAL_OUTPOST_CHANNEL_BALANCE: Map = + Map::new("total_outpost_channel_balances"); + +/// Get the Outpost channels for a given CW20-ICS20 channel +/// +/// The Outposts must be configured and connected before this will return any values +pub fn get_outpost_from_cw20ics20_channel( + deps: Deps, + cw20ics20_channel: &str, +) -> Result { + OUTPOSTS + .range(deps.storage, None, None, Order::Ascending) + .find_map(|item| { + let (_, value) = item.ok()?; + if value.cw20_ics20 == cw20ics20_channel { + Some(value) + } else { + None + } + }) + .ok_or(ContractError::UnknownOutpost {}) +} + +/// Get the Outpost channels for a given contract channel +/// +/// The Outposts must be configured and connected before this will return any values +pub fn get_transfer_channel_from_outpost_channel( + deps: Deps, + outpost_channel: &str, +) -> Result { + OUTPOSTS + .range(deps.storage, None, None, Order::Ascending) + .find_map(|item| { + let (_, value) = item.ok()?; + if value.outpost == outpost_channel { + Some(value) + } else { + None + } + }) + .ok_or(ContractError::UnknownOutpost {}) +} + +/// Increase the balance of xASTRO minted via a specific Outpost +pub(crate) fn increase_channel_balance( + storage: &mut dyn Storage, + timestamp: u64, + outpost_channel: &str, + amount: Uint128, +) -> Result<(), StdError> { + let last_balance = channel_balance_at(storage, outpost_channel, timestamp)?; + OUTPOST_CHANNEL_BALANCES.save( + storage, + (outpost_channel, timestamp), + &last_balance.checked_add(amount)?, + )?; + + let last_total_balance = total_balance_at(storage, timestamp)?; + TOTAL_OUTPOST_CHANNEL_BALANCE.save(storage, timestamp, &last_total_balance.checked_add(amount)?) +} + +/// Decrease the balance of xASTRO minted via a specific Outpost +/// This will return an error if the balance is insufficient +pub(crate) fn decrease_channel_balance( + storage: &mut dyn Storage, + timestamp: u64, + outpost_channel: &str, + amount: Uint128, +) -> Result<(), StdError> { + let last_balance = channel_balance_at(storage, outpost_channel, timestamp)?; + OUTPOST_CHANNEL_BALANCES.save( + storage, + (outpost_channel, timestamp), + &last_balance.checked_sub(amount)?, + )?; + + let last_total_balance = total_balance_at(storage, timestamp)?; + TOTAL_OUTPOST_CHANNEL_BALANCE.save(storage, timestamp, &last_total_balance.checked_sub(amount)?) +} + +/// Fetches last known balance of a channel before or on timestamp +pub(crate) fn channel_balance_at( + storage: &dyn Storage, + outpost_channel: &str, + timestamp: u64, +) -> StdResult { + let balance_opt = OUTPOST_CHANNEL_BALANCES + .prefix(outpost_channel) + .range( + storage, + None, + Some(Bound::inclusive(timestamp)), + Order::Descending, + ) + .next() + .transpose()? + .map(|(_, value)| value); + + Ok(balance_opt.unwrap_or_else(Uint128::zero)) +} + +/// Returns the total channel balances at a specific time +pub fn total_balance_at(storage: &dyn Storage, timestamp: u64) -> StdResult { + // Look for the last value recorded before the current block (if none then value is zero) + let end = Bound::inclusive(timestamp); + let last_value = TOTAL_OUTPOST_CHANNEL_BALANCE + .range(storage, None, Some(end), Order::Descending) + .next(); + + if let Some(value) = last_value { + let (_, v) = value?; + return Ok(v); + } + + Ok(Uint128::zero()) +} diff --git a/contracts/outpost/.cargo/config b/contracts/outpost/.cargo/config new file mode 100644 index 00000000..f5174787 --- /dev/null +++ b/contracts/outpost/.cargo/config @@ -0,0 +1,6 @@ +[alias] +wasm = "build --release --lib --target wasm32-unknown-unknown" +wasm-debug = "build --lib --target wasm32-unknown-unknown" +unit-test = "test --lib" +integration-test = "test --test integration" +schema = "run --bin schema" diff --git a/contracts/outpost/Cargo.toml b/contracts/outpost/Cargo.toml new file mode 100644 index 00000000..6cba447a --- /dev/null +++ b/contracts/outpost/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "astroport-outpost" +version = "0.1.0" +authors = ["Astroport"] +edition = "2021" +description = "Forwards interchain actions to the Astroport Hub" +license = "GPL-3.0" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for quicker tests, cargo test --lib +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cw2 = "1.0.1" +cw20 = "0.15" +cosmwasm-schema = "1.1.0" +cw-utils = "1.0.1" +cosmwasm-std = { version = "1.1.0", features = ["iterator", "ibc3"] } +cw-storage-plus = "0.15" +schemars = "0.8.12" +semver = "1.0.17" +serde = { version = "1.0.164", default-features = false, features = ["derive"] } +thiserror = "1.0.40" +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-governance = { path = "../../packages/astroport-governance" } +serde-json-wasm = "0.5.1" +base64 = { version = "0.13.0", default-features = false } + +[dev-dependencies] +cw-multi-test = "0.16.5" +anyhow = "1.0" diff --git a/contracts/outpost/README.md b/contracts/outpost/README.md new file mode 100644 index 00000000..ac267a48 --- /dev/null +++ b/contracts/outpost/README.md @@ -0,0 +1,131 @@ +# Outpost + +The Outpost contract enables staking, unstaking, voting in governance as well as voting on vxASTRO emissions from any chain where the Outpost contract is deployed on. The Hub and Outpost contracts are designed to work together, connected over IBC channels. + +The Outpost defines the following messages that can be received over IBC: + +```rust +/// Defines the messages that can be sent from the Hub to an Outpost +#[cw_serde] +pub enum Outpost { + /// Mint xASTRO tokens for the user + MintXAstro { receiver: String, amount: Uint128 }, +} +``` + +The Outpost is responsible for validation before sending data to the Hub. In a case such as voting, it will query the xASTRO contract for the user's holding at the time a proposal was added and submit that as the voting power. + +The Outpost defines the following execute messages: + +```rust +#[cw_serde] +pub enum ExecuteMsg { + /// Receive a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// Update parameters in the Outpost contract. Only the owner is allowed to + /// update the config + UpdateConfig { + /// The new Hub address + hub_addr: Option, + }, + /// Cast a vote on an Assembly proposal from an Outpost + CastAssemblyVote { + /// The ID of the proposal to vote on + proposal_id: u64, + /// The vote choice + vote: ProposalVoteOption, + }, + /// Cast a vote during an emissions voting period + CastEmissionsVote { + /// The votes in the format (pool address, percent of voting power) + votes: Vec<(String, u16)>, + }, + /// Kick an unlocked voter's voting power from the Generator Controller lite + KickUnlocked { + /// The address of the user to kick + user: Addr, + }, + /// Withdraw stuck funds from the Hub in case of specific IBC failures + WithdrawHubFunds {}, +} +``` + +## Message details + +**Receive xASTRO via a Cw20HookMsg message for unstaking** + +To unstake xASTRO from an Outpost a user needs to send the xASTRO to the Outpost. Once received, the contract burns the xASTRO and informs the Hub to unstake the true xASTRO on the Hub and return the resulting ASTRO. Should the IBC transactions fail at any point, the funds are returned to the user. + +The following needs to be executed on the Outpost xASTRO contract. `msg` in this case is the base64 of `{"unstake":{}}` + +```json +{ + "send": { + "contract": "wasm123", + "amount": "1000000", + "msg":"eyJ1bnN0YWtlIjp7fX0=" + } +} +``` + + +**Update Config** + +Update config allows the owner to set a new address for the Hub. Updating the Hub address will remove the known Hub channel and a new one will need to be established. + +```json +{ + "update_config": { + "hub_addr": "wasm123..." + } +} +``` + +**Cast a governance vote in the Assembly** + +In order to cast a vote we need to know the voting power of a user at the time the proposal was created. The contract will retrieve the proposal information if it doesn't have it cached locally before validating the xASTRO holdings and submitting the vote. + +```json +{ + "cast_assembly_vote":{ + "proposal_id": 1, + "vote": "for" + } +} +``` + +**Cast a vote on vxASTRO emissions** + +During voting periods in vxASTRO a user can vote on where emissions should be directed. The contract will check the vxASTRO holdings of the user before submitting the vote. + +```json +{ + "cast_emissions_vote": { + "votes":[ + ["wasm123..pool...", 1000] + ] + } +} +``` + +**Kick an unlocked vxASTRO user** + +When a user unlocks in vxASTRO their voting power is removed immediately. This call may only be made by the vxASTRO contract. Once called the unlock is sent to the Hub to execute on the Generator Controller on the Hub. + +```json +{ + "kick_unlocked":{ + "user":"wasm123" + } +} +``` + +**Withdraw funds from the Hub** + +In cases where specific IBC messages failed (mostly due to timeouts) there could be a situation where the funds are "stuck" on the Hub chain. To allow users to withdraw these funds we hold it in the Hub contract. `WithdrawHubFunds` will submit a request for the funds from the Hub and the funds will be sent over the CW20-ICS20 bridge again, if the user had funds stuck. + +```json +{ + "withdraw_hub_funds":{} +} +``` \ No newline at end of file diff --git a/contracts/outpost/src/contract.rs b/contracts/outpost/src/contract.rs new file mode 100644 index 00000000..5a0fd50d --- /dev/null +++ b/contracts/outpost/src/contract.rs @@ -0,0 +1,123 @@ +use astroport_governance::interchain::{MAX_IBC_TIMEOUT_SECONDS, MIN_IBC_TIMEOUT_SECONDS}; +use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response}; +use cw2::set_contract_version; + +use astroport_governance::outpost::{Config, InstantiateMsg, MigrateMsg}; + +use crate::error::ContractError; +use crate::state::CONFIG; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "astroport-outpost"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Instantiates the contract, storing the config. +/// Returns a `Response` object on successful execution or a `ContractError` on failure. +#[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)?; + + if !(MIN_IBC_TIMEOUT_SECONDS..=MAX_IBC_TIMEOUT_SECONDS).contains(&msg.ibc_timeout_seconds) { + return Err(ContractError::InvalidIBCTimeout { + timeout: msg.ibc_timeout_seconds, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS, + }); + } + + let config = Config { + owner: deps.api.addr_validate(&msg.owner)?, + hub_addr: msg.hub_addr, + // The Hub channel will be set when the connection is established + hub_channel: None, + xastro_token_addr: deps.api.addr_validate(&msg.xastro_token_addr)?, + vxastro_token_addr: deps.api.addr_validate(&msg.vxastro_token_addr)?, + ibc_timeout_seconds: msg.ibc_timeout_seconds, + }; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default()) +} + +/// Migrates the contract to a new version. +#[entry_point] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Err(ContractError::MigrationError {}) +} + +#[cfg(test)] +mod tests { + + use super::*; + + use crate::{ + contract::instantiate, + mock::{mock_all, HUB, OWNER, VXASTRO_TOKEN, XASTRO_TOKEN}, + }; + + // Test Cases: + // + // Expect Success + // - Invalid IBC timeouts are rejected + // + #[test] + fn invalid_ibc_timeout() { + let (mut deps, env, info) = mock_all(OWNER); + + // Test MAX + 1 + let ibc_timeout_seconds = MAX_IBC_TIMEOUT_SECONDS + 1; + let err = instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::InvalidIBCTimeout { + timeout: ibc_timeout_seconds, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS + } + ); + + // Test MIN - 1 + let ibc_timeout_seconds = MIN_IBC_TIMEOUT_SECONDS - 1; + let err = instantiate( + deps.as_mut(), + env, + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap_err(); + + assert_eq!( + err, + ContractError::InvalidIBCTimeout { + timeout: ibc_timeout_seconds, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS + } + ); + } +} diff --git a/contracts/outpost/src/error.rs b/contracts/outpost/src/error.rs new file mode 100644 index 00000000..b10ea324 --- /dev/null +++ b/contracts/outpost/src/error.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +/// This enum describes bribes contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("You can not send 0 tokens")] + ZeroAmount {}, + + #[error( + "Proposal {0} is being queried from the Hub, please try again in a few minutes", + proposal_id + )] + PendingVoteExists { proposal_id: u64 }, + + #[error( + "The address has no voting power at the start of the proposal: {0}", + address + )] + NoVotingPower { address: String }, + + #[error("The IBC channel to the Hub has not been set")] + MissingHubChannel {}, + + #[error("The user has already voted on this proposal")] + AlreadyVoted {}, + + #[error("Channel already established: {channel_id}")] + ChannelAlreadyEstablished { channel_id: String }, + + #[error("Invalid source port {invalid}. Should be : {valid}")] + InvalidSourcePort { invalid: String, valid: String }, + + #[error("Invalid IBC timeout: {timeout}, must be between {min} and {max} seconds")] + InvalidIBCTimeout { timeout: u64, min: u64, max: u64 }, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} diff --git a/contracts/outpost/src/execute.rs b/contracts/outpost/src/execute.rs new file mode 100644 index 00000000..b82b56bb --- /dev/null +++ b/contracts/outpost/src/execute.rs @@ -0,0 +1,1322 @@ +use astroport_governance::interchain::{MAX_IBC_TIMEOUT_SECONDS, MIN_IBC_TIMEOUT_SECONDS}; +use astroport_governance::utils::check_contract_supports_channel; +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + from_binary, to_binary, Addr, CosmosMsg, DepsMut, Env, IbcMsg, MessageInfo, Response, StdError, + WasmMsg, +}; +use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; + +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use astroport_governance::outpost::Config; +use astroport_governance::{ + assembly::ProposalVoteOption, + interchain::Hub, + outpost::{Cw20HookMsg, ExecuteMsg}, + voting_escrow_lite::get_emissions_voting_power, +}; + +use crate::query::get_user_voting_power; +use crate::state::VOTES; +use crate::{ + error::ContractError, + state::{PendingVote, CONFIG, OWNERSHIP_PROPOSAL, PENDING_VOTES, PROPOSALS_CACHE}, +}; + +/// Exposes all the execute functions available in the contract. +/// +/// ## Execute messages +/// * **ExecuteMsg::Receive(cw20_msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes +/// it depending on the received template. +/// +/// RemoveOutpost { outpost_addr } Removes an outpost from the hub but does not close the channel, but all messages will be rejected +/// +/// * **ExecuteMsg::Receive(msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes +/// it depending on the received template. +/// +/// * **ExecuteMsg::UpdateConfig { hub_addr }** Update parameters in the Outpost contract. Only the owner is allowed to +/// update the config +/// +/// * **ExecuteMsg::CastAssemblyVote { proposal_id, vote }** Cast a vote on an Assembly proposal from an Outpost +/// +/// * **ExecuteMsg::CastEmissionsVote { votes }** Cast a vote during an emissions voting period +/// +/// * **ExecuteMsg::KickUnlocked { user }** Kick an unlocked voter's voting power from the Generator Controller on the Hub +/// +/// * **ExecuteMsg::WithdrawHubFunds {}** Withdraw stuck funds from the Hub in case of specific IBC failures +/// +/// * **ExecuteMsg::ProposeNewOwner { new_owner, expires_in }** Creates a new request to change +/// contract ownership. +/// +/// * **ExecuteMsg::DropOwnershipProposal {}** Removes a request to change contract ownership. +/// +/// * **ExecuteMsg::ClaimOwnership {}** Claims contract ownership. +/// +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::UpdateConfig { + hub_addr, + hub_channel, + ibc_timeout_seconds, + } => update_config(deps, env, info, hub_addr, hub_channel, ibc_timeout_seconds), + ExecuteMsg::CastAssemblyVote { proposal_id, vote } => { + cast_assembly_vote(deps, env, info, proposal_id, vote) + } + ExecuteMsg::CastEmissionsVote { votes } => cast_emissions_vote(deps, env, info, votes), + ExecuteMsg::KickUnlocked { user } => kick_unlocked(deps, env, info, user), + ExecuteMsg::KickBlacklisted { user } => kick_blacklisted(deps, env, info, user), + ExecuteMsg::WithdrawHubFunds {} => withdraw_hub_funds(deps, env, info), + ExecuteMsg::ProposeNewOwner { + new_owner, + expires_in, + } => { + let config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + new_owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config: Config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) + }) + .map_err(Into::into) + } + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on +/// the received template +/// +/// Funds received here must be from the xASTRO contract and is used for +/// unstaking. +/// +/// * **cw20_msg** CW20 message to process +fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // We only allow xASTRO tokens to be sent here + if info.sender != config.xastro_token_addr { + return Err(ContractError::Unauthorized {}); + } + + match from_binary(&cw20_msg.msg)? { + Cw20HookMsg::Unstake {} => execute_remote_unstake(deps, env, cw20_msg), + } +} + +/// Start the process of unstaking xASTRO from the Hub +/// +/// This burns the xASTRO we previously received and sends the unstake message +/// to the Hub where to original xASTRO will be unstaked and ASTRO returned +/// to the sender of this transaction. +/// +/// Note: Incase of IBC failures they xASTRO will be returned to the user or +/// they'll need to withdraw the unstaked ASTRO from the Hub using ExecuteMsg::WithdrawHubFunds +fn execute_remote_unstake( + deps: DepsMut, + env: Env, + msg: Cw20ReceiveMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Burn the xASTRO tokens we previously minted + let burn_msg = Cw20ExecuteMsg::Burn { amount: msg.amount }; + let wasm_msg = WasmMsg::Execute { + contract_addr: config.xastro_token_addr.to_string(), + msg: to_binary(&burn_msg)?, + funds: vec![], + }; + + let hub_channel = config + .hub_channel + .ok_or(ContractError::MissingHubChannel {})?; + + // Construct the unstake message to send to the Hub + let unstake = Hub::Unstake { + receiver: msg.sender.to_string(), + amount: msg.amount, + }; + let hub_unstake_msg: CosmosMsg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: hub_channel.clone(), + data: to_binary(&unstake)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + + Ok(Response::default() + .add_message(wasm_msg) + .add_message(hub_unstake_msg) + .add_attribute("action", unstake.to_string()) + .add_attribute("amount", msg.amount.to_string()) + .add_attribute("channel", hub_channel)) +} + +/// Update the Outpost config +fn update_config( + deps: DepsMut, + env: Env, + info: MessageInfo, + hub_addr: Option, + hub_channel: Option, + ibc_timeout_seconds: Option, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + // Only owner can update the config + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + if let Some(hub_addr) = hub_addr { + // We can't validate the Hub address + config.hub_addr = hub_addr; + // If a new Hub address is set, we clear the channel as we + // must create a new IBC channel + config.hub_channel = None; + } + + if let Some(hub_channel) = hub_channel { + // Ensure we have the channel that is being set + check_contract_supports_channel(deps.querier, &env.contract.address, &hub_channel)?; + + // Update the channel to the correct one + config.hub_channel = Some(hub_channel); + } + + if let Some(ibc_timeout_seconds) = ibc_timeout_seconds { + if !(MIN_IBC_TIMEOUT_SECONDS..=MAX_IBC_TIMEOUT_SECONDS).contains(&ibc_timeout_seconds) { + return Err(ContractError::InvalidIBCTimeout { + timeout: ibc_timeout_seconds, + min: MIN_IBC_TIMEOUT_SECONDS, + max: MAX_IBC_TIMEOUT_SECONDS, + }); + } + config.ibc_timeout_seconds = ibc_timeout_seconds; + } + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default()) +} + +/// Cast a vote on a proposal from an Outpost +/// +/// To validate the xASTRO holdings at the time the proposal was created we first +/// query the Hub for the proposal information if it hasn't been queried yet. Once +/// the proposal information is received we validate the vote and submit it +fn cast_assembly_vote( + deps: DepsMut, + env: Env, + info: MessageInfo, + proposal_id: u64, + vote_option: ProposalVoteOption, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + let hub_channel = config + .hub_channel + .ok_or(ContractError::MissingHubChannel {})?; + + // Check if this user has voted already + if VOTES.has(deps.storage, (&info.sender, proposal_id)) { + return Err(ContractError::AlreadyVoted {}); + } + + // If we have this proposal in our local cached already, we can continue + // with fetching the voting power and submitting the vote + if let Some(proposal) = PROPOSALS_CACHE.may_load(deps.storage, proposal_id)? { + let voting_power = + get_user_voting_power(deps.as_ref(), info.sender.clone(), proposal.start_time)?; + + if voting_power.is_zero() { + return Err(ContractError::NoVotingPower { + address: info.sender.to_string(), + }); + } + + // Construct the vote message and submit it to the Hub + let cast_vote = Hub::CastAssemblyVote { + proposal_id: proposal.id.u64(), + vote_option: vote_option.clone(), + voter: info.sender.clone(), + voting_power, + }; + let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: hub_channel, + data: to_binary(&cast_vote)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + + // Log the vote to prevent spamming + VOTES.save(deps.storage, (&info.sender, proposal_id), &vote_option)?; + + return Ok(Response::new() + .add_message(hub_msg) + .add_attribute("action", cast_vote.to_string()) + .add_attribute("user", info.sender.to_string())); + } + + // If we don't have the proposal in our local cache it means that no + // vote has been cast from this Outpost for this proposal + // In this case we temporarily store the vote and submit an IBC transaction + // to fetch the proposal information. When the information is received via + // an IBC reply, we validate the data and submit the actual vote + + // If we already have a pending vote for this proposal we return an error + // as we're waiting for the proposal IBC query to return. We can't store + // lots of votes as we have no way to automatically submit them without + // the risk of running out of gas + + if PENDING_VOTES.has(deps.storage, proposal_id) { + return Err(ContractError::PendingVoteExists { proposal_id }); + } + + // Temporarily store the vote + let pending_vote = PendingVote { + proposal_id, + voter: info.sender, + vote_option, + }; + PENDING_VOTES.save(deps.storage, proposal_id, &pending_vote)?; + + // Query for proposal + let query_proposal = Hub::QueryProposal { id: proposal_id }; + let hub_query_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: hub_channel, + data: to_binary(&query_proposal)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + + Ok(Response::default() + .add_message(hub_query_msg) + .add_attribute("action", query_proposal.to_string()) + .add_attribute("id", proposal_id.to_string())) +} + +/// Cast a vote on emissions during a vxASTRO voting period +/// +/// We validate the voting power by checking the vxASTRO power at this +/// moment as vxASTRO lite does not have any warmup period +fn cast_emissions_vote( + deps: DepsMut, + env: Env, + info: MessageInfo, + votes: Vec<(String, u16)>, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Validate vxASTRO voting power + let vxastro_voting_power = + get_emissions_voting_power(&deps.querier, config.vxastro_token_addr, &info.sender)?; + + if vxastro_voting_power.is_zero() { + return Err(ContractError::NoVotingPower { + address: info.sender.to_string(), + }); + } + + let hub_channel = config + .hub_channel + .ok_or(ContractError::MissingHubChannel {})?; + + // Construct the vote message and submit it to the Hub + let cast_vote = Hub::CastEmissionsVote { + voter: info.sender.clone(), + voting_power: vxastro_voting_power, + votes, + }; + let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: hub_channel, + data: to_binary(&cast_vote)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + Ok(Response::new() + .add_message(hub_msg) + .add_attribute("action", cast_vote.to_string()) + .add_attribute("user", info.sender.to_string())) +} + +/// Kick an unlocked voter from the Generator Controller on the Hub +/// which will remove their voting power immediately. +/// +/// We only finalise the unlock in the vxASTRO contract when this kick is +/// successful +fn kick_unlocked( + deps: DepsMut, + env: Env, + info: MessageInfo, + user: Addr, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // This may only be called from the vxASTRO lite contract + if info.sender != config.vxastro_token_addr { + return Err(ContractError::Unauthorized {}); + } + + let hub_channel = config + .hub_channel + .ok_or(ContractError::MissingHubChannel {})?; + + // Construct the kick message and submit it to the Hub + let kick_unlocked = Hub::KickUnlockedVoter { + voter: user.clone(), + }; + let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: hub_channel, + data: to_binary(&kick_unlocked)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + + Ok(Response::new() + .add_message(hub_msg) + .add_attribute("action", kick_unlocked.to_string()) + .add_attribute("user", user)) +} + +/// Kick a blacklisted voter from the Generator Controller on the Hub +/// which will remove their voting power immediately. +/// +/// This can be called multiple times without unintended side effects +fn kick_blacklisted( + deps: DepsMut, + env: Env, + info: MessageInfo, + user: Addr, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // This may only be called from the vxASTRO lite contract + if info.sender != config.vxastro_token_addr { + return Err(ContractError::Unauthorized {}); + } + + let hub_channel = config + .hub_channel + .ok_or(ContractError::MissingHubChannel {})?; + + // Construct the kick message and submit it to the Hub + let kick_blacklisted = Hub::KickBlacklistedVoter { + voter: user.clone(), + }; + let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: hub_channel, + data: to_binary(&kick_blacklisted)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + + Ok(Response::new() + .add_message(hub_msg) + .add_attribute("action", kick_blacklisted.to_string()) + .add_attribute("user", user)) +} + +/// Submit a request to withdraw / retry sending funds stuck on the Hub +/// back to the sender address. This is possible because of IBC failures. +/// +/// This will only return the funds of the user executing this transaction. +fn withdraw_hub_funds( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + let hub_channel = config + .hub_channel + .ok_or(ContractError::MissingHubChannel {})?; + + // Construct the withdraw message and submit it to the Hub + let withdraw = Hub::WithdrawFunds { + user: info.sender.clone(), + }; + let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: hub_channel, + data: to_binary(&withdraw)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + + Ok(Response::new() + .add_message(hub_msg) + .add_attribute("action", withdraw.to_string()) + .add_attribute("user", info.sender.to_string())) +} + +#[cfg(test)] +mod tests { + + use super::*; + + use cosmwasm_std::{testing::mock_info, IbcMsg, ReplyOn, SubMsg, Uint128, Uint64}; + + use crate::{ + contract::instantiate, + mock::{mock_all, setup_channel, HUB, OWNER, VXASTRO_TOKEN, XASTRO_TOKEN}, + query::query, + }; + use astroport_governance::interchain::{Hub, ProposalSnapshot}; + + // Test Cases: + // + // Expect Success + // - An unstake IBC message is emitted + // + // Expect Error + // - No xASTRO is sent to the contract + // - The funds sent to the contract is not xASTRO + // - The Hub address and channel isn't set + // + #[test] + fn unstake() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let user_funds = Uint128::from(1000u128); + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Set up valid Hub + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Attempt to unstake with an incorrect token + let err = execute( + deps.as_mut(), + env.clone(), + mock_info("not_xastro", &[]), + astroport_governance::outpost::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: user.to_string(), + amount: user_funds, + msg: to_binary(&astroport_governance::outpost::Cw20HookMsg::Unstake {}).unwrap(), + }), + ) + .unwrap_err(); + + assert_eq!(err, ContractError::Unauthorized {}); + + // Attempt to unstake correctly + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(XASTRO_TOKEN, &[]), + astroport_governance::outpost::ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: user.to_string(), + amount: user_funds, + msg: to_binary(&astroport_governance::outpost::Cw20HookMsg::Unstake {}).unwrap(), + }), + ) + .unwrap(); + + // Build the expected message + let ibc_message = to_binary(&Hub::Unstake { + receiver: user.to_string(), + amount: user_funds, + }) + .unwrap(); + + // We should have two messages + assert_eq!(res.messages.len(), 2); + + // First message must be the burn of the amount of xASTRO sent + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: XASTRO_TOKEN.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Burn { amount: user_funds }).unwrap(), + funds: vec![], + } + .into(), + } + ); + + // Second message must be the IBC unstake + assert_eq!( + res.messages[1], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: ibc_message, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - The config is updated + // + // Expect Error + // - When the config is updated by a non-owner + // + #[test] + fn update_config() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + // Attempt to update the hub address by a non-owner + let err = execute( + deps.as_mut(), + env.clone(), + mock_info("not_owner", &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: Some("new_hub".to_string()), + hub_channel: None, + ibc_timeout_seconds: None, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::Unauthorized {}); + + let config = query( + deps.as_ref(), + env.clone(), + astroport_governance::outpost::QueryMsg::Config {}, + ) + .unwrap(); + + // Ensure the config set during instantiation is still there + assert_eq!( + config, + to_binary(&astroport_governance::outpost::Config { + owner: Addr::unchecked(OWNER), + xastro_token_addr: Addr::unchecked(XASTRO_TOKEN), + vxastro_token_addr: Addr::unchecked(VXASTRO_TOKEN), + hub_addr: HUB.to_string(), + hub_channel: None, + ibc_timeout_seconds: 10, + }) + .unwrap() + ); + + // Attempt to update the hub address by the owner + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: Some("new_owner_hub".to_string()), + hub_channel: None, + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + let config = query( + deps.as_ref(), + env.clone(), + astroport_governance::outpost::QueryMsg::Config {}, + ) + .unwrap(); + + // Ensure the config set after the update is correct + // Once a new Hub is set, the Hub channel is cleared to allow a new + // connection + assert_eq!( + config, + to_binary(&astroport_governance::outpost::Config { + owner: Addr::unchecked(OWNER), + xastro_token_addr: Addr::unchecked(XASTRO_TOKEN), + vxastro_token_addr: Addr::unchecked(VXASTRO_TOKEN), + hub_addr: "new_owner_hub".to_string(), + hub_channel: None, + ibc_timeout_seconds: 10, + }) + .unwrap() + ); + + // Update the hub channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-15".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + let config = query( + deps.as_ref(), + env.clone(), + astroport_governance::outpost::QueryMsg::Config {}, + ) + .unwrap(); + + // Ensure the config set after the update is correct + // Once a new Hub is set, the Hub channel is cleared to allow a new + // connection + assert_eq!( + config, + to_binary(&astroport_governance::outpost::Config { + owner: Addr::unchecked(OWNER), + xastro_token_addr: Addr::unchecked(XASTRO_TOKEN), + vxastro_token_addr: Addr::unchecked(VXASTRO_TOKEN), + hub_addr: "new_owner_hub".to_string(), + hub_channel: Some("channel-15".to_string()), + ibc_timeout_seconds: 10, + }) + .unwrap() + ); + + // Update the IBC timeout + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: None, + ibc_timeout_seconds: Some(35), + }, + ) + .unwrap(); + + let config = query( + deps.as_ref(), + env, + astroport_governance::outpost::QueryMsg::Config {}, + ) + .unwrap(); + + // Ensure the config set after the update is correct + // Once a new Hub is set, the Hub channel is cleared to allow a new + // connection + assert_eq!( + config, + to_binary(&astroport_governance::outpost::Config { + owner: Addr::unchecked(OWNER), + xastro_token_addr: Addr::unchecked(XASTRO_TOKEN), + vxastro_token_addr: Addr::unchecked(VXASTRO_TOKEN), + hub_addr: "new_owner_hub".to_string(), + hub_channel: Some("channel-15".to_string()), + ibc_timeout_seconds: 35, + }) + .unwrap() + ); + } + + // Test Cases: + // + // Expect Success + // - A proposal query is emitted when the proposal is not in the cache + // - A vote is emitted when the proposal is in the cache + // + // Expect Error + // - User has no voting power at the time of the proposal + // + #[test] + fn vote_on_proposal() { + let (mut deps, env, info) = mock_all(OWNER); + + let proposal_id = 1u64; + let user = "user"; + let voting_power = 1000u64; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up valid Hub + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Cast a vote with no proposal in the cache + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(user, &[]), + astroport_governance::outpost::ExecuteMsg::CastAssemblyVote { + proposal_id: 1, + vote: astroport_governance::assembly::ProposalVoteOption::For, + }, + ) + .unwrap(); + + // Wrap the query + let ibc_message = to_binary(&Hub::QueryProposal { id: proposal_id }).unwrap(); + + // Ensure a query is emitted + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: ibc_message, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + + // Add a proposal to the cache + PROPOSALS_CACHE + .save( + &mut deps.storage, + proposal_id, + &ProposalSnapshot { + id: Uint64::from(proposal_id), + start_time: 1689939457, + }, + ) + .unwrap(); + + // Cast a vote with a proposal in the cache + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(user, &[]), + astroport_governance::outpost::ExecuteMsg::CastAssemblyVote { + proposal_id, + vote: astroport_governance::assembly::ProposalVoteOption::For, + }, + ) + .unwrap(); + + // Build the expected message + let ibc_message = to_binary(&Hub::CastAssemblyVote { + proposal_id, + voter: Addr::unchecked(user), + vote_option: astroport_governance::assembly::ProposalVoteOption::For, + voting_power: Uint128::from(voting_power), + }) + .unwrap(); + + // We should only have 1 message + assert_eq!(res.messages.len(), 1); + + // Ensure a vote is emitted + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: ibc_message, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + + // Cast a vote on a proposal already voted on + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(user, &[]), + astroport_governance::outpost::ExecuteMsg::CastAssemblyVote { + proposal_id, + vote: astroport_governance::assembly::ProposalVoteOption::For, + }, + ) + .unwrap_err(); + + assert_eq!(err, ContractError::AlreadyVoted {}); + + // Check that we can query the vote + let vote_data = query( + deps.as_ref(), + env, + astroport_governance::outpost::QueryMsg::ProposalVoted { + proposal_id, + user: user.to_string(), + }, + ) + .unwrap(); + + assert_eq!(vote_data, to_binary(&ProposalVoteOption::For).unwrap()); + } + + // Test Cases: + // + // Expect Success + // - An emissions vote is emitted is the user has voting power + // + // Expect Error + // - User has no voting power + // + #[test] + fn vote_on_emissions() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let votes = vec![("pool".to_string(), 10000u16)]; + let voting_power = 1000u64; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up valid Hub + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Cast a vote on emissions + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(user, &[]), + astroport_governance::outpost::ExecuteMsg::CastEmissionsVote { + votes: votes.clone(), + }, + ) + .unwrap(); + + // Build the expected message + let ibc_message = to_binary(&Hub::CastEmissionsVote { + voter: Addr::unchecked(user), + votes, + voting_power: Uint128::from(voting_power), + }) + .unwrap(); + + // We should only have 1 message + assert_eq!(res.messages.len(), 1); + + // Ensure a vote is emitted + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: ibc_message, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - The kick message is forwarded + // + // Expect Error + // - When the sender is not the vxASTRO contract + // + #[test] + fn kick_unlocked() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up valid Hub + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Kick a user as another user, not allowed + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(user, &[]), + astroport_governance::outpost::ExecuteMsg::KickUnlocked { + user: Addr::unchecked(user), + }, + ) + .unwrap_err(); + + assert_eq!(err, ContractError::Unauthorized {}); + + // Kick a user as the vxASTRO contract + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(VXASTRO_TOKEN, &[]), + astroport_governance::outpost::ExecuteMsg::KickUnlocked { + user: Addr::unchecked(user), + }, + ) + .unwrap(); + + // Build the expected message + let ibc_message = to_binary(&Hub::KickUnlockedVoter { + voter: Addr::unchecked(user), + }) + .unwrap(); + + // We should only have 1 message + assert_eq!(res.messages.len(), 1); + + // Ensure a kick is emitted + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: ibc_message, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - The kick message is forwarded + // + // Expect Error + // - When the sender is not the vxASTRO contract + // + #[test] + fn kick_blacklisted() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up valid Hub + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Kick a user as another user, not allowed + let err = execute( + deps.as_mut(), + env.clone(), + mock_info(user, &[]), + astroport_governance::outpost::ExecuteMsg::KickBlacklisted { + user: Addr::unchecked(user), + }, + ) + .unwrap_err(); + + assert_eq!(err, ContractError::Unauthorized {}); + + // Kick a user as the vxASTRO contract + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(VXASTRO_TOKEN, &[]), + astroport_governance::outpost::ExecuteMsg::KickBlacklisted { + user: Addr::unchecked(user), + }, + ) + .unwrap(); + + // Build the expected message + let ibc_message = to_binary(&Hub::KickBlacklistedVoter { + voter: Addr::unchecked(user), + }) + .unwrap(); + + // We should only have 1 message + assert_eq!(res.messages.len(), 1); + + // Ensure a kick is emitted + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: ibc_message, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - The kick message is forwarded + // + // Expect Error + // - When the sender is not the vxASTRO contract + // + #[test] + fn withdraw_funds() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up valid Hub + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Withdraw stuck funds from the Hub + let res = execute( + deps.as_mut(), + env.clone(), + mock_info(user, &[]), + astroport_governance::outpost::ExecuteMsg::WithdrawHubFunds {}, + ) + .unwrap(); + + // Build the expected message + let ibc_message = to_binary(&Hub::WithdrawFunds { + user: Addr::unchecked(user), + }) + .unwrap(); + + // We should only have 1 message + assert_eq!(res.messages.len(), 1); + + // Ensure a withdrawal is emitted + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: ibc_message, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + } +} diff --git a/contracts/outpost/src/ibc.rs b/contracts/outpost/src/ibc.rs new file mode 100644 index 00000000..35b2e14e --- /dev/null +++ b/contracts/outpost/src/ibc.rs @@ -0,0 +1,662 @@ +use cosmwasm_std::{ + ensure, entry_point, from_binary, to_binary, CosmosMsg, Deps, DepsMut, Env, + Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, + IbcChannelOpenMsg, IbcChannelOpenResponse, IbcMsg, IbcOrder, IbcPacketAckMsg, + IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Never, StdError, StdResult, +}; + +use astroport_governance::interchain::{get_contract_from_ibc_port, Hub, Outpost, Response}; + +use crate::{ + error::ContractError, + ibc_failure::handle_failed_messages, + ibc_mint::handle_ibc_xastro_mint, + query::get_user_voting_power, + state::{CONFIG, PENDING_VOTES, PROPOSALS_CACHE}, +}; + +pub const IBC_APP_VERSION: &str = "astroport-outpost-v1"; +pub const IBC_ORDERING: IbcOrder = IbcOrder::Unordered; + +/// Handle the opening of a new IBC channel +/// +/// We verify that the connection is using the correct configuration +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_open( + _deps: DepsMut, + _env: Env, + msg: IbcChannelOpenMsg, +) -> Result { + let channel = msg.channel(); + + if channel.order != IBC_ORDERING { + return Err(ContractError::Std(StdError::generic_err( + "Ordering is invalid. The channel must be unordered".to_string(), + ))); + } + if channel.version != IBC_APP_VERSION { + return Err(ContractError::Std(StdError::generic_err(format!( + "Must set version to `{IBC_APP_VERSION}`" + )))); + } + + if let Some(counter_version) = msg.counterparty_version() { + if counter_version != IBC_APP_VERSION { + return Err(ContractError::Std(StdError::generic_err(format!( + "Counterparty version must be `{IBC_APP_VERSION}`" + )))); + } + } + + Ok(Some(Ibc3ChannelOpenResponse { + version: IBC_APP_VERSION.to_string(), + })) +} + +/// Handle the connection of a new IBC channel +/// +/// We verify that the connection is being made to the configured Hub and +/// if the channel has not been set, add it +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_connect( + deps: DepsMut, + _env: Env, + msg: IbcChannelConnectMsg, +) -> Result { + let channel = msg.channel(); + + if let Some(counter_version) = msg.counterparty_version() { + if counter_version != IBC_APP_VERSION { + return Err(ContractError::Std(StdError::generic_err(format!( + "Counterparty version must be `{IBC_APP_VERSION}`" + )))); + } + } + + // Only a connection to the Hub is allowed + let counterparty_port = + get_contract_from_ibc_port(channel.counterparty_endpoint.port_id.as_str()); + + let config = CONFIG.load(deps.storage)?; + match config.hub_channel { + Some(channel_id) => { + return Err(ContractError::ChannelAlreadyEstablished { channel_id }); + } + None => { + if counterparty_port != config.hub_addr { + return Err(ContractError::InvalidSourcePort { + invalid: counterparty_port.to_string(), + valid: config.hub_addr.to_string(), + }); + } + } + } + + Ok(IbcBasicResponse::new() + .add_attribute("action", "ibc_connect") + .add_attribute("channel_id", &channel.endpoint.channel_id)) +} + +/// Handle the receiving the packets while wrapping the actual call to provide +/// returning errors as an acknowledgement. +/// +/// This allows the original caller from another chain to handle the failure +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_receive( + deps: DepsMut, + env: Env, + msg: IbcPacketReceiveMsg, +) -> Result { + do_packet_receive(deps, env, msg).or_else(|err| { + // Construct an error acknowledgement that can be handled on the Hub + let ack_data = to_binary(&Response::new_error(err.to_string())).unwrap(); + + Ok(IbcReceiveResponse::new() + .add_attribute("action", "ibc_packet_receive") + .add_attribute("error", err.to_string()) + .set_ack(ack_data)) + }) +} + +/// Process the received packet and return the response +/// +/// Packets are expected to be wrapped in the Outpost format, if it doesn't conform +/// it will be failed. +/// +/// If a ContractError is returned, it will be wrapped into a Response +/// containing the error to be handled on the Outpost +fn do_packet_receive( + deps: DepsMut, + _env: Env, + msg: IbcPacketReceiveMsg, +) -> Result { + block_unauthorized_packets( + deps.as_ref(), + msg.packet.src.port_id.clone(), + msg.packet.dest.channel_id.clone(), + )?; + + // Parse the packet data into a Hub message + let hub_msg: Outpost = from_binary(&msg.packet.data)?; + match hub_msg { + Outpost::MintXAstro { receiver, amount } => handle_ibc_xastro_mint(deps, receiver, amount), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_timeout( + deps: DepsMut, + _env: Env, + msg: IbcPacketTimeoutMsg, +) -> Result { + let mut response = IbcBasicResponse::new().add_attribute("action", "ibc_packet_timeout"); + + // In case of an IBC timeout we might need to reverse actions similar + // to failed messages. + // We look at the original packet to determine what failed and take + // the appropriate action + let failed_msg: Hub = from_binary(&msg.packet.data)?; + response = handle_failed_messages(deps, failed_msg, response)?; + + Ok(response) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_ack( + deps: DepsMut, + env: Env, + msg: IbcPacketAckMsg, +) -> Result { + let mut response = IbcBasicResponse::new().add_attribute("action", "ibc_packet_ack"); + + let ack: Result = from_binary(&msg.acknowledgement.data); + match ack { + Ok(hub_response) => { + match hub_response { + Response::QueryProposal(proposal) => { + // We cache the proposal ID and start time for future vote + // checks without needing to query the Hub again + PROPOSALS_CACHE.save(deps.storage, proposal.id.u64(), &proposal)?; + + // We need to submit the initial vote that triggered this + // proposal to be queried from the pending vote cache + if let Some(pending_vote) = + PENDING_VOTES.may_load(deps.storage, proposal.id.u64())? + { + let config = CONFIG.load(deps.storage)?; + + let voting_power = get_user_voting_power( + deps.as_ref(), + pending_vote.voter.clone(), + proposal.start_time, + )?; + + if voting_power.is_zero() { + return Err(ContractError::NoVotingPower { + address: pending_vote.voter.to_string(), + }); + } + + let hub_channel = config + .hub_channel + .ok_or(ContractError::MissingHubChannel {})?; + + // Construct the vote message and submit it to the Hub + let cast_vote = Hub::CastAssemblyVote { + proposal_id: proposal.id.u64(), + vote_option: pending_vote.vote_option, + voter: pending_vote.voter.clone(), + voting_power, + }; + let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { + channel_id: hub_channel, + data: to_binary(&cast_vote)?, + timeout: env + .block + .time + .plus_seconds(config.ibc_timeout_seconds) + .into(), + }); + response = response + .add_message(hub_msg) + .add_attribute("action", cast_vote.to_string()) + .add_attribute("user", pending_vote.voter.to_string()); + + // Remove this pending vote from the cache + PENDING_VOTES.remove(deps.storage, proposal.id.u64()); + } + + response = response + .add_attribute("hub_response", "query_response") + .add_attribute("response_type", "proposal") + .add_attribute("proposal_id", proposal.id.to_string()) + .add_attribute("proposal_start", proposal.start_time.to_string()) + } + Response::Result { + action, + address, + error, + } => { + response = response + .add_attribute("action", action.unwrap_or_else(|| "unknown".to_string())) + .add_attribute("user", address.unwrap_or_else(|| "unknown".to_string())) + .add_attribute("err", error.unwrap_or_else(|| "unknown".to_string())) + } + } + } + Err(err) => { + // In case of error, ack.data will be in the format similar to + // {"error":"ABCI code: 5: error handling packet: see events for details"} + // but the events do not contain the details + // + // Instead we look at the original packet to determine what failed, + // the reason for the failure can't be determined at this time due + // to a limitation in wasmd/wasmvm. For us we just need to know what failed, + // the reason is not required to continue + // See https://github.com/CosmWasm/cosmwasm/issues/1707 + + let raw_error = base64::encode(&msg.acknowledgement.data); + // Attach the errors to the response + response = response + .add_attribute("raw_error", raw_error) + .add_attribute("ack_error", err.to_string()); + + // Handle the possible failures + let original: Hub = from_binary(&msg.original_packet.data)?; + response = handle_failed_messages(deps, original, response)?; + } + } + Ok(response) +} + +/// Handle the closing of IBC channels, which we don't allow +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_close( + _deps: DepsMut, + _env: Env, + _channel: IbcChannelCloseMsg, +) -> StdResult { + Err(StdError::generic_err("Closing channel is not allowed")) +} + +/// Checks the provided port against the known Hub. +/// +/// If the port doesn't exist, this function will return an error, effectively blocking the packet. +fn block_unauthorized_packets( + deps: Deps, + port_id: String, + channel_id: String, +) -> Result<(), ContractError> { + let config = CONFIG.load(deps.storage)?; + let counterparty_port = get_contract_from_ibc_port(port_id.as_str()); + ensure!( + config.hub_addr == counterparty_port, + ContractError::Unauthorized {} + ); + + ensure!( + config.hub_channel == Some(channel_id), + ContractError::Unauthorized {} + ); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use astroport_governance::interchain::ProposalSnapshot; + use cosmwasm_std::{ + testing::{mock_info, MOCK_CONTRACT_ADDR}, + Addr, IbcAcknowledgement, IbcEndpoint, IbcPacket, ReplyOn, SubMsg, Uint128, Uint64, + }; + + use super::*; + use crate::{ + contract::instantiate, + execute::execute, + mock::{mock_all, mock_channel, setup_channel, HUB, OWNER, VXASTRO_TOKEN, XASTRO_TOKEN}, + state::PendingVote, + }; + + // Test Cases: + // + // Expect Success + // - Creating a channel with correct settings + // + // Expect Error + // - Attempt to create a channel with an invalid version + // - Attempt to create a channel with an invalid ordering + #[test] + fn ibc_open_channel() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // A connection with invalid ordering is not allowed + let channel = mock_channel( + "wasm.outpost", + "channel-2", + "wasm.unknown_contract", + "channel-7", + IbcOrder::Ordered, + "non-astroport-v1", + ); + let open_msg = IbcChannelOpenMsg::new_init(channel); + let err = ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err( + "Ordering is invalid. The channel must be unordered" + )) + ); + + // A connection with invalid version is not allowed + let channel = mock_channel( + "wasm.outpost", + "channel-2", + "wasm.unknown_contract", + "channel-7", + IbcOrder::Unordered, + "non-astroport-v1", + ); + let open_msg = IbcChannelOpenMsg::new_init(channel); + let err = ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err( + "Must set version to `astroport-outpost-v1`" + )) + ); + + // A connection with correct settings is allowed + let channel = mock_channel( + "wasm.outpost", + "channel-2", + "wasm.unknown_contract", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + let open_msg = IbcChannelOpenMsg::new_init(channel); + ibc_channel_open(deps.as_mut(), env, open_msg).unwrap(); + + // let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + // ibc_channel_connect(deps.as_mut(), env.clone(), connect_msg).unwrap(); + } + + // Test Cases: + // + // Expect Success + // - Creating a channel with an allowed Outpost + // + // Expect Error + // - Attempt to connect a channel with an invalid version + // - Attempt to connect a channel before registering an Outpost + // - Attempt to connect a channel with an unauthorize Outpost address + #[test] + fn ibc_connect_channel() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + // Opening a connection with unknown contracts is not allowed + let channel = mock_channel( + "wasm.outpost", + "channel-2", + "wasm.unknown_contract", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + let err = ibc_channel_connect(deps.as_mut(), env.clone(), connect_msg).unwrap_err(); + assert_eq!( + err, + ContractError::InvalidSourcePort { + invalid: "unknown_contract".to_string(), + valid: "hub".to_string() + } + ); + + // Opening a connection with the hub is allowed + let channel = mock_channel( + "wasm.outpost", + "channel-3", + format!("wasm.{}", HUB).as_str(), + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + + // Attempt to connect with the wrong IBC app version + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel.clone(), "WRONG_VERSION"); + let err = ibc_channel_connect(deps.as_mut(), env.clone(), connect_msg).unwrap_err(); + assert_eq!( + err, + ContractError::Std(StdError::generic_err(format!( + "Counterparty version must be `{}`", + IBC_APP_VERSION + ))) + ); + + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + ibc_channel_connect(deps.as_mut(), env.clone(), connect_msg).unwrap(); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Attempting to open the channel again is not allowed + let channel = mock_channel( + "wasm.outpost", + "channel-3", + format!("wasm.{}", HUB).as_str(), + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.as_mut(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + let err = ibc_channel_connect(deps.as_mut(), env, connect_msg).unwrap_err(); + assert_eq!( + err, + ContractError::ChannelAlreadyEstablished { + channel_id: "channel-3".to_string(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - Query results returned in the acknoledgement data is processed correctly + #[test] + fn ibc_ack_packet() { + let (mut deps, env, info) = mock_all(OWNER); + + let proposal_id = 1u64; + let user = "user"; + let voting_power = 1000u64; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up valid Hub + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // The pending would be stored in the contract before the query is sent + let pending_vote = PendingVote { + proposal_id, + voter: Addr::unchecked(user), + vote_option: astroport_governance::assembly::ProposalVoteOption::For, + }; + PENDING_VOTES + .save(&mut deps.storage, proposal_id, &pending_vote) + .unwrap(); + + let proposal_response = Response::QueryProposal(ProposalSnapshot { + id: Uint64::from(proposal_id), + start_time: 1689942949u64, + }); + + let ack = IbcAcknowledgement::new(to_binary(&proposal_response).unwrap()); + let mint_msg = to_binary(&Outpost::MintXAstro { + receiver: "user".to_owned(), + amount: Uint128::one(), + }) + .unwrap(); + let original_packet = IbcPacket::new( + mint_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: format!("wasm.{}", HUB), + channel_id: "channel-7".to_string(), + }, + 3, + env.block.time.plus_seconds(10).into(), + ); + + let ack_msg = IbcPacketAckMsg::new(ack, original_packet, Addr::unchecked("relayer")); + let res = ibc_packet_ack(deps.as_mut(), env.clone(), ack_msg).unwrap(); + + // If we received the proposal, we can now submit the vote + assert_eq!(res.messages.len(), 1); + + // Build the expected message + let ibc_message = to_binary(&Hub::CastAssemblyVote { + proposal_id, + voter: Addr::unchecked(user), + vote_option: astroport_governance::assembly::ProposalVoteOption::For, + voting_power: Uint128::from(voting_power), + }) + .unwrap(); + + // Ensure a vote is emitted + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: IbcMsg::SendPacket { + channel_id: "channel-3".to_string(), + data: ibc_message, + timeout: env.block.time.plus_seconds(ibc_timeout_seconds).into(), + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - Creating a channel with an allowed Outpost + // + // Expect Error + // - Attempt to connect a channel with an invalid version + // - Attempt to connect a channel before registering an Outpost + // - Attempt to connect a channel with an unauthorize Outpost address + #[test] + fn ibc_close_channel() { + let (mut deps, env, info) = mock_all(OWNER); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + let channel = mock_channel( + "wasm.outpost", + "channel-3", + "wasm.hub", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + + let close_msg = IbcChannelCloseMsg::new_init(channel); + let err = ibc_channel_close(deps.as_mut(), env, close_msg).unwrap_err(); + + assert_eq!(err, StdError::generic_err("Closing channel is not allowed")); + } +} diff --git a/contracts/outpost/src/ibc_failure.rs b/contracts/outpost/src/ibc_failure.rs new file mode 100644 index 00000000..1ccd9747 --- /dev/null +++ b/contracts/outpost/src/ibc_failure.rs @@ -0,0 +1,656 @@ +use cosmwasm_std::{to_binary, DepsMut, IbcBasicResponse, WasmMsg}; + +use astroport_governance::{interchain::Hub, voting_escrow_lite}; + +use crate::{ + error::ContractError, + ibc_mint::mint_xastro_msg, + state::{CONFIG, PENDING_VOTES, VOTES}, +}; + +pub fn handle_failed_messages( + deps: DepsMut, + failed_msg: Hub, + mut response: IbcBasicResponse, +) -> Result { + match failed_msg.clone() { + Hub::CastAssemblyVote { + proposal_id, voter, .. + } => { + // Vote failed, remove vote from the log so user may retry + VOTES.remove(deps.storage, (&voter, proposal_id)); + + response = response + .add_attribute("interchain_action", failed_msg.to_string()) + .add_attribute("user", voter.to_string()); + } + Hub::CastEmissionsVote { voter, .. } => { + response = response + .add_attribute("interchain_action", failed_msg.to_string()) + .add_attribute("user", voter.to_string()); + } + Hub::QueryProposal { id } => { + // If the proposal query failed we need to remove the pending vote + // otherwise no other vote will be possible for this proposal + let pending_vote = PENDING_VOTES.load(deps.storage, id)?; + PENDING_VOTES.remove(deps.storage, id); + + response = response + .add_attribute("interchain_action", failed_msg.to_string()) + .add_attribute("user", pending_vote.voter.to_string()); + } + + Hub::Unstake { receiver, amount } => { + // Unstaking involves us burning the received xASTRO before + // sending the unstake message to the Hub. If the unstaking + // fails we need to mint the xASTRO back to the user + let msg = mint_xastro_msg(deps.as_ref(), receiver.clone(), amount)?; + response = response + .add_message(msg) + .add_attribute("interchain_action", failed_msg.to_string()) + .add_attribute("user", receiver); + } + Hub::KickUnlockedVoter { voter } => { + // The voting power has not been removed for this user and we must + // relock their unlocking position + let config = CONFIG.load(deps.storage)?; + + let relock_msg = voting_escrow_lite::ExecuteMsg::Relock { + user: voter.to_string(), + }; + + let msg = WasmMsg::Execute { + contract_addr: config.vxastro_token_addr.to_string(), + msg: to_binary(&relock_msg)?, + funds: vec![], + }; + + response = response + .add_message(msg) + .add_attribute("interchain_action", failed_msg.to_string()) + .add_attribute("user", voter); + } + Hub::WithdrawFunds { user } => { + response = response + .add_attribute("interchain_action", failed_msg.to_string()) + .add_attribute("user", user.to_string()); + } + // Not all Hub responses will be received here, we only handle the ones we have + // control over + _ => { + response = response.add_attribute("action", failed_msg.to_string()); + } + } + Ok(response) +} + +#[cfg(test)] +mod tests { + use cosmwasm_std::{ + attr, + testing::{mock_info, MOCK_CONTRACT_ADDR}, + to_binary, Addr, IbcEndpoint, IbcPacket, IbcPacketTimeoutMsg, ReplyOn, StdError, SubMsg, + Uint128, WasmMsg, + }; + + use super::*; + use crate::{ + contract::instantiate, + execute::execute, + ibc::ibc_packet_timeout, + mock::{mock_all, setup_channel, HUB, OWNER, VXASTRO_TOKEN, XASTRO_TOKEN}, + state::PendingVote, + }; + + // Test Cases: + // + // Expect Success + // - xASTRO is returned to the original sender + // + // Expect Error + // - Receive timeout from a different channel + #[test] + fn unstake_failure() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let amount = Uint128::from(1000u64); + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Attempt to get timeout from different contract + let original_unstake_msg = to_binary(&Hub::Unstake { + receiver: user.to_string(), + amount, + }) + .unwrap(); + + let packet = IbcPacket::new( + original_unstake_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: format!("wasm.{}", HUB), + channel_id: "channel-7".to_string(), + }, + 4, + env.block.time.plus_seconds(ibc_timeout_seconds).into(), + ); + + // When the timeout occurs, we should see an unstake message to return the ASTRO to the user + let timeout_packet = IbcPacketTimeoutMsg::new(packet, Addr::unchecked("relayer")); + let res = ibc_packet_timeout(deps.as_mut(), env, timeout_packet).unwrap(); + + // Should have exactly one message + assert_eq!(res.messages.len(), 1); + + // Verify that the mint message matches the expected message + let xastro_mint_msg = to_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: user.to_string(), + amount, + }) + .unwrap(); + + // We should see the mint xASTRO SubMessage + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: XASTRO_TOKEN.to_string(), + msg: xastro_mint_msg, + funds: vec![], + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - Vote fails to reach the Hub + #[test] + fn governance_vote_failure() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let proposal_id = 1u64; + let voting_power = Uint128::from(1000u64); + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Construct the original message + let original_msg = to_binary(&Hub::CastAssemblyVote { + proposal_id, + voter: Addr::unchecked(user), + vote_option: astroport_governance::assembly::ProposalVoteOption::For, + voting_power, + }) + .unwrap(); + // Authorised channels + let packet = IbcPacket::new( + original_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: format!("wasm.{}", HUB), + channel_id: "channel-7".to_string(), + }, + 4, + env.block.time.plus_seconds(ibc_timeout_seconds).into(), + ); + + // When the timeout occurs, we should see the correct attributes emitted + let timeout_packet = IbcPacketTimeoutMsg::new(packet, Addr::unchecked("relayer")); + let res = ibc_packet_timeout(deps.as_mut(), env, timeout_packet).unwrap(); + + // Should have no messages + assert_eq!(res.messages.len(), 0); + + // Should have the correct attributes + assert_eq!( + res.attributes, + vec![ + attr("action".to_string(), "ibc_packet_timeout".to_string()), + attr( + "interchain_action".to_string(), + "cast_assembly_vote".to_string() + ), + attr("user".to_string(), user.to_string()), + ] + ); + } + + // Test Cases: + // + // Expect Success + // - Emissions Vote fails to reach the Hub + #[test] + fn emissions_vote_failure() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let votes = vec![("pool".to_string(), 10000u16)]; + let voting_power = Uint128::from(1000u64); + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Construct the original message + let original_msg = to_binary(&Hub::CastEmissionsVote { + voter: Addr::unchecked(user), + voting_power, + votes, + }) + .unwrap(); + // Authorised channels + let packet = IbcPacket::new( + original_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: format!("wasm.{}", HUB), + channel_id: "channel-7".to_string(), + }, + 4, + env.block.time.plus_seconds(ibc_timeout_seconds).into(), + ); + + // When the timeout occurs, we should see the correct attributes emitted + let timeout_packet = IbcPacketTimeoutMsg::new(packet, Addr::unchecked("relayer")); + let res = ibc_packet_timeout(deps.as_mut(), env, timeout_packet).unwrap(); + + // Should have no messages + assert_eq!(res.messages.len(), 0); + + // Should have the correct attributes + assert_eq!( + res.attributes, + vec![ + attr("action".to_string(), "ibc_packet_timeout".to_string()), + attr( + "interchain_action".to_string(), + "cast_emissions_vote".to_string() + ), + attr("user".to_string(), user.to_string()), + ] + ); + } + + // Test Cases: + // + // Expect Success + // - Proposal query fails + #[test] + fn query_proposal_failure() { + let (mut deps, env, info) = mock_all(OWNER); + + let proposal_id = 1u64; + let user = "user"; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Construct the original message + let original_msg = to_binary(&Hub::QueryProposal { id: proposal_id }).unwrap(); + // Authorised channels + let packet = IbcPacket::new( + original_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: format!("wasm.{}", HUB), + channel_id: "channel-7".to_string(), + }, + 4, + env.block.time.plus_seconds(ibc_timeout_seconds).into(), + ); + + // Ensure we have a pending vote + PENDING_VOTES + .save( + &mut deps.storage, + proposal_id, + &PendingVote { + proposal_id, + vote_option: astroport_governance::assembly::ProposalVoteOption::For, + voter: Addr::unchecked(user), + }, + ) + .unwrap(); + + // When the timeout occurs, we should see the correct attributes emitted + let timeout_packet = IbcPacketTimeoutMsg::new(packet, Addr::unchecked("relayer")); + let res = ibc_packet_timeout(deps.as_mut(), env, timeout_packet).unwrap(); + + // Should have no messages + assert_eq!(res.messages.len(), 0); + + // Should have the correct attributes + assert_eq!( + res.attributes, + vec![ + attr("action".to_string(), "ibc_packet_timeout".to_string()), + attr( + "interchain_action".to_string(), + "query_proposal".to_string() + ), + attr("user".to_string(), user.to_string()), + ] + ); + + // Also ensure pending votes for this proposal was removed + let err = PENDING_VOTES.load(&deps.storage, proposal_id).unwrap_err(); + + assert_eq!( + err, + StdError::NotFound { + kind: "astroport_outpost::state::PendingVote".to_string() + } + ); + } + + // Test Cases: + // + // Expect Success + // - Kicking unlocked fails to reach the Hub + #[test] + fn kick_unlocked() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Construct the original message + let original_msg = to_binary(&Hub::KickUnlockedVoter { + voter: Addr::unchecked(user), + }) + .unwrap(); + // Authorised channels + let packet = IbcPacket::new( + original_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: format!("wasm.{}", HUB), + channel_id: "channel-7".to_string(), + }, + 4, + env.block.time.plus_seconds(ibc_timeout_seconds).into(), + ); + + // When the timeout occurs, we should see the correct attributes emitted + let timeout_packet = IbcPacketTimeoutMsg::new(packet, Addr::unchecked("relayer")); + let res = ibc_packet_timeout(deps.as_mut(), env, timeout_packet).unwrap(); + + // Should have 1 relock message + assert_eq!(res.messages.len(), 1); + + // Should have the correct attributes + assert_eq!( + res.attributes, + vec![ + attr("action".to_string(), "ibc_packet_timeout".to_string()), + attr( + "interchain_action".to_string(), + "kick_unlocked_voter".to_string() + ), + attr("user".to_string(), user.to_string()), + ] + ); + + // Confirm relock message is correct + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: VXASTRO_TOKEN.to_string(), + msg: to_binary( + &astroport_governance::voting_escrow_lite::ExecuteMsg::Relock { + user: user.to_string() + } + ) + .unwrap(), + funds: vec![], + } + .into(), + } + ); + } + + // Test Cases: + // + // Expect Success + // - Kicking unlocked fails to reach the Hub + #[test] + fn withdraw_funds() { + let (mut deps, env, info) = mock_all(OWNER); + + let user = "user"; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Construct the original message + let original_msg = to_binary(&Hub::WithdrawFunds { + user: Addr::unchecked(user), + }) + .unwrap(); + // Authorised channels + let packet = IbcPacket::new( + original_msg, + IbcEndpoint { + port_id: format!("wasm.{}", MOCK_CONTRACT_ADDR), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: format!("wasm.{}", HUB), + channel_id: "channel-7".to_string(), + }, + 4, + env.block.time.plus_seconds(ibc_timeout_seconds).into(), + ); + + // When the timeout occurs, we should see the correct attributes emitted + let timeout_packet = IbcPacketTimeoutMsg::new(packet, Addr::unchecked("relayer")); + let res = ibc_packet_timeout(deps.as_mut(), env, timeout_packet).unwrap(); + + // Should have no messages + assert_eq!(res.messages.len(), 0); + + // Should have the correct attributes + assert_eq!( + res.attributes, + vec![ + attr("action".to_string(), "ibc_packet_timeout".to_string()), + attr( + "interchain_action".to_string(), + "withdraw_funds".to_string() + ), + attr("user".to_string(), user.to_string()), + ] + ); + } +} diff --git a/contracts/outpost/src/ibc_mint.rs b/contracts/outpost/src/ibc_mint.rs new file mode 100644 index 00000000..3f21510c --- /dev/null +++ b/contracts/outpost/src/ibc_mint.rs @@ -0,0 +1,180 @@ +use astroport_governance::interchain::Response; +use cosmwasm_std::{to_binary, Deps, DepsMut, IbcReceiveResponse, Uint128, WasmMsg}; +use cw20::Cw20ExecuteMsg; + +use crate::{error::ContractError, state::CONFIG}; + +/// Mint new xASTRO based on the message received from the Hub, it cannot be +/// called directly. +/// +/// This is called in response to a staking message sent to the Hub +pub fn handle_ibc_xastro_mint( + deps: DepsMut, + recipient: String, + amount: Uint128, +) -> Result { + // Mint the new amount of xASTRO to the recipient that originally initiated + // the ASTRO staking + let msg = mint_xastro_msg(deps.as_ref(), recipient.clone(), amount)?; + + // If the minting succeeds, the ack will be sent back to the Hub + let ack_data = to_binary(&Response::new_success( + "mint_xastro".to_owned(), + recipient.to_string(), + ))?; + + let response = IbcReceiveResponse::new() + .add_message(msg) + .set_ack(ack_data) + .add_attribute("action", "mint_xastro") + .add_attribute("user", recipient) + .add_attribute("amount", amount); + + Ok(response) +} + +/// Create a new message to mint xASTRO to a specific address +pub fn mint_xastro_msg( + deps: Deps, + recipient: String, + amount: Uint128, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + let mint_msg = Cw20ExecuteMsg::Mint { recipient, amount }; + Ok(WasmMsg::Execute { + contract_addr: config.xastro_token_addr.to_string(), + msg: to_binary(&mint_msg)?, + funds: vec![], + }) +} + +#[cfg(test)] +mod tests { + use astroport_governance::interchain::Outpost; + use cosmwasm_std::{ + from_binary, testing::mock_info, Addr, IbcPacketReceiveMsg, ReplyOn, SubMsg, Uint128, + }; + + use super::*; + use crate::{ + contract::instantiate, + execute::execute, + ibc::ibc_packet_receive, + mock::{mock_all, mock_ibc_packet, setup_channel, HUB, OWNER, VXASTRO_TOKEN, XASTRO_TOKEN}, + }; + + // Test Cases: + // + // Expect Success + // - Mint the amount of xASTRO from the Hub to the recipient + // + // Expect Error + // - Sender is not the Hub + #[test] + fn ibc_mint_xastro() { + let (mut deps, env, info) = mock_all(OWNER); + + let receiver = "user"; + let amount = Uint128::from(1000u64); + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds: 10, + }, + ) + .unwrap(); + + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + let ibc_mint = to_binary(&Outpost::MintXAstro { + receiver: receiver.to_string(), + amount, + }) + .unwrap(); + + // Attempts to mint xASTRO from any other address than the Hub + let recv_packet = mock_ibc_packet("wasm.nothub", "channel-7", ibc_mint.clone()); + + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); + let ack: Response = from_binary(&res.acknowledgement).unwrap(); + match ack { + Response::Result { error, .. } => { + assert!(error == Some("Unauthorized".to_string())); + } + _ => panic!("Wrong response type"), + } + + // Attempts to mint xASTRO from any other channel than the Hub + let recv_packet = mock_ibc_packet(&format!("wasm.{}", HUB), "channel-7", ibc_mint.clone()); + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); + let ack: Response = from_binary(&res.acknowledgement).unwrap(); + match ack { + Response::Result { error, .. } => { + assert!(error == Some("Unauthorized".to_string())); + } + _ => panic!("Wrong response type"), + } + + // Mint from Hub contract and channel + let recv_packet = mock_ibc_packet(&format!("wasm.{}", HUB), "channel-3", ibc_mint); + let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); + let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); + + let ack: Response = from_binary(&res.acknowledgement).unwrap(); + match ack { + Response::Result { error, .. } => { + assert!(error.is_none()); + } + _ => panic!("Wrong response type"), + } + + // Should have exactly one message + assert_eq!(res.messages.len(), 1); + + // Verify that the mint message matches the expected message + let xastro_mint_msg = to_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: receiver.to_string(), + amount, + }) + .unwrap(); + + // We should see the mint xASTRO SubMessage + assert_eq!( + res.messages[0], + SubMsg { + id: 0, + gas_limit: None, + reply_on: ReplyOn::Never, + msg: WasmMsg::Execute { + contract_addr: XASTRO_TOKEN.to_string(), + msg: xastro_mint_msg, + funds: vec![], + } + .into(), + } + ); + } +} diff --git a/contracts/outpost/src/lib.rs b/contracts/outpost/src/lib.rs new file mode 100644 index 00000000..04c41938 --- /dev/null +++ b/contracts/outpost/src/lib.rs @@ -0,0 +1,11 @@ +pub mod contract; +pub mod error; +pub mod execute; +pub mod ibc; +pub mod ibc_failure; +pub mod ibc_mint; +pub mod query; +pub mod state; + +#[cfg(test)] +mod mock; diff --git a/contracts/outpost/src/mock.rs b/contracts/outpost/src/mock.rs new file mode 100644 index 00000000..b8bb7e9b --- /dev/null +++ b/contracts/outpost/src/mock.rs @@ -0,0 +1,218 @@ +#[cfg(test)] +use cosmwasm_std::from_binary; +use cosmwasm_std::{ + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + to_binary, Binary, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, + IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, ListChannelsResponse, MessageInfo, OwnedDeps, + Timestamp, Uint128, +}; + +use cosmwasm_std::testing::MOCK_CONTRACT_ADDR; +use cosmwasm_std::{ + from_slice, Empty, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, +}; + +use crate::ibc::{ibc_channel_connect, ibc_channel_open, IBC_APP_VERSION}; + +pub const CONTRACT_PORT: &str = "ibc:wasm1234567890abcdef"; +pub const CONNECTION_ID: &str = "connection-2"; +pub const OWNER: &str = "owner"; +pub const HUB: &str = "hub"; +pub const XASTRO_TOKEN: &str = "xastro"; +pub const VXASTRO_TOKEN: &str = "vxastro"; + +/// mock_dependencies is a drop-in replacement for cosmwasm_std::testing::mock_dependencies. +/// This uses the Astroport CustomQuerier. +#[cfg(test)] +pub fn mock_dependencies() -> OwnedDeps { + let custom_querier: WasmMockQuerier = + WasmMockQuerier::new(MockQuerier::new(&[(MOCK_CONTRACT_ADDR, &[])])); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: custom_querier, + custom_query_type: Default::default(), + } +} + +/// WasmMockQuerier will respond to requests from the custom querier, +/// providing responses to the contracts +pub struct WasmMockQuerier { + base: MockQuerier, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + // MockQuerier doesn't support Custom, so we ignore it completely + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }) + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { + if contract_addr == XASTRO_TOKEN { + match from_binary(msg).unwrap() { + astroport::xastro_outpost_token::QueryMsg::BalanceAt { + address: _, + timestamp: _, + } => { + let balance = astroport::token::BalanceResponse { + balance: Uint128::from(1000u128), + }; + SystemResult::Ok(to_binary(&balance).into()) + } + _ => { + panic!("DO NOT ENTER HERE") + } + } + } else { + match from_binary(msg).unwrap() { + astroport_governance::voting_escrow_lite::QueryMsg::UserDepositAt { + user:_, + timestamp:_, + } => { + let balance = astroport::token::BalanceResponse { + balance: Uint128::zero(), + }; + SystemResult::Ok(to_binary(&balance).into()) + } + astroport_governance::voting_escrow_lite::QueryMsg::UserEmissionsVotingPower { + user:_, + } => { + let balance = astroport_governance::voting_escrow_lite::VotingPowerResponse { + voting_power: Uint128::from(1000u128), + }; + SystemResult::Ok(to_binary(&balance).into()) + } + _ => { + panic!("DO NOT ENTER HERE") + } + } + } + } + QueryRequest::Ibc(IbcQuery::ListChannels { .. }) => { + let response = ListChannelsResponse { + channels: vec![ + IbcChannel::new( + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcOrder::Unordered, + "version", + "connection-1", + ), + IbcChannel::new( + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-15".to_string(), + }, + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcOrder::Unordered, + "version", + "connection-1", + ), + ], + }; + SystemResult::Ok(to_binary(&response).into()) + } + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { base } + } +} + +/// Mock the dependencies for unit tests +pub fn mock_all( + sender: &str, +) -> ( + OwnedDeps, + Env, + MessageInfo, +) { + let deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info(sender, &[]); + (deps, env, info) +} + +/// Mock an IBC channel +pub fn mock_channel( + our_port: &str, + our_channel_id: &str, + counter_port: &str, + counter_channel: &str, + ibc_order: IbcOrder, + ibc_version: &str, +) -> IbcChannel { + IbcChannel::new( + IbcEndpoint { + port_id: our_port.into(), + channel_id: our_channel_id.into(), + }, + IbcEndpoint { + port_id: counter_port.into(), + channel_id: counter_channel.into(), + }, + ibc_order, + ibc_version.to_string(), + CONNECTION_ID, + ) +} + +/// Set up a valid channel for use in tests +pub fn setup_channel(mut deps: DepsMut, env: Env) { + let channel = mock_channel( + "wasm.outpost", + "channel-3", + "wasm.hub", + "channel-7", + IbcOrder::Unordered, + IBC_APP_VERSION, + ); + let open_msg = IbcChannelOpenMsg::new_init(channel.clone()); + ibc_channel_open(deps.branch(), env.clone(), open_msg).unwrap(); + let connect_msg = IbcChannelConnectMsg::new_ack(channel, IBC_APP_VERSION); + ibc_channel_connect(deps, env, connect_msg).unwrap(); +} + +/// Construct a mock IBC packet +pub fn mock_ibc_packet(remote_port: &str, my_channel: &str, data: Binary) -> IbcPacket { + IbcPacket::new( + data, + IbcEndpoint { + port_id: remote_port.to_string(), + channel_id: "channel-3".to_string(), + }, + IbcEndpoint { + port_id: CONTRACT_PORT.to_string(), + channel_id: my_channel.to_string(), + }, + 3, + Timestamp::from_seconds(1665321069).into(), + ) +} diff --git a/contracts/outpost/src/query.rs b/contracts/outpost/src/query.rs new file mode 100644 index 00000000..a0192a73 --- /dev/null +++ b/contracts/outpost/src/query.rs @@ -0,0 +1,174 @@ +use cosmwasm_std::{entry_point, to_binary, Addr, Binary, Deps, Env, StdResult, Uint128}; + +use astroport::xastro_outpost_token::get_voting_power_at_time; +use astroport_governance::outpost::QueryMsg; +use astroport_governance::voting_escrow_lite::get_user_deposit_at_time; + +use crate::error::ContractError; +use crate::state::{CONFIG, VOTES}; + +/// Expose available contract queries. +/// +/// ## Queries +/// * **QueryMsg::Config {}** Returns the config of the Outpost +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), + QueryMsg::ProposalVoted { proposal_id, user } => { + let user_address = deps.api.addr_validate(&user)?; + to_binary(&VOTES.load(deps.storage, (&user_address, proposal_id))?) + } + } +} + +/// Get the user's voting power in total for xASTRO and vxASTRO +/// +/// xASTRO is taken at the time the proposal was added +/// vxASTRO is taken at the current time +pub fn get_user_voting_power( + deps: Deps, + user: Addr, + proposal_start: u64, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Get the user's xASTRO balance at the time the proposal was added + let voting_power = get_voting_power_at_time( + &deps.querier, + config.xastro_token_addr.clone(), + user.clone(), + proposal_start, + ) + .unwrap_or(Uint128::zero()); + + // Get the user's underlying xASTRO deposit at the time the proposal was added + let vxastro_balance = get_user_deposit_at_time( + &deps.querier, + config.vxastro_token_addr, + user, + proposal_start, + ) + .unwrap_or(Uint128::zero()); + + Ok(voting_power.checked_add(vxastro_balance)?) +} + +#[cfg(test)] +mod tests { + + use super::*; + + use cosmwasm_std::{testing::mock_info, StdError, Uint64}; + + use crate::{ + contract::instantiate, + execute::execute, + mock::{mock_all, setup_channel, HUB, OWNER, VXASTRO_TOKEN, XASTRO_TOKEN}, + query::query, + state::PROPOSALS_CACHE, + }; + use astroport_governance::{assembly::ProposalVoteOption, interchain::ProposalSnapshot}; + + // Test Cases: + // + // Expect Success + // - Can query for a vote already cast + // + // Expect Error + // - Must fail if the vote doesn't exist + // + #[test] + fn query_votes() { + let (mut deps, env, info) = mock_all(OWNER); + + let proposal_id = 1u64; + let user = "user"; + let ibc_timeout_seconds = 10u64; + + instantiate( + deps.as_mut(), + env.clone(), + info, + astroport_governance::outpost::InstantiateMsg { + owner: OWNER.to_string(), + xastro_token_addr: XASTRO_TOKEN.to_string(), + vxastro_token_addr: VXASTRO_TOKEN.to_string(), + hub_addr: HUB.to_string(), + ibc_timeout_seconds, + }, + ) + .unwrap(); + + // Set up valid Hub + setup_channel(deps.as_mut(), env.clone()); + + // Update config with new channel + execute( + deps.as_mut(), + env.clone(), + mock_info(OWNER, &[]), + astroport_governance::outpost::ExecuteMsg::UpdateConfig { + hub_addr: None, + hub_channel: Some("channel-3".to_string()), + ibc_timeout_seconds: None, + }, + ) + .unwrap(); + + // Add a proposal to the cache + PROPOSALS_CACHE + .save( + &mut deps.storage, + proposal_id, + &ProposalSnapshot { + id: Uint64::from(proposal_id), + start_time: 1689939457, + }, + ) + .unwrap(); + + // Cast a vote with a proposal in the cache + execute( + deps.as_mut(), + env.clone(), + mock_info(user, &[]), + astroport_governance::outpost::ExecuteMsg::CastAssemblyVote { + proposal_id, + vote: astroport_governance::assembly::ProposalVoteOption::For, + }, + ) + .unwrap(); + + // Check that we can query the vote that was cast + let vote_data = query( + deps.as_ref(), + env.clone(), + astroport_governance::outpost::QueryMsg::ProposalVoted { + proposal_id, + user: user.to_string(), + }, + ) + .unwrap(); + + assert_eq!(vote_data, to_binary(&ProposalVoteOption::For).unwrap()); + + // Check that we receive an error when querying a vote that doesn't exist + let err = query( + deps.as_ref(), + env, + astroport_governance::outpost::QueryMsg::ProposalVoted { + proposal_id, + user: "other_user".to_string(), + }, + ) + .unwrap_err(); + + assert_eq!( + err, + StdError::NotFound { + kind: "astroport_governance::assembly::ProposalVoteOption".to_string() + } + ); + } +} diff --git a/contracts/outpost/src/state.rs b/contracts/outpost/src/state.rs new file mode 100644 index 00000000..7161607f --- /dev/null +++ b/contracts/outpost/src/state.rs @@ -0,0 +1,34 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; +use cw_storage_plus::{Item, Map}; + +use astroport::common::OwnershipProposal; +use astroport_governance::{ + assembly::ProposalVoteOption, interchain::ProposalSnapshot, outpost::Config, +}; + +#[cw_serde] +pub struct PendingVote { + /// The proposal ID to vote on + pub proposal_id: u64, + /// The user voting + pub voter: Addr, + /// The choice in vote + pub vote_option: ProposalVoteOption, +} + +/// Store the contract config +pub const CONFIG: Item = Item::new("config"); + +/// Store a local cache of proposals to verify votes are allowed +pub const PROPOSALS_CACHE: Map = Map::new("proposals_cache"); + +/// Store the pending votes for a proposal while the information is being +/// retrieved from the Hub +pub const PENDING_VOTES: Map = Map::new("pending_votes"); + +/// Record of who has voted on which governance proposal +pub const VOTES: Map<(&Addr, u64), ProposalVoteOption> = Map::new("votes"); + +/// Contains a proposal to change contract ownership +pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 00b6a961..f4868141 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -19,5 +19,5 @@ cw20 = "0.15" cosmwasm-std = { version = "1.1", features = ["ibc3"] } cw-storage-plus = "0.15" cosmwasm-schema = "1.1" -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } thiserror = { version = "1.0" } diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index d2af5841..b37f7bf1 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -148,6 +148,14 @@ pub enum ExecuteMsg { proposal_id: u64, status: ProposalStatus, }, + /// Remove all votes cast from all Outposts in case of a vulnerability + /// in IBC or the contracts that allow manipulation of governance. + /// + /// This can only be called by the guardian. + RemoveOutpostVotes { + /// Proposal identifier + proposal_id: u64, + }, } /// Thie enum describes all the queries available in the contract. @@ -236,6 +244,9 @@ pub struct Config { pub proposal_required_threshold: Decimal, /// Whitelisted links pub whitelisted_links: Vec, + /// Guardian address that may cancel Outpost votes in case of a vulnerability + /// in IBC or the contracts that allow manipulation of governance + pub guardian_addr: Option, } impl Config { @@ -336,6 +347,8 @@ pub struct UpdateConfig { pub whitelist_remove: Option>, /// Links to add to whitelist pub whitelist_add: Option>, + /// Guardian address that may cancel Outpost votes in case of a vulnerability + pub guardian_addr: Option, } /// This structure stores data for a proposal. @@ -349,8 +362,12 @@ pub struct Proposal { pub status: ProposalStatus, /// `For` power of proposal pub for_power: Uint128, + /// `For` power of proposal cast from all Outposts + pub outpost_for_power: Uint128, /// `Against` power of proposal pub against_power: Uint128, + /// `Against` power of proposal cast from all Outposts + pub outpost_against_power: Uint128, /// `For` votes for the proposal pub for_voters: Vec, /// `Against` votes for the proposal diff --git a/packages/astroport-governance/src/hub.rs b/packages/astroport-governance/src/hub.rs new file mode 100644 index 00000000..95fd59eb --- /dev/null +++ b/packages/astroport-governance/src/hub.rs @@ -0,0 +1,145 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128, Uint64}; +use cw20::Cw20ReceiveMsg; + +/// Holds the parameters used for creating a Hub contract +#[cw_serde] +pub struct InstantiateMsg { + /// The contract owner + pub owner: String, + /// The address of the Assembly contract on the Hub + pub assembly_addr: String, + /// The address of the CW20-ICS20 contract on the Hub that supports + /// memo handling + pub cw20_ics20_addr: String, + /// The address of the xASTRO staking contract on the Hub + pub staking_addr: String, + /// The address of the generator controller contract on the Hub + pub generator_controller_addr: String, + /// The timeout in seconds for IBC packets + pub ibc_timeout_seconds: u64, +} + +/// The contract migration message +/// We currently take no arguments for migrations +#[cw_serde] +pub struct MigrateMsg {} + +/// Describes the execute messages available in the contract +#[cw_serde] +pub enum ExecuteMsg { + /// Receive a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// Update parameters in the Hub contract. Only the owner is allowed to + /// update the config + UpdateConfig { + /// The timeout in seconds for IBC packets + ibc_timeout_seconds: Option, + }, + /// Add a new Outpost to the Hub. Only allowed Outposts can send IBC messages + AddOutpost { + /// The remote contract address of the Outpost to add + outpost_addr: String, + /// The channel connecting us to the Outpost + outpost_channel: String, + /// The channel to use for CW20-ICS20 IBC transfers + cw20_ics20_channel: String, + }, + /// Remove an Outpost from the Hub + RemoveOutpost { + /// The remote contract address of the Outpost to remove + outpost_addr: String, + }, + /// Propose a new owner for the contract + ProposeNewOwner { new_owner: String, expires_in: u64 }, + /// Remove the ownership transfer proposal + DropOwnershipProposal {}, + /// Claim contract ownership + ClaimOwnership {}, +} + +/// Messages handled via CW20 transfers +#[cw_serde] +pub enum Cw20HookMsg { + /// Handles instructions received via an IBC transfer memo in the + /// CW20-ICS20 contract + OutpostMemo { + /// The channel the memo was received on + channel: String, + /// The original sender of the packet on the outpost + sender: String, + /// The original intended receiver of the packet on the Hub + receiver: String, + /// The memo containing the JSON to handle + memo: String, + }, + /// Handle failed CW20 IBC transfers + TransferFailure { + // The original sender where the funds should be returned to + receiver: String, + }, +} + +/// Describes the query messages available in the contract +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the config of the Hub + #[returns(Config)] + Config {}, + /// Returns the balance of funds held for a user + #[returns(HubBalance)] + UserFunds { user: Addr }, + /// Returns the list of the current Outposts on the Hub + #[returns(Vec)] + Outposts { + start_after: Option, + limit: Option, + }, + /// Returns the current balance of xASTRO minted via a specific Outpost channel + #[returns(HubBalance)] + ChannelBalanceAt { channel: String, timestamp: Uint64 }, + /// Returns the total balance of all xASTRO minted via Outposts + #[returns(HubBalance)] + TotalChannelBalancesAt { timestamp: Uint64 }, +} + +/// The config of the Hub +#[cw_serde] +pub struct Config { + /// The owner of the contract + pub owner: Addr, + /// The address of the Assembly contract on the Hub + pub assembly_addr: Addr, + /// The address of the CW20-ICS20 contract on the Hub that supports memo + /// handling + pub cw20_ics20_addr: Addr, + /// The address of the ASTRO token contract on the Hub + pub token_addr: Addr, + /// The address of the xASTRO token contract on the Hub + pub xtoken_addr: Addr, + /// The address of the staking contract on the Hub + pub staking_addr: Addr, + /// The address of the generator controller contract on the Hub + pub generator_controller_addr: Addr, + /// The timeout in seconds for IBC packets + pub ibc_timeout_seconds: u64, +} + +/// A response containing the Outpost address and channels +#[cw_serde] +pub struct OutpostConfig { + /// The address of the Outpost contract on another chain + pub address: String, + /// The channel connecting the Hub contract with that Outpost contract + pub channel: String, + /// The CS20-ICS20 channel ASTRO is transferred through + pub cw20_ics20_channel: String, +} + +/// A response containing the balance of a channel or user on the Hub +#[cw_serde] +pub struct HubBalance { + /// The balance of the user or channel + pub balance: Uint128, +} diff --git a/packages/astroport-governance/src/interchain.rs b/packages/astroport-governance/src/interchain.rs new file mode 100644 index 00000000..521f6f46 --- /dev/null +++ b/packages/astroport-governance/src/interchain.rs @@ -0,0 +1,164 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128, Uint64}; +use std::fmt::{Display, Formatter, Result}; + +use crate::assembly::ProposalVoteOption; + +// Minimum IBC timeout is 5 seconds +pub const MIN_IBC_TIMEOUT_SECONDS: u64 = 5; +// Maximum IBC timeout is 1 hour +pub const MAX_IBC_TIMEOUT_SECONDS: u64 = 60 * 60; + +/// Hub defines the messages that can be sent from an Outpost to the Hub +#[cw_serde] +#[non_exhaustive] +pub enum Hub { + /// Queries the Assembly for a proposal by ID via the Hub + QueryProposal { + /// The ID of the proposal to query + id: u64, + }, + /// Cast a vote on an Assembly proposal + CastAssemblyVote { + /// The ID of the proposal to vote on + proposal_id: u64, + /// The address of the voter + voter: Addr, + /// The vote choice + vote_option: ProposalVoteOption, + /// The voting power held by the voter, in this case xASTRO holdings + voting_power: Uint128, + }, + /// Cast a vote during an emissions voting period + CastEmissionsVote { + /// The address of the voter + voter: Addr, + /// The voting power held by the voter, in this case vxASTRO lite holdings + voting_power: Uint128, + /// The votes in the format (pool address, percent of voting power) + votes: Vec<(String, u16)>, + }, + /// Stake ASTRO tokens for xASTRO + Stake {}, + /// Unstake xASTRO tokens for ASTRO + Unstake { + // The user requesting the unstake and that should receive it + receiver: String, + /// The amount of xASTRO to unstake + amount: Uint128, + }, + /// Kick an unlocked voter's voting power from the Generator Controller lite + KickUnlockedVoter { + /// The address of the voter to kick + voter: Addr, + }, + /// Kick a blacklisted voter's voting power from the Generator Controller lite + KickBlacklistedVoter { + /// The address of the voter that has been blacklisted + voter: Addr, + }, + /// Withdraw stuck funds from the Hub in case of specific IBC failures + WithdrawFunds { + /// The address of the user to withdraw funds for + user: Addr, + }, +} + +/// Defines the messages that can be sent from the Hub to an Outpost +#[cw_serde] +pub enum Outpost { + /// Mint xASTRO tokens for the user + MintXAstro { receiver: String, amount: Uint128 }, +} + +/// Defines a minimal proposal that is cached on the Outpost +#[cw_serde] +pub struct ProposalSnapshot { + /// Unique proposal ID + pub id: Uint64, + /// Start time of proposal + pub start_time: u64, +} + +/// Defines the messages that can be returned in response to an IBC Hub or +/// Outpost message +#[cw_serde] +pub enum Response { + /// The response to a QueryProposal message that includes a minimal Proposal + QueryProposal(ProposalSnapshot), + /// A generic response to a Hub/Outpost message, mostly used for indicating success + /// or error handling + Result { + /// The action that was performed, None if no specific action was taken + action: Option, + /// The address of the user that took the action, None if the result + /// isn't specific to an address + address: Option, + /// The error message, if None, the action was successful + error: Option, + }, +} + +/// Utility functions for InterchainResponse to ease creation of responses +impl Response { + /// Create a new success response that sets address and action but leaves + /// error as None + pub fn new_success(action: String, address: String) -> Self { + Response::Result { + action: Some(action), + address: Some(address), + error: None, + } + } + /// Create a new error response that sets address and action to None + /// while adding the error message + pub fn new_error(error: String) -> Self { + Response::Result { + action: None, + address: None, + error: Some(error), + } + } +} + +/// Implements Display for Hub +impl Display for Hub { + fn fmt(&self, f: &mut Formatter) -> Result { + write!( + f, + "{}", + match self { + Hub::Stake { .. } => "stake", + Hub::CastAssemblyVote { .. } => "cast_assembly_vote", + Hub::CastEmissionsVote { .. } => "cast_emissions_vote", + Hub::QueryProposal { .. } => "query_proposal", + Hub::Unstake { .. } => "unstake", + Hub::KickUnlockedVoter { .. } => "kick_unlocked_voter", + Hub::KickBlacklistedVoter { .. } => "kick_blacklisted_voter", + Hub::WithdrawFunds { .. } => "withdraw_funds", + } + ) + } +} + +/// Implements Display for Outpost +impl Display for Outpost { + fn fmt(&self, f: &mut Formatter) -> Result { + write!( + f, + "{}", + match self { + Outpost::MintXAstro { .. } => "MintXAstro", + } + ) + } +} + +/// Get the address from an IBC port. If the port is prefixed with `wasm.`, +/// strip it out, if not, return the port as is. +pub fn get_contract_from_ibc_port(ibc_port: &str) -> &str { + match ibc_port.strip_prefix("wasm.") { + Some(suffix) => suffix, // prints: inj1234 + None => ibc_port, + } +} diff --git a/packages/astroport-governance/src/lib.rs b/packages/astroport-governance/src/lib.rs index 655f6f2c..b973465c 100644 --- a/packages/astroport-governance/src/lib.rs +++ b/packages/astroport-governance/src/lib.rs @@ -3,6 +3,8 @@ pub mod builder_unlock; pub mod escrow_fee_distributor; pub mod generator_controller; pub mod generator_controller_lite; +pub mod hub; +pub mod interchain; pub mod nft; pub mod outpost; pub mod utils; diff --git a/packages/astroport-governance/src/outpost.rs b/packages/astroport-governance/src/outpost.rs index 134d68f5..15cba93e 100644 --- a/packages/astroport-governance/src/outpost.rs +++ b/packages/astroport-governance/src/outpost.rs @@ -1,12 +1,107 @@ -use cosmwasm_schema::cw_serde; +use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Addr; +use cw20::Cw20ReceiveMsg; + +use crate::assembly::ProposalVoteOption; + +/// Holds the parameters used for creating an Outpost contract +#[cw_serde] +pub struct InstantiateMsg { + /// The contract owner + pub owner: String, + /// The address of the xASTRO token contract on the Outpost + pub xastro_token_addr: String, + /// The address of the vxASTRO lite contract on the Outpost + pub vxastro_token_addr: String, + /// The address of the Hub contract on the Hub chain + pub hub_addr: String, + /// The timeout in seconds for IBC packets + pub ibc_timeout_seconds: u64, +} + +/// The contract migration message +/// We currently take no arguments for migrations +#[cw_serde] +pub struct MigrateMsg {} /// Describes the execute messages available in the contract #[cw_serde] pub enum ExecuteMsg { + /// Receive a message of type [`Cw20ReceiveMsg`] + Receive(Cw20ReceiveMsg), + /// Update parameters in the Outpost contract. Only the owner is allowed to + /// update the config + UpdateConfig { + /// The new Hub address + hub_addr: Option, + /// The new Hub IBC channel + hub_channel: Option, + /// The timeout in seconds for IBC packets + ibc_timeout_seconds: Option, + }, + /// Cast a vote on an Assembly proposal from an Outpost + CastAssemblyVote { + /// The ID of the proposal to vote on + proposal_id: u64, + /// The vote choice + vote: ProposalVoteOption, + }, + /// Cast a vote during an emissions voting period + CastEmissionsVote { + /// The votes in the format (pool address, percent of voting power) + votes: Vec<(String, u16)>, + }, /// Kick an unlocked voter's voting power from the Generator Controller lite KickUnlocked { /// The address of the user to kick user: Addr, }, + /// Kick a blacklisted voter's voting power from the Generator Controller lite + KickBlacklisted { + /// The address of the user that has been blacklisted + user: Addr, + }, + /// Withdraw stuck funds from the Hub in case of specific IBC failures + WithdrawHubFunds {}, + /// Propose a new owner for the contract + ProposeNewOwner { new_owner: String, expires_in: u64 }, + /// Remove the ownership transfer proposal + DropOwnershipProposal {}, + /// Claim contract ownership + ClaimOwnership {}, +} + +/// Messages handled via CW20 transfers +#[cw_serde] +pub enum Cw20HookMsg { + /// Unstake xASTRO from the Hub and return the ASTRO to the sender + Unstake {}, +} + +/// Describes the query messages available in the contract +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the config of the Outpost + #[returns(Config)] + Config {}, + #[returns(ProposalVoteOption)] + ProposalVoted { proposal_id: u64, user: String }, +} + +/// The config of the Outpost +#[cw_serde] +pub struct Config { + /// The owner of the contract + pub owner: Addr, + /// The address of the Hub contract on the Hub chain + pub hub_addr: String, + /// The channel used to communicate with the Hub + pub hub_channel: Option, + /// The address of the xASTRO token contract on the Outpost + pub xastro_token_addr: Addr, + /// The address of the vxASTRO lite contract on the Outpost + pub vxastro_token_addr: Addr, + /// The timeout in seconds for IBC packets + pub ibc_timeout_seconds: u64, } diff --git a/packages/astroport-governance/src/utils.rs b/packages/astroport-governance/src/utils.rs index 586fd050..d4395d8d 100644 --- a/packages/astroport-governance/src/utils.rs +++ b/packages/astroport-governance/src/utils.rs @@ -1,10 +1,10 @@ -use std::convert::TryInto; - use cosmwasm_std::{ - Addr, Decimal, Fraction, IbcQuery, ListChannelsResponse, OverflowError, QuerierWrapper, - QueryRequest, StdError, StdResult, Uint128, Uint256, + to_binary, Addr, Decimal, Fraction, IbcQuery, ListChannelsResponse, OverflowError, + QuerierWrapper, QueryRequest, StdError, StdResult, Uint128, Uint256, Uint64, WasmQuery, }; +use crate::hub::HubBalance; + /// Seconds in one week. It is intended for period number calculation. pub const WEEK: u64 = 7 * 86400; // lock period is rounded down by week @@ -128,19 +128,19 @@ pub fn calc_voting_power( old_vp.saturating_sub(shift) } -/// Checks that controller supports given IBC-channel. +/// Checks that a contract supports a given IBC-channel. /// ## Params /// * **querier** is an object of type [`QuerierWrapper`]. /// -/// * **ibc_controller** is an ibc controller contract address. +/// * **contract** is the contract to check channel support on. /// /// * **given_channel** is an IBC channel id the function needs to check. -pub fn check_controller_supports_channel( +pub fn check_contract_supports_channel( querier: QuerierWrapper, - ibc_controller: &Addr, + contract: &Addr, given_channel: &String, ) -> Result<(), StdError> { - let port_id = Some(format!("wasm.{ibc_controller}")); + let port_id = Some(format!("wasm.{contract}")); let ListChannelsResponse { channels } = querier.query(&QueryRequest::Ibc(IbcQuery::ListChannels { port_id }))?; channels @@ -148,6 +148,27 @@ pub fn check_controller_supports_channel( .find(|channel| &channel.endpoint.channel_id == given_channel) .map(|_| ()) .ok_or_else(|| StdError::GenericErr { - msg: format!("IBC controller does not have channel {0}", given_channel), + msg: format!("The contract does not have channel {0}", given_channel), }) } + +/// Retrieves the total amount of voting power held by all Outposts at a given time +/// ## Params +/// * **querier** is an object of type [`QuerierWrapper`]. +/// +/// * **contract** is the Hub contract address +/// +/// * **timestamp** The unix timestamp at which to query the total voting power +pub fn get_total_outpost_voting_power_at( + querier: QuerierWrapper, + contract: &Addr, + timestamp: u64, +) -> Result { + let response: HubBalance = querier.query(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: contract.to_string(), + msg: to_binary(&crate::hub::QueryMsg::TotalChannelBalancesAt { + timestamp: Uint64::from(timestamp), + })?, + }))?; + Ok(response.balance) +} diff --git a/tarpaulin-report.html b/tarpaulin-report.html new file mode 100644 index 00000000..e86c899f --- /dev/null +++ b/tarpaulin-report.html @@ -0,0 +1,660 @@ + + + + + + + +

+ + + + + + \ No newline at end of file From cccde2c619bb4fc1e0a23c926b1595ab66f909c1 Mon Sep 17 00:00:00 2001 From: Donovan Solms Date: Fri, 18 Aug 2023 14:05:47 +0200 Subject: [PATCH 04/47] fix(governance-package): Bump version to 1.4 (#31) --- Cargo.lock | 30 ++++++++++++------------ packages/astroport-governance/Cargo.toml | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2853d801..1553cc74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,7 +24,7 @@ name = "astro-assembly" version = "1.6.0" dependencies = [ "anyhow", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-hub", "astroport-nft", "astroport-staking", @@ -104,7 +104,7 @@ dependencies = [ name = "astroport-escrow-fee-distributor" version = "1.0.2" dependencies = [ - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-tests", "astroport-token", "cosmwasm-schema", @@ -137,7 +137,7 @@ version = "2.3.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" dependencies = [ "astroport 2.9.0", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -162,7 +162,7 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "1.3.0" +version = "1.4.0" dependencies = [ "astroport 3.3.2", "cosmwasm-schema", @@ -178,7 +178,7 @@ version = "1.0.0" dependencies = [ "anyhow", "astroport 3.3.2", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.16.5", @@ -203,7 +203,7 @@ dependencies = [ name = "astroport-nft" version = "1.0.0" dependencies = [ - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -217,7 +217,7 @@ version = "0.1.0" dependencies = [ "anyhow", "astroport 3.3.2", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "base64", "cosmwasm-schema", "cosmwasm-std", @@ -273,7 +273,7 @@ dependencies = [ "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-pair", "astroport-staking", "astroport-token", @@ -297,7 +297,7 @@ dependencies = [ "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-pair", "astroport-staking", "astroport-token", @@ -433,7 +433,7 @@ name = "builder-unlock" version = "1.2.3" dependencies = [ "astroport 2.9.0", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-token", "cosmwasm-schema", "cosmwasm-std", @@ -1000,7 +1000,7 @@ dependencies = [ "anyhow", "astroport-factory", "astroport-generator", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-pair", "astroport-staking", "astroport-tests", @@ -1025,7 +1025,7 @@ dependencies = [ "anyhow", "astroport-factory", "astroport-generator", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-pair", "astroport-staking", "astroport-tests-lite", @@ -1655,7 +1655,7 @@ version = "1.3.0" dependencies = [ "anyhow", "astroport-escrow-fee-distributor", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-staking", "astroport-token", "cosmwasm-schema", @@ -1674,7 +1674,7 @@ name = "voting-escrow-delegation" version = "1.0.0" dependencies = [ "anyhow", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-nft", "astroport-tests", "cosmwasm-schema", @@ -1694,7 +1694,7 @@ name = "voting-escrow-lite" version = "1.0.0" dependencies = [ "anyhow", - "astroport-governance 1.3.0", + "astroport-governance 1.4.0", "astroport-staking", "astroport-token", "cosmwasm-schema", diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index f4868141..3a683b5c 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-governance" -version = "1.3.0" +version = "1.4.0" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" From 259fbc78d33f1b76e4213054babc95a1d9202f5c Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Wed, 23 Aug 2023 18:29:45 +0400 Subject: [PATCH 05/47] feat(crates publish): prepare cargo files; add publish script --- .github/workflows/code_coverage.yml | 2 +- .github/workflows/release_artifacts.yml | 6 +- packages/astroport-governance/Cargo.toml | 2 +- scripts/publish_crates.sh | 99 ++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 5 deletions(-) create mode 100755 scripts/publish_crates.sh diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index 659112ad..bf03e2f8 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -30,7 +30,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - uses: actions/cache@v3 + - uses: actions/cache@v2 with: path: | ~/.cargo/bin diff --git a/.github/workflows/release_artifacts.yml b/.github/workflows/release_artifacts.yml index b0803ab1..09824580 100644 --- a/.github/workflows/release_artifacts.yml +++ b/.github/workflows/release_artifacts.yml @@ -7,6 +7,8 @@ on: jobs: release-artifacts: runs-on: ubuntu-latest + permissions: + contents: write steps: - uses: actions/checkout@v3 - name: Build Artifacts @@ -17,6 +19,4 @@ jobs: uses: softprops/action-gh-release@v1 with: files: cosmwasm-artifacts.tar.gz - token: ${{ secrets.GH_TOKEN }} - env: - GITHUB_REPOSITORY: astroport-fi/astroport-governance \ No newline at end of file + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 3a683b5c..7332ccca 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -19,5 +19,5 @@ cw20 = "0.15" cosmwasm-std = { version = "1.1", features = ["ibc3"] } cw-storage-plus = "0.15" cosmwasm-schema = "1.1" -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "3" } thiserror = { version = "1.0" } diff --git a/scripts/publish_crates.sh b/scripts/publish_crates.sh new file mode 100755 index 00000000..a6b53c4e --- /dev/null +++ b/scripts/publish_crates.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash + +set -eu +set -o pipefail + +declare CONTRACTS +declare ROOT_DIR +declare FIRST_CRATES +declare SKIP_CRATES +declare DRY_FLAGS + +# NOTE: astroport-governance and astro-satellite-package should be published first + +if [ -z "${1:-}" ]; then + echo "Usage: $0 [optional: --publish]" + echo "If flag --publish is not set, only dry-run will be performed." + echo "NOTE: astroport-governance and astro-satellite-package should be published first." + exit 1 +fi + +DRY_FLAGS="--dry-run --allow-dirty" +if [ -z "${2:-}" ]; then + echo "Dry run mode" +else + echo "Publishing mode" + DRY_FLAGS="" +fi + +publish() { + local cargo_error temp_err_file ret_code=0 + local crate="$1" + + echo "Publishing $crate ..." + + set +e + + # Run 'cargo publish' and redirect stderr to a temporary file + temp_err_file="/tmp/cargo-publish-error-$crate.$$" + # shellcheck disable=SC2086 + cargo publish -p "$crate" --locked $DRY_FLAGS 2> >(tee "$temp_err_file") + ret_code=$? + cargo_error="$(<"$temp_err_file")" + rm "$temp_err_file" + + set -e + + # Sleep for 60 seconds if the crate was published successfully + [ $ret_code -eq 0 ] && [ -z "$DRY_FLAGS" ] && sleep 60 + + # Check if the error is related to the crate version already being uploaded + if [[ $cargo_error =~ "the remote server responded with an error: crate version" && $cargo_error =~ "is already uploaded" ]]; then + ret_code=0 + fi + + # Skip if the error is related to the crate version not being found in the registry and + # the script is running in dry-run mode + if [[ $cargo_error =~ "no matching package named" || $cargo_error =~ "failed to select a version for the requirement" ]] && \ + [[ -n "$DRY_FLAGS" ]]; then + ret_code=0 + fi + + # Return the original exit code from 'cargo publish' + return $ret_code +} + +ROOT_DIR="$(realpath "$1")" + +FIRST_CRATES="astroport-governance" +SKIP_CRATES="ALL" + +main() { + for contract in $FIRST_CRATES; do + publish "$contract" + done + + if [[ "$SKIP_CRATES" == "ALL" ]]; then + echo "Skipping publishing other crates" && return 0 + fi + + CONTRACTS="$(cargo metadata --no-deps --locked --manifest-path "$ROOT_DIR/Cargo.toml" --format-version 1 | + jq -r --arg contracts "$ROOT_DIR/contracts" \ + '.packages[] + | select(.manifest_path | startswith($contracts)) + | .name')" + + echo -e "Publishing crates:\n$CONTRACTS" + + for contract in $CONTRACTS; do + if [[ "$FIRST_CRATES $SKIP_CRATES" == *"$contract"* ]]; then + continue + fi + + publish "$contract" + done + + return 0 +} + +main && echo "ALL DONE" \ No newline at end of file From 19cf042770a787414a1966b321e05eb2548c7c16 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:46:17 +0300 Subject: [PATCH 06/47] feat(builder_unlock): add percent of tokens unlocked at the cliff --- .github/workflows/check_artifacts.yml | 116 +++ .github/workflows/check_artifacts_size.yml | 24 - .github/workflows/code_coverage.yml | 4 +- .github/workflows/tests_and_checks.yml | 14 +- Cargo.lock | 84 +-- contracts/assembly/Cargo.toml | 8 +- contracts/assembly/tests/integration.rs | 1 + contracts/builder_unlock/Cargo.toml | 6 +- contracts/builder_unlock/src/contract.rs | 54 +- contracts/builder_unlock/tests/integration.rs | 319 ++++++++- contracts/escrow_fee_distributor/Cargo.toml | 2 +- contracts/generator_controller/Cargo.toml | 12 +- .../generator_controller_lite/Cargo.toml | 12 +- contracts/voting_escrow/Cargo.toml | 4 +- contracts/voting_escrow_lite/Cargo.toml | 4 +- .../src/builder_unlock.rs | 7 +- packages/astroport-tests-lite/Cargo.toml | 14 +- packages/astroport-tests/Cargo.toml | 14 +- scripts/check_artifacts_size.sh | 14 +- tarpaulin-report.html | 660 ------------------ 20 files changed, 542 insertions(+), 831 deletions(-) create mode 100644 .github/workflows/check_artifacts.yml delete mode 100644 .github/workflows/check_artifacts_size.yml delete mode 100644 tarpaulin-report.html diff --git a/.github/workflows/check_artifacts.yml b/.github/workflows/check_artifacts.yml new file mode 100644 index 00000000..53bcb987 --- /dev/null +++ b/.github/workflows/check_artifacts.yml @@ -0,0 +1,116 @@ +name: Compiled binaries checks + +on: + pull_request: + push: + branches: + - main + +env: + CARGO_TERM_COLOR: always + CARGO_NET_GIT_FETCH_WITH_CLI: true + +jobs: + fetch_deps: + name: Fetch cargo dependencies + runs-on: ubuntu-latest + + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.11.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v3 + - uses: webfactory/ssh-agent@v0.7.0 + with: + ssh-private-key: | + ${{ secrets.CORE_PRIVATE_KEY }} + + - uses: actions/cache@v3 + if: always() + with: + path: | + ~/.cargo/bin + ~/.cargo/git/checkouts + ~/.cargo/git/db + ~/.cargo/registry/cache + ~/.cargo/registry/index + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + + - run: | + git config url."ssh://git@github.com/astroport-fi/hidden_astroport_core.git".insteadOf "https://github.com/astroport-fi/hidden_astroport_core" + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: 1.68.0 + override: true + + - name: Fetch cargo deps + uses: actions-rs/cargo@v1 + with: + command: fetch + args: --locked + + check-artifacts-size: + runs-on: ubuntu-latest + name: Check Artifacts Size + needs: fetch_deps + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin + ~/.cargo/git/checkouts + ~/.cargo/git/db + ~/.cargo/registry/cache + ~/.cargo/registry/index + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + # docker can't pull private sources, so we fail if cache is missing + fail-on-cache-miss: true + + - name: Build Artifacts + run: | + docker run \ + -v "$GITHUB_WORKSPACE":/code \ + -v ~/.cargo/registry:/usr/local/cargo/registry \ + -v ~/.cargo/git:/usr/local/cargo/git \ + cosmwasm/workspace-optimizer:0.12.13 + + - name: Save artifacts cache + uses: actions/cache/save@v3 + with: + path: artifacts + key: ${{ runner.os }}-artifacts-${{ hashFiles('**/Cargo.lock') }} + + - name: Check Artifacts Size + run: | + $GITHUB_WORKSPACE/scripts/check_artifacts_size.sh + + cosmwasm-check: + runs-on: ubuntu-latest + name: Cosmwasm check + needs: check-artifacts-size + steps: + # We need this only to get Cargo.lock + - name: Checkout sources + uses: actions/checkout@v3 + - name: Restore cached artifacts + uses: actions/cache/restore@v3 + with: + path: artifacts + key: ${{ runner.os }}-artifacts-${{ hashFiles('**/Cargo.lock') }} + fail-on-cache-miss: true + - name: Install cosmwasm-check + # Uses --debug for compilation speed + run: cargo install --debug --version 1.4.0 cosmwasm-check + - name: Cosmwasm check + run: | + cosmwasm-check $GITHUB_WORKSPACE/artifacts/*.wasm --available-capabilities staking,iterator,stargate diff --git a/.github/workflows/check_artifacts_size.yml b/.github/workflows/check_artifacts_size.yml deleted file mode 100644 index c7ecb6ad..00000000 --- a/.github/workflows/check_artifacts_size.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Check Artifacts Size -on: - pull_request: - -jobs: - check-artifacts-size: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: | - ~/.cargo/bin - ~/.cargo/git/checkouts - ~/.cargo/git/db - ~/.cargo/registry/cache - ~/.cargo/registry/index - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - - - name: Check Artifacts Size - run: | - $GITHUB_WORKSPACE/scripts/check_artifacts_size.sh \ No newline at end of file diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index bf03e2f8..9fea23e5 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -30,7 +30,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v2 - - uses: actions/cache@v2 + - uses: actions/cache@v3 with: path: | ~/.cargo/bin @@ -56,7 +56,7 @@ jobs: version: '0.22.0' args: '--timeout 300' - - name: Upload coverage reports to Codecov + - name: Upload to codecov.io if: github.ref == 'refs/heads/main' uses: codecov/codecov-action@v3 with: diff --git a/.github/workflows/tests_and_checks.yml b/.github/workflows/tests_and_checks.yml index 4005a142..a3249686 100644 --- a/.github/workflows/tests_and_checks.yml +++ b/.github/workflows/tests_and_checks.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Cancel Previous Runs - uses: styfle/cancel-workflow-action@0.9.1 + uses: styfle/cancel-workflow-action@0.11.0 with: access_token: ${{ github.token }} @@ -25,11 +25,11 @@ jobs: with: ssh-private-key: | ${{ secrets.CORE_PRIVATE_KEY }} - ${{ secrets.IBC_PRIVATE_KEY }} - - name: Checkout sources - uses: actions/checkout@v2 - - uses: actions/cache@v2 + - uses: actions/checkout@v3 + + - uses: actions/cache@v3 + if: always() with: path: | ~/.cargo/bin @@ -39,9 +39,11 @@ jobs: ~/.cargo/registry/index target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - run: | git config url."ssh://git@github.com/astroport-fi/hidden_astroport_core.git".insteadOf "https://github.com/astroport-fi/hidden_astroport_core" - # git config url."ssh://git@github.com/astroport-fi/astroport_ibc.git".insteadOf "https://github.com/astroport-fi/astroport_ibc" - name: Install stable toolchain uses: actions-rs/toolchain@v1 diff --git a/Cargo.lock b/Cargo.lock index 1553cc74..95f8b617 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "astro-assembly" -version = "1.6.0" +version = "1.6.1" dependencies = [ "anyhow", "astroport-governance 1.4.0", @@ -44,20 +44,6 @@ dependencies = [ "voting-escrow-lite", ] -[[package]] -name = "astroport" -version = "2.9.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw20 0.15.1", - "itertools", - "uint", -] - [[package]] name = "astroport" version = "2.10.0" @@ -75,8 +61,8 @@ dependencies = [ [[package]] name = "astroport" -version = "3.3.2" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#d881a4ec782a1cef1a0c262880e9d6868b015e15" +version = "3.6.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -92,7 +78,7 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#d881a4ec782a1cef1a0c262880e9d6868b015e15" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -118,13 +104,14 @@ dependencies = [ [[package]] name = "astroport-factory" -version = "1.5.1" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" +version = "1.6.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 2.9.0", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", "cw2 0.15.1", "itertools", "protobuf", @@ -133,14 +120,15 @@ dependencies = [ [[package]] name = "astroport-generator" -version = "2.3.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" +version = "2.3.2" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 2.9.0", + "astroport 3.6.0", "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", "cw1-whitelist", "cw2 0.15.1", "cw20 0.15.1", @@ -151,7 +139,7 @@ dependencies = [ [[package]] name = "astroport-governance" version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance?branch=feat/merge_hidden_2023_05_22#1e865abe55093d249b69b538e2d54472b643d6c7" +source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" dependencies = [ "astroport 2.10.0", "cosmwasm-schema", @@ -164,7 +152,7 @@ dependencies = [ name = "astroport-governance" version = "1.4.0" dependencies = [ - "astroport 3.3.2", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -177,7 +165,7 @@ name = "astroport-hub" version = "1.0.0" dependencies = [ "anyhow", - "astroport 3.3.2", + "astroport 3.6.0", "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", @@ -193,8 +181,8 @@ dependencies = [ [[package]] name = "astroport-ibc" -version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#f9f4def037d117275de31fffef36ddda388baf48" +version = "1.2.0" +source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#ffb48ebfd7dbbc010cf86c9b02bad236c456fca0" dependencies = [ "cosmwasm-schema", ] @@ -216,7 +204,7 @@ name = "astroport-outpost" version = "0.1.0" dependencies = [ "anyhow", - "astroport 3.3.2", + "astroport 3.6.0", "astroport-governance 1.4.0", "base64", "cosmwasm-schema", @@ -235,13 +223,14 @@ dependencies = [ [[package]] name = "astroport-pair" -version = "1.3.1" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" +version = "1.5.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 2.9.0", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", "cw2 0.15.1", "cw20 0.15.1", "integer-sqrt", @@ -252,12 +241,13 @@ dependencies = [ [[package]] name = "astroport-staking" version = "1.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 2.9.0", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", "cw2 0.15.1", "cw20 0.15.1", "protobuf", @@ -269,7 +259,7 @@ name = "astroport-tests" version = "1.0.0" dependencies = [ "anyhow", - "astroport 2.9.0", + "astroport 3.6.0", "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", @@ -293,7 +283,7 @@ version = "1.0.0" dependencies = [ "anyhow", "astro-assembly", - "astroport 2.9.0", + "astroport 3.6.0", "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", @@ -314,9 +304,9 @@ dependencies = [ [[package]] name = "astroport-token" version = "1.1.1" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 2.9.0", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -328,9 +318,9 @@ dependencies = [ [[package]] name = "astroport-whitelist" version = "1.0.1" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 2.9.0", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist", @@ -340,10 +330,10 @@ dependencies = [ [[package]] name = "astroport-xastro-token" -version = "1.0.2" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/merge_public_20230519#f3d99b17d6c6ba017398ded63d8cba74b4a3989f" +version = "1.1.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 2.9.0", + "astroport 3.6.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -430,9 +420,9 @@ checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" [[package]] name = "builder-unlock" -version = "1.2.3" +version = "2.0.0" dependencies = [ - "astroport 2.9.0", + "astroport 3.6.0", "astroport-governance 1.4.0", "astroport-token", "cosmwasm-schema", @@ -1102,7 +1092,7 @@ dependencies = [ [[package]] name = "ibc-controller-package" version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#f9f4def037d117275de31fffef36ddda388baf48" +source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#ffb48ebfd7dbbc010cf86c9b02bad236c456fca0" dependencies = [ "astroport-governance 1.2.0", "astroport-ibc", diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 9da87756..716e800c 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astro-assembly" -version = "1.6.0" +version = "1.6.1" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" @@ -26,13 +26,13 @@ cosmwasm-schema = "1.1" [dev-dependencies] cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-xastro-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-xastro-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } astroport-hub = { path = "../hub" } voting-escrow = { path = "../voting_escrow" } voting-escrow-lite = { path = "../voting_escrow_lite" } voting-escrow-delegation = { path = "../voting_escrow_delegation" } astroport-nft = { path = "../nft" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } builder-unlock = { path = "../builder_unlock" } anyhow = "1" diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 51e395c8..bcf5c039 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -604,6 +604,7 @@ fn test_successful_proposal() { start_time: 12_345, cliff: 5, duration: 500, + percent_at_cliff: None, }, proposed_receiver: None, }; diff --git a/contracts/builder_unlock/Cargo.toml b/contracts/builder_unlock/Cargo.toml index 2cf175d7..96337916 100644 --- a/contracts/builder_unlock/Cargo.toml +++ b/contracts/builder_unlock/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "builder-unlock" -version = "1.2.3" +version = "2.0.0" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" @@ -20,10 +20,10 @@ cw20 = "0.15" cosmwasm-std = "1.1" cw-storage-plus = "0.15" astroport-governance = { path = "../../packages/astroport-governance" } -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } thiserror = { version = "1.0" } cosmwasm-schema = "1.1" [dev-dependencies] cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } diff --git a/contracts/builder_unlock/src/contract.rs b/contracts/builder_unlock/src/contract.rs index 206f1e51..ec603d24 100644 --- a/contracts/builder_unlock/src/contract.rs +++ b/contracts/builder_unlock/src/contract.rs @@ -1,26 +1,23 @@ -use crate::astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; - use cosmwasm_std::{ attr, from_binary, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128, WasmMsg, }; -use cw2::{get_contract_version, set_contract_version}; +use cw2::set_contract_version; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; use cw_storage_plus::Bound; -use crate::astroport::asset::addr_opt_validate; -use crate::contract::helpers::{compute_unlocked_amount, compute_withdraw_amount}; -use crate::migration::MigrateMsg; use astroport_governance::builder_unlock::msg::{ AllocationResponse, ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg, SimulateWithdrawResponse, StateResponse, }; use astroport_governance::builder_unlock::{AllocationParams, AllocationStatus, Config, Schedule}; - use astroport_governance::{DEFAULT_LIMIT, MAX_LIMIT}; +use crate::astroport::asset::addr_opt_validate; +use crate::astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use crate::contract::helpers::{compute_unlocked_amount, compute_withdraw_amount}; use crate::state::{CONFIG, OWNERSHIP_PROPOSAL, PARAMS, STATE, STATUS}; // Version and name used for contract migration. @@ -743,29 +740,6 @@ fn query_simulate_withdraw( )) } -/// Manages contract migration -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult { - let contract_version = get_contract_version(deps.storage)?; - - match contract_version.contract.as_ref() { - "builder-unlock" => match contract_version.version.as_ref() { - "1.2.0" => {} - "1.2.2" => {} - _ => return Err(StdError::generic_err("Contract can't be migrated!")), - }, - _ => return Err(StdError::generic_err("Contract can't be migrated!")), - }; - - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - Ok(Response::new() - .add_attribute("previous_contract_name", &contract_version.contract) - .add_attribute("previous_contract_version", &contract_version.version) - .add_attribute("new_contract_name", CONTRACT_NAME) - .add_attribute("new_contract_version", CONTRACT_VERSION)) -} - //---------------------------------------------------------------------------------------- // Helper Functions //---------------------------------------------------------------------------------------- @@ -786,11 +760,21 @@ mod helpers { // Tokens haven't begun unlocking if timestamp < schedule.start_time + schedule.cliff { unlock_checkpoint - } - // Tokens unlock linearly between start time and end time - else if (timestamp < schedule.start_time + schedule.duration) && schedule.duration != 0 { - let unlocked_amount = - amount.multiply_ratio(timestamp - schedule.start_time, schedule.duration); + } else if (timestamp < schedule.start_time + schedule.duration) && schedule.duration != 0 { + // If percent_at_cliff is set, then this amount should be unlocked at cliff. + // The rest of tokens are vested linearly between cliff and end_time + let unlocked_amount = if let Some(percent_at_cliff) = schedule.percent_at_cliff { + let amount_at_cliff = amount * percent_at_cliff; + + amount_at_cliff + + amount.saturating_sub(amount_at_cliff).multiply_ratio( + timestamp - schedule.start_time - schedule.cliff, + schedule.duration - schedule.cliff, + ) + } else { + // Tokens unlock linearly between start time and end time + amount.multiply_ratio(timestamp - schedule.start_time, schedule.duration) + }; if unlocked_amount > unlock_checkpoint { unlocked_amount diff --git a/contracts/builder_unlock/tests/integration.rs b/contracts/builder_unlock/tests/integration.rs index 7e949907..3cd71405 100644 --- a/contracts/builder_unlock/tests/integration.rs +++ b/contracts/builder_unlock/tests/integration.rs @@ -1,13 +1,14 @@ use astroport::token::InstantiateMsg as TokenInstantiateMsg; -use astroport_governance::builder_unlock::{AllocationParams, Schedule}; +use cosmwasm_std::{attr, to_binary, Addr, Decimal, StdResult, Timestamp, Uint128}; +use cw20::BalanceResponse; +use cw_multi_test::{App, BasicApp, ContractWrapper, Executor}; +use std::time::SystemTime; use astroport_governance::builder_unlock::msg::{ AllocationResponse, ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg, SimulateWithdrawResponse, StateResponse, }; -use cosmwasm_std::{attr, to_binary, Addr, StdResult, Timestamp, Uint128}; -use cw20::BalanceResponse; -use cw_multi_test::{App, BasicApp, ContractWrapper, Executor}; +use astroport_governance::builder_unlock::{AllocationParams, Schedule}; const OWNER: &str = "owner"; @@ -221,6 +222,7 @@ fn test_create_allocations() { start_time: 1642402274u64, cliff: 0u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -233,6 +235,7 @@ fn test_create_allocations() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -245,6 +248,7 @@ fn test_create_allocations() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -411,7 +415,8 @@ fn test_create_allocations() { Schedule { start_time: 1642402274u64, cliff: 0u64, - duration: 31536000u64 + duration: 31536000u64, + percent_at_cliff: None, } ); @@ -432,7 +437,8 @@ fn test_create_allocations() { Schedule { start_time: 1642402274u64, cliff: 7776000u64, - duration: 31536000u64 + duration: 31536000u64, + percent_at_cliff: None, } ); @@ -453,7 +459,8 @@ fn test_create_allocations() { Schedule { start_time: 1642402274u64, cliff: 7776000u64, - duration: 31536000u64 + duration: 31536000u64, + percent_at_cliff: None, } ); @@ -501,6 +508,7 @@ fn test_withdraw() { start_time: 1642402274u64, cliff: 0u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -513,6 +521,7 @@ fn test_withdraw() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -525,6 +534,7 @@ fn test_withdraw() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -850,6 +860,7 @@ fn test_propose_new_receiver() { start_time: 1642402274u64, cliff: 0u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -862,6 +873,7 @@ fn test_propose_new_receiver() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -874,6 +886,7 @@ fn test_propose_new_receiver() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -991,6 +1004,7 @@ fn test_drop_new_receiver() { start_time: 1642402274u64, cliff: 0u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1003,6 +1017,7 @@ fn test_drop_new_receiver() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1015,6 +1030,7 @@ fn test_drop_new_receiver() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1132,6 +1148,7 @@ fn test_claim_receiver() { start_time: 1642402274u64, cliff: 0u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1144,6 +1161,7 @@ fn test_claim_receiver() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1156,6 +1174,7 @@ fn test_claim_receiver() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1269,6 +1288,7 @@ fn test_claim_receiver() { start_time: 0u64, cliff: 0u64, duration: 0u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1293,6 +1313,7 @@ fn test_claim_receiver() { start_time: alloc_resp_before.params.unlock_schedule.start_time, cliff: alloc_resp_before.params.unlock_schedule.cliff, duration: alloc_resp_before.params.unlock_schedule.duration, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1355,6 +1376,7 @@ fn test_increase_and_decrease_allocation() { start_time: 1_571_797_419u64, cliff: 300u64, duration: 1_534_700u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1593,6 +1615,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 0u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1605,6 +1628,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1617,6 +1641,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1663,6 +1688,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 0u64, duration: 31536000u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1678,6 +1704,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1693,6 +1720,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1709,6 +1737,7 @@ fn test_updates_schedules() { start_time: 123u64, cliff: 123u64, duration: 123u64, + percent_at_cliff: None, }, )], }, @@ -1732,6 +1761,7 @@ fn test_updates_schedules() { start_time: 123u64, cliff: 123u64, duration: 123u64, + percent_at_cliff: None, }, ), ( @@ -1740,6 +1770,7 @@ fn test_updates_schedules() { start_time: 123u64, cliff: 123u64, duration: 123u64, + percent_at_cliff: None, }, ), ], @@ -1763,6 +1794,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, ), ( @@ -1771,6 +1803,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, ), ], @@ -1790,6 +1823,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1805,6 +1839,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1830,6 +1865,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1842,6 +1878,7 @@ fn test_updates_schedules() { start_time: 1642402274, cliff: 0, duration: 31536000, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1854,6 +1891,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1880,6 +1918,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1905,3 +1944,269 @@ fn check_allocation( Ok(()) } + +fn query_cw20_bal(app: &mut App, cw20_addr: &Addr, address: &Addr) -> u128 { + app.wrap() + .query_wasm_smart::( + cw20_addr, + &cw20::Cw20QueryMsg::Balance { + address: address.to_string(), + }, + ) + .unwrap() + .balance + .u128() +} + +#[test] +fn test_create_allocations_with_custom_cliff() { + let mut app = mock_app(); + let (unlock_instance, astro_instance, _) = init_contracts(&mut app); + let total_astro = Uint128::new(1_000_000_000000); + let owner = Addr::unchecked(OWNER); + + mint_some_astro( + &mut app, + owner.clone(), + astro_instance.clone(), + total_astro, + owner.to_string(), + ); + + let now_ts = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + app.update_block(|block| block.time = Timestamp::from_seconds(now_ts)); + let day = 86400u64; + + let investor1 = Addr::unchecked("investor1"); + let investor2 = Addr::unchecked("investor2"); + let investor3 = Addr::unchecked("investor3"); + let mut allocations = vec![]; + allocations.push(( + investor1.to_string(), + AllocationParams { + amount: Uint128::from(500_000_000000u64), + unlock_schedule: Schedule { + start_time: now_ts, + cliff: day * 365, // 1 year + duration: 3 * day * 365, // 3 years + percent_at_cliff: None, + }, + proposed_receiver: None, + }, + )); + allocations.push(( + investor2.to_string(), + AllocationParams { + amount: Uint128::from(100_000_000000u64), + unlock_schedule: Schedule { + start_time: now_ts - day * 30, // 1 month ago + cliff: 6 * day * 30, // 6 months + duration: 3 * day * 365, // 3 years + percent_at_cliff: Some(Decimal::from_ratio(1u8, 6u8)), // one sixth + }, + proposed_receiver: None, + }, + )); + allocations.push(( + investor3.to_string(), + AllocationParams { + amount: Uint128::from(400_000_000000u64), + unlock_schedule: Schedule { + start_time: now_ts - day * 365, // 1 year ago + cliff: 6 * day * 30, // 6 months + duration: 3 * day * 365, // 3 years + percent_at_cliff: Some(Decimal::percent(20)), // 20% at cliff + }, + proposed_receiver: None, + }, + )); + + // Create allocations + app.execute_contract( + owner.clone(), + astro_instance.clone(), + &cw20::Cw20ExecuteMsg::Send { + contract: unlock_instance.to_string(), + amount: total_astro, + msg: to_binary(&ReceiveMsg::CreateAllocations { + allocations: allocations.clone(), + }) + .unwrap(), + }, + &[], + ) + .unwrap(); + + // Investor1's allocation just has been created + let err = app + .execute_contract( + investor1.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: No unlocked ASTRO to be withdrawn" + ); + + // Investor2 needs to wait 5 months more + let err = app + .execute_contract( + investor2.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: No unlocked ASTRO to be withdrawn" + ); + + // Investor3 has 20% of his allocation unlocked + linearly unlocked astro for the last 6 months + app.execute_contract( + investor3.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor3); + let amount_at_cliff = allocations[2].1.amount.u128() / 5; + let amount_linearly_vested = 64699_453551; + assert_eq!(balance, amount_at_cliff + amount_linearly_vested); + + // shift by 5 months + app.update_block(|block| block.time = block.time.plus_seconds(5 * 30 * day)); + + // Investor1 is still waiting + let err = app + .execute_contract( + investor1.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: No unlocked ASTRO to be withdrawn" + ); + + // Investor2 receives his one sixth of the allocation + app.execute_contract( + investor2.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor2); + assert_eq!(balance, 16666_666666); + + // Investor3 continues to receive linearly unlocked astro + app.execute_contract( + investor3.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor3); + assert_eq!(balance, 197158_469945); + + // shift by 7 months + app.update_block(|block| block.time = block.time.plus_seconds(215 * day)); + + // Investor1 receives his allocation (linearly unlocked from start point) + app.execute_contract( + investor1.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor1); + assert_eq!(balance, 166666_666666); + + // Investor2 continues to receive linearly unlocked astro + app.execute_contract( + investor2.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor2); + assert_eq!(balance, 36247_723132); + + // Investor3 continues to receive linearly unlocked astro + app.execute_contract( + investor3.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor3); + assert_eq!(balance, 272349_726775); + + // shift by 2 years + app.update_block(|block| block.time = block.time.plus_seconds(2 * 365 * day)); + + // Investor1 receives whole allocation + app.execute_contract( + investor1.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor1); + assert_eq!(balance, 500000_000000); + + // Investor2 receives whole allocation + app.execute_contract( + investor2.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor2); + assert_eq!(balance, 100000_000000); + + // Investor3 receives whole allocation + app.execute_contract( + investor3.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_cw20_bal(&mut app, &astro_instance, &investor3); + assert_eq!(balance, 400000_000000); + + app.update_block(|block| block.time = block.time.plus_seconds(day)); + + // No more ASTRO left for withdrawals + for investor in &[investor1, investor2, investor3] { + let err = app + .execute_contract( + investor.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: No unlocked ASTRO to be withdrawn" + ); + } +} diff --git a/contracts/escrow_fee_distributor/Cargo.toml b/contracts/escrow_fee_distributor/Cargo.toml index a0d74d70..389add17 100644 --- a/contracts/escrow_fee_distributor/Cargo.toml +++ b/contracts/escrow_fee_distributor/Cargo.toml @@ -25,5 +25,5 @@ cosmwasm-schema = "1.1" [dev-dependencies] cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } astroport-tests = { path = "../../packages/astroport-tests" } diff --git a/contracts/generator_controller/Cargo.toml b/contracts/generator_controller/Cargo.toml index 618538cc..caac7412 100644 --- a/contracts/generator_controller/Cargo.toml +++ b/contracts/generator_controller/Cargo.toml @@ -36,12 +36,12 @@ cosmwasm-schema = "1.1" cw-multi-test = "0.15" astroport-tests = { path = "../../packages/astroport-tests" } -astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core" } cw20 = "0.15" voting-escrow = { path = "../voting_escrow" } anyhow = "1" diff --git a/contracts/generator_controller_lite/Cargo.toml b/contracts/generator_controller_lite/Cargo.toml index e95f2e33..0fec7417 100644 --- a/contracts/generator_controller_lite/Cargo.toml +++ b/contracts/generator_controller_lite/Cargo.toml @@ -36,12 +36,12 @@ cosmwasm-schema = "1.1" cw-multi-test = "0.16" astroport-tests-lite = { path = "../../packages/astroport-tests-lite" } -astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core" } cw20 = "0.15" voting-escrow = { path = "../voting_escrow" } anyhow = "1" diff --git a/contracts/voting_escrow/Cargo.toml b/contracts/voting_escrow/Cargo.toml index 9f0db9fd..44795e97 100644 --- a/contracts/voting_escrow/Cargo.toml +++ b/contracts/voting_escrow/Cargo.toml @@ -34,8 +34,8 @@ cosmwasm-schema = "1.1" [dev-dependencies] cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } astroport-escrow-fee-distributor = { path = "../escrow_fee_distributor" } anyhow = "1" proptest = "1.0" diff --git a/contracts/voting_escrow_lite/Cargo.toml b/contracts/voting_escrow_lite/Cargo.toml index 92f94d28..d454d612 100644 --- a/contracts/voting_escrow_lite/Cargo.toml +++ b/contracts/voting_escrow_lite/Cargo.toml @@ -34,8 +34,8 @@ cosmwasm-schema = "1.1" [dev-dependencies] cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } astroport-generator-controller = { path = "../../contracts/generator_controller_lite", package = "generator-controller-lite" } anyhow = "1" proptest = "1.0" diff --git a/packages/astroport-governance/src/builder_unlock.rs b/packages/astroport-governance/src/builder_unlock.rs index 87ed89df..29d42d9f 100644 --- a/packages/astroport-governance/src/builder_unlock.rs +++ b/packages/astroport-governance/src/builder_unlock.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, StdError, Uint128}; +use cosmwasm_std::{Addr, Decimal, StdError, Uint128}; /// This structure stores general parameters for the builder unlock contract. #[cw_serde] @@ -34,6 +34,8 @@ pub struct Schedule { pub cliff: u64, /// Time after the cliff during which the remaining tokens linearly unlock pub duration: u64, + /// Percentage of tokens unlocked at the cliff + pub percent_at_cliff: Option, } /// This structure stores the parameters used to describe an ASTRO allocation. @@ -123,11 +125,12 @@ impl AllocationStatus { } pub mod msg { - use crate::builder_unlock::Schedule; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint128; use cw20::Cw20ReceiveMsg; + use crate::builder_unlock::Schedule; + use super::{AllocationParams, AllocationStatus, Config}; /// This structure holds the initial parameters used to instantiate the contract. diff --git a/packages/astroport-tests-lite/Cargo.toml b/packages/astroport-tests-lite/Cargo.toml index c6ed825d..0f62c867 100644 --- a/packages/astroport-tests-lite/Cargo.toml +++ b/packages/astroport-tests-lite/Cargo.toml @@ -18,17 +18,17 @@ cosmwasm-std = "1.1" cosmwasm-schema = "1.1" cw-multi-test = "0.16" -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } astroport-escrow-fee-distributor = { path = "../../contracts/escrow_fee_distributor" } astroport-governance = { path = "../astroport-governance" } voting-escrow-lite = { path = "../../contracts/voting_escrow_lite" } generator-controller-lite = { path = "../../contracts/generator_controller_lite" } astro-assembly = { path = "../../contracts/assembly" } -astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core" } anyhow = "1" diff --git a/packages/astroport-tests/Cargo.toml b/packages/astroport-tests/Cargo.toml index fd284106..823d7d6d 100644 --- a/packages/astroport-tests/Cargo.toml +++ b/packages/astroport-tests/Cargo.toml @@ -18,16 +18,16 @@ cosmwasm-std = "1.1" cosmwasm-schema = "1.1" cw-multi-test = "0.15" -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } astroport-escrow-fee-distributor = { path = "../../contracts/escrow_fee_distributor" } astroport-governance = { path = "../astroport-governance" } voting-escrow = { path = "../../contracts/voting_escrow" } generator-controller = { path = "../../contracts/generator_controller" } -astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } -astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/merge_public_20230519" } +astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core" } anyhow = "1" diff --git a/scripts/check_artifacts_size.sh b/scripts/check_artifacts_size.sh index 5b250550..178120ff 100755 --- a/scripts/check_artifacts_size.sh +++ b/scripts/check_artifacts_size.sh @@ -3,15 +3,9 @@ set -e set -o pipefail -projectPath=$(cd "$(dirname "${0}")" && cd ../ && pwd) - -docker run -v "$projectPath":/code \ - --mount type=volume,source="$(basename "$projectPath")_cache",target=/code/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/workspace-optimizer:0.12.9 - - -maximum_size=700 +# terra: https://github.com/terra-money/wasmd/blob/2308975f45eac299bdf246737674482eaa51051c/x/wasm/types/validation.go#L12 +# injective: https://github.com/InjectiveLabs/wasmd/blob/e087f275712b5f0a798791495dee0e453d67cad3/x/wasm/types/validation.go#L19 +maximum_size=800 for artifact in artifacts/*.wasm; do artifactsize=$(du -k "$artifact" | cut -f 1) @@ -21,4 +15,4 @@ for artifact in artifacts/*.wasm; do echo "Max size: $maximum_size" exit 1 fi -done \ No newline at end of file +done diff --git a/tarpaulin-report.html b/tarpaulin-report.html deleted file mode 100644 index e86c899f..00000000 --- a/tarpaulin-report.html +++ /dev/null @@ -1,660 +0,0 @@ - - - - - - - -
- - - - - - \ No newline at end of file From 3071dab091f88fac33594574cf3bdb34d9674189 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:08:08 +0400 Subject: [PATCH 07/47] fix(assembly): replace ListChannels with Channel query * fix(assembly): replace ListChannels with Channel query * fix(tests): Add IbcQuery::Channel mock --------- Co-authored-by: Donovan Solms --- Cargo.lock | 64 +++++++++--------- contracts/assembly/Cargo.toml | 2 +- contracts/hub/src/mock.rs | 78 +++++----------------- contracts/outpost/src/mock.rs | 49 +++++--------- packages/astroport-governance/Cargo.toml | 2 +- packages/astroport-governance/src/utils.rs | 25 ++++--- 6 files changed, 82 insertions(+), 138 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95f8b617..979ef62c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,10 +21,10 @@ checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "astro-assembly" -version = "1.6.1" +version = "1.6.2" dependencies = [ "anyhow", - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-hub", "astroport-nft", "astroport-staking", @@ -61,8 +61,8 @@ dependencies = [ [[package]] name = "astroport" -version = "3.6.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +version = "3.6.2" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#062bb4b1c9ee9ff079f97850135573a426be16bc" dependencies = [ "astroport-circular-buffer", "cosmwasm-schema", @@ -78,7 +78,7 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#062bb4b1c9ee9ff079f97850135573a426be16bc" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -90,7 +90,7 @@ dependencies = [ name = "astroport-escrow-fee-distributor" version = "1.0.2" dependencies = [ - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-tests", "astroport-token", "cosmwasm-schema", @@ -107,7 +107,7 @@ name = "astroport-factory" version = "1.6.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.2", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -123,8 +123,8 @@ name = "astroport-generator" version = "2.3.2" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", - "astroport-governance 1.4.0", + "astroport 3.6.2", + "astroport-governance 1.4.1", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -150,9 +150,9 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "1.4.0" +version = "1.4.1" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.2", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -165,8 +165,8 @@ name = "astroport-hub" version = "1.0.0" dependencies = [ "anyhow", - "astroport 3.6.0", - "astroport-governance 1.4.0", + "astroport 3.6.2", + "astroport-governance 1.4.1", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.16.5", @@ -191,7 +191,7 @@ dependencies = [ name = "astroport-nft" version = "1.0.0" dependencies = [ - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -204,8 +204,8 @@ name = "astroport-outpost" version = "0.1.0" dependencies = [ "anyhow", - "astroport 3.6.0", - "astroport-governance 1.4.0", + "astroport 3.6.2", + "astroport-governance 1.4.1", "base64", "cosmwasm-schema", "cosmwasm-std", @@ -226,7 +226,7 @@ name = "astroport-pair" version = "1.5.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.2", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -243,7 +243,7 @@ name = "astroport-staking" version = "1.1.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.2", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -259,11 +259,11 @@ name = "astroport-tests" version = "1.0.0" dependencies = [ "anyhow", - "astroport 3.6.0", + "astroport 3.6.2", "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-pair", "astroport-staking", "astroport-token", @@ -283,11 +283,11 @@ version = "1.0.0" dependencies = [ "anyhow", "astro-assembly", - "astroport 3.6.0", + "astroport 3.6.2", "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-pair", "astroport-staking", "astroport-token", @@ -306,7 +306,7 @@ name = "astroport-token" version = "1.1.1" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.2", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -320,7 +320,7 @@ name = "astroport-whitelist" version = "1.0.1" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.2", "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist", @@ -333,7 +333,7 @@ name = "astroport-xastro-token" version = "1.1.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.2", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -422,8 +422,8 @@ checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" name = "builder-unlock" version = "2.0.0" dependencies = [ - "astroport 3.6.0", - "astroport-governance 1.4.0", + "astroport 3.6.2", + "astroport-governance 1.4.1", "astroport-token", "cosmwasm-schema", "cosmwasm-std", @@ -990,7 +990,7 @@ dependencies = [ "anyhow", "astroport-factory", "astroport-generator", - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-pair", "astroport-staking", "astroport-tests", @@ -1015,7 +1015,7 @@ dependencies = [ "anyhow", "astroport-factory", "astroport-generator", - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-pair", "astroport-staking", "astroport-tests-lite", @@ -1645,7 +1645,7 @@ version = "1.3.0" dependencies = [ "anyhow", "astroport-escrow-fee-distributor", - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-staking", "astroport-token", "cosmwasm-schema", @@ -1664,7 +1664,7 @@ name = "voting-escrow-delegation" version = "1.0.0" dependencies = [ "anyhow", - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-nft", "astroport-tests", "cosmwasm-schema", @@ -1684,7 +1684,7 @@ name = "voting-escrow-lite" version = "1.0.0" dependencies = [ "anyhow", - "astroport-governance 1.4.0", + "astroport-governance 1.4.1", "astroport-staking", "astroport-token", "cosmwasm-schema", diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 716e800c..81335d7b 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astro-assembly" -version = "1.6.1" +version = "1.6.2" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" diff --git a/contracts/hub/src/mock.rs b/contracts/hub/src/mock.rs index b5ae25a9..11b99a9e 100644 --- a/contracts/hub/src/mock.rs +++ b/contracts/hub/src/mock.rs @@ -4,8 +4,8 @@ use std::cell::Cell; use cosmwasm_std::{from_binary, Uint64}; use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - to_binary, Addr, Binary, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, - IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, ListChannelsResponse, MessageInfo, OwnedDeps, + to_binary, Addr, Binary, ChannelResponse, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, + IbcChannelOpenMsg, IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, MessageInfo, OwnedDeps, Timestamp, Uint128, }; @@ -149,67 +149,23 @@ impl WasmMockQuerier { } } } - QueryRequest::Ibc(IbcQuery::ListChannels { .. }) => { - let response = ListChannelsResponse { - channels: vec![ - IbcChannel::new( - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-1".to_string(), - }, - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-1".to_string(), - }, - IbcOrder::Unordered, - "version", - "connection-1", - ), - IbcChannel::new( - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-2".to_string(), - }, - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-2".to_string(), - }, - IbcOrder::Unordered, - "version", - "connection-1", - ), - IbcChannel::new( - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-3".to_string(), - }, - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-1".to_string(), - }, - IbcOrder::Unordered, - "version", - "connection-1", - ), - IbcChannel::new( - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-100".to_string(), - }, - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-1".to_string(), - }, - IbcOrder::Unordered, - "version", - "connection-1", - ), - ], + QueryRequest::Ibc(IbcQuery::Channel { .. }) => { + let response = ChannelResponse { + channel: Some(IbcChannel::new( + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcOrder::Unordered, + "version", + "connection-1", + )), }; SystemResult::Ok(to_binary(&response).into()) - // if contract_addr != "cw20_ics20" { - // return SystemResult::Err(SystemError::Unknown {}); - // } } _ => self.base.handle_query(request), } diff --git a/contracts/outpost/src/mock.rs b/contracts/outpost/src/mock.rs index b8bb7e9b..25dae261 100644 --- a/contracts/outpost/src/mock.rs +++ b/contracts/outpost/src/mock.rs @@ -2,8 +2,8 @@ use cosmwasm_std::from_binary; use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - to_binary, Binary, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, - IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, ListChannelsResponse, MessageInfo, OwnedDeps, + to_binary, Binary, ChannelResponse, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, + IbcChannelOpenMsg, IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, MessageInfo, OwnedDeps, Timestamp, Uint128, }; @@ -102,36 +102,21 @@ impl WasmMockQuerier { } } } - QueryRequest::Ibc(IbcQuery::ListChannels { .. }) => { - let response = ListChannelsResponse { - channels: vec![ - IbcChannel::new( - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-3".to_string(), - }, - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-1".to_string(), - }, - IbcOrder::Unordered, - "version", - "connection-1", - ), - IbcChannel::new( - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-15".to_string(), - }, - IbcEndpoint { - port_id: "wasm".to_string(), - channel_id: "channel-1".to_string(), - }, - IbcOrder::Unordered, - "version", - "connection-1", - ), - ], + QueryRequest::Ibc(IbcQuery::Channel { .. }) => { + let response = ChannelResponse { + channel: Some(IbcChannel::new( + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcEndpoint { + port_id: "wasm".to_string(), + channel_id: "channel-1".to_string(), + }, + IbcOrder::Unordered, + "version", + "connection-1", + )), }; SystemResult::Ok(to_binary(&response).into()) } diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 7332ccca..220c44fe 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-governance" -version = "1.4.0" +version = "1.4.1" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" diff --git a/packages/astroport-governance/src/utils.rs b/packages/astroport-governance/src/utils.rs index d4395d8d..6c8aa993 100644 --- a/packages/astroport-governance/src/utils.rs +++ b/packages/astroport-governance/src/utils.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - to_binary, Addr, Decimal, Fraction, IbcQuery, ListChannelsResponse, OverflowError, - QuerierWrapper, QueryRequest, StdError, StdResult, Uint128, Uint256, Uint64, WasmQuery, + to_binary, Addr, ChannelResponse, Decimal, Fraction, IbcQuery, OverflowError, QuerierWrapper, + QueryRequest, StdError, StdResult, Uint128, Uint256, Uint64, WasmQuery, }; use crate::hub::HubBalance; @@ -141,15 +141,18 @@ pub fn check_contract_supports_channel( given_channel: &String, ) -> Result<(), StdError> { let port_id = Some(format!("wasm.{contract}")); - let ListChannelsResponse { channels } = - querier.query(&QueryRequest::Ibc(IbcQuery::ListChannels { port_id }))?; - channels - .iter() - .find(|channel| &channel.endpoint.channel_id == given_channel) - .map(|_| ()) - .ok_or_else(|| StdError::GenericErr { - msg: format!("The contract does not have channel {0}", given_channel), - }) + let ChannelResponse { channel } = querier.query( + &IbcQuery::Channel { + channel_id: given_channel.to_string(), + port_id, + } + .into(), + )?; + channel.map(|_| ()).ok_or_else(|| { + StdError::generic_err(format!( + "The contract does not have channel {given_channel}" + )) + }) } /// Retrieves the total amount of voting power held by all Outposts at a given time From 912f1629c364f1da85831a1b4473dc61bf4671e9 Mon Sep 17 00:00:00 2001 From: Donovan Solms <4567303+donovansolms@users.noreply.github.com> Date: Wed, 1 Nov 2023 14:23:00 +0200 Subject: [PATCH 08/47] feat(assembly): Implement TokenFactory xASTRO queries --- Cargo.lock | 54 ++++-- contracts/assembly/Cargo.toml | 3 +- contracts/assembly/src/contract.rs | 159 ++++++++--------- contracts/assembly/src/error.rs | 4 + contracts/assembly/src/migration.rs | 163 +----------------- packages/astroport-governance/src/assembly.rs | 39 ++--- 6 files changed, 136 insertions(+), 286 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 95f8b617..13e947cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,7 +21,7 @@ checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "astro-assembly" -version = "1.6.1" +version = "2.0.0" dependencies = [ "anyhow", "astroport-governance 1.4.0", @@ -35,6 +35,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test 0.15.1", "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", "cw2 0.15.1", "cw20 0.15.1", "ibc-controller-package", @@ -59,12 +60,27 @@ dependencies = [ "uint", ] +[[package]] +name = "astroport" +version = "3.6.0" +dependencies = [ + "astroport-circular-buffer 0.1.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.1", + "cw20 0.15.1", + "cw3", + "itertools", + "uint", +] + [[package]] name = "astroport" version = "3.6.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport-circular-buffer", + "astroport-circular-buffer 0.1.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -75,6 +91,16 @@ dependencies = [ "uint", ] +[[package]] +name = "astroport-circular-buffer" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "thiserror", +] + [[package]] name = "astroport-circular-buffer" version = "0.1.0" @@ -107,7 +133,7 @@ name = "astroport-factory" version = "1.6.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -123,7 +149,7 @@ name = "astroport-generator" version = "2.3.2" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", @@ -165,7 +191,7 @@ name = "astroport-hub" version = "1.0.0" dependencies = [ "anyhow", - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", @@ -204,7 +230,7 @@ name = "astroport-outpost" version = "0.1.0" dependencies = [ "anyhow", - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", "base64", "cosmwasm-schema", @@ -226,7 +252,7 @@ name = "astroport-pair" version = "1.5.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -243,7 +269,7 @@ name = "astroport-staking" version = "1.1.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -259,7 +285,7 @@ name = "astroport-tests" version = "1.0.0" dependencies = [ "anyhow", - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", @@ -283,7 +309,7 @@ version = "1.0.0" dependencies = [ "anyhow", "astro-assembly", - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", @@ -306,7 +332,7 @@ name = "astroport-token" version = "1.1.1" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -320,7 +346,7 @@ name = "astroport-whitelist" version = "1.0.1" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist", @@ -333,7 +359,7 @@ name = "astroport-xastro-token" version = "1.1.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -422,7 +448,7 @@ checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" name = "builder-unlock" version = "2.0.0" dependencies = [ - "astroport 3.6.0", + "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", "astroport-token", "cosmwasm-schema", diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 716e800c..4d6afb14 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astro-assembly" -version = "1.6.1" +version = "2.0.0" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" @@ -23,6 +23,7 @@ astroport-governance = { path = "../../packages/astroport-governance" } ibc-controller-package = { git = "https://github.com/astroport-fi/astroport_ibc", branch = "main" } thiserror = { version = "1.0" } cosmwasm-schema = "1.1" +cw-utils = "1.0.1" [dev-dependencies] cw-multi-test = "0.15" diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 3e68d186..d8ae9145 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -1,21 +1,21 @@ use cosmwasm_std::{ - attr, entry_point, from_binary, to_binary, wasm_execute, Addr, Binary, CosmosMsg, Decimal, - Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, Uint128, Uint64, WasmMsg, + attr, coin, entry_point, to_binary, wasm_execute, BankMsg, Binary, CosmosMsg, Decimal, Deps, + DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128, Uint64, WasmMsg, }; use cw2::{get_contract_version, set_contract_version}; -use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20ReceiveMsg}; +use cw20::{BalanceResponse, Cw20ExecuteMsg}; use cw_storage_plus::Bound; +use cw_utils::must_pay; use std::str::FromStr; use crate::astroport; use astroport_governance::assembly::{ - helpers::validate_links, Config, Cw20HookMsg, ExecuteMsg, InstantiateMsg, Proposal, - ProposalListResponse, ProposalStatus, ProposalVoteOption, ProposalVotesResponse, QueryMsg, - UpdateConfig, + helpers::validate_links, Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalListResponse, + ProposalStatus, ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, }; use crate::astroport::asset::addr_opt_validate; -use astroport::xastro_token::QueryMsg as XAstroTokenQueryMsg; +use astroport::tokenfactory_tracker::QueryMsg as TokenFactoryTrackerQueryMsg; use astroport_governance::builder_unlock::msg::{ AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, StateResponse, }; @@ -28,7 +28,7 @@ use astroport_governance::voting_escrow_lite::{ }; use crate::error::ContractError; -use crate::migration::{migrate_config_to_160, migrate_proposals_to_v160, MigrateMsg}; +use crate::migration::MigrateMsg; use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT}; use ibc_controller_package::ExecuteMsg as ControllerExecuteMsg; @@ -59,8 +59,13 @@ pub fn instantiate( validate_links(&msg.whitelisted_links)?; + // TODO: Check that the xastro_denom_tracking_address reports the tracked_denom + // to be the same as xastro_denom + let config = Config { - xastro_token_addr: deps.api.addr_validate(&msg.xastro_token_addr)?, + xastro_denom: msg.xastro_denom, + // TODO: Address?, check naming + xastro_denom_tracking: msg.xastro_denom_tracking_address, vxastro_token_addr: addr_opt_validate(deps.api, &msg.vxastro_token_addr)?, voting_escrow_delegator_addr: addr_opt_validate( deps.api, @@ -120,7 +125,22 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::Receive(cw20_msg) => receive_cw20(deps, env, info, cw20_msg), + ExecuteMsg::SubmitProposal { + title, + description, + link, + messages, + ibc_channel, + } => submit_proposal( + deps, + env, + info, + title, + description, + link, + messages, + ibc_channel, + ), ExecuteMsg::CastVote { proposal_id, vote } => cast_vote(deps, env, info, proposal_id, vote), ExecuteMsg::CastOutpostVote { proposal_id, @@ -160,37 +180,6 @@ pub fn execute( } } -/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. -/// -/// * **cw20_msg** CW20 message to process. -pub fn receive_cw20( - deps: DepsMut, - env: Env, - info: MessageInfo, - cw20_msg: Cw20ReceiveMsg, -) -> Result { - match from_binary(&cw20_msg.msg)? { - Cw20HookMsg::SubmitProposal { - title, - description, - link, - messages, - ibc_channel, - } => submit_proposal( - deps, - env, - info, - Addr::unchecked(cw20_msg.sender), - cw20_msg.amount, - title, - description, - link, - messages, - ibc_channel, - ), - } -} - /// Submit a brand new proposal and locks some xASTRO as an anti-spam mechanism. /// /// * **sender** proposal submitter. @@ -209,8 +198,6 @@ pub fn submit_proposal( deps: DepsMut, env: Env, info: MessageInfo, - sender: Addr, - deposit_amount: Uint128, title: String, description: String, link: Option, @@ -219,9 +206,9 @@ pub fn submit_proposal( ) -> Result { let config = CONFIG.load(deps.storage)?; - if info.sender != config.xastro_token_addr { - return Err(ContractError::Unauthorized {}); - } + // Ensure that the correct token is sent. This will fail if + // zero tokens are sent. + let mut deposit_amount = must_pay(&info, &config.xastro_denom)?; if deposit_amount < config.proposal_required_deposit { return Err(ContractError::InsufficientDeposit {}); @@ -243,7 +230,7 @@ pub fn submit_proposal( let proposal = Proposal { proposal_id: count, - submitter: sender.clone(), + submitter: info.sender.clone(), status: ProposalStatus::Active, for_power: Uint128::zero(), outpost_for_power: Uint128::zero(), @@ -275,7 +262,7 @@ pub fn submit_proposal( Ok(Response::new().add_attributes(vec![ attr("action", "submit_proposal"), - attr("submitter", sender), + attr("submitter", info.sender), attr("proposal_id", count), attr( "proposal_end_height", @@ -440,7 +427,8 @@ pub fn cast_outpost_vote( ])) } -/// Ends proposal voting period and sets the proposal status by id. +/// Ends proposal voting period, sets the proposal status by id and returns +/// xASTRO submitted for the proposal. pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result { let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?; @@ -488,14 +476,10 @@ pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result Std // We use the previous block because it always has an up-to-date checkpoint. // BalanceAt will always return the balance information in the previous block, // so we don't subtract one block from proposal.start_block. - let xastro_amount: BalanceResponse = deps.querier.query_wasm_smart( - config.xastro_token_addr, - &XAstroTokenQueryMsg::BalanceAt { + // let xastro_amount: BalanceResponse = deps.querier.query_wasm_smart( + // config.xastro_token_addr, + // &XAstroTokenQueryMsg::BalanceAt { + // address: sender.clone(), + // block: proposal.start_block, + // }, + // )?; + + // TODO: Comment, we query the balance tracking contract for xASTRO + let xastro_amount: Uint128 = deps.querier.query_wasm_smart( + config.xastro_denom_tracking, + &TokenFactoryTrackerQueryMsg::BalanceAt { address: sender.clone(), - block: proposal.start_block, + timestamp: Some(Uint64::from(proposal.start_time)), }, )?; - let mut total = xastro_amount.balance; + + let mut total = xastro_amount; let locked_amount: AllocationResponse = deps.querier.query_wasm_smart( config.builder_unlock_addr, @@ -1107,10 +1101,16 @@ pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult< // This is the address' xASTRO balance at the previous block (proposal.start_block - 1). // We use the previous block because it always has an up-to-date checkpoint. + // let mut total: Uint128 = deps.querier.query_wasm_smart( + // &config.xastro_token_addr, + // &XAstroTokenQueryMsg::TotalSupplyAt { + // block: proposal.start_block - 1, + // }, + // )?; let mut total: Uint128 = deps.querier.query_wasm_smart( - &config.xastro_token_addr, - &XAstroTokenQueryMsg::TotalSupplyAt { - block: proposal.start_block - 1, + config.xastro_denom_tracking, + &TokenFactoryTrackerQueryMsg::TotalSupplyAt { + timestamp: Some(Uint64::from(proposal.start_time)), }, )?; @@ -1142,25 +1142,8 @@ pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult< /// Manages contract migration. #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(mut deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { - let contract_version = get_contract_version(deps.storage)?; - - match contract_version.contract.as_ref() { - "astro-assembly" => match contract_version.version.as_ref() { - "1.3.0" => { - let cfg = migrate_config_to_160(deps.branch(), msg)?; - migrate_proposals_to_v160(deps.branch(), &cfg)?; - } - _ => return Err(ContractError::MigrationError {}), - }, - _ => return Err(ContractError::MigrationError {}), - }; - - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - Ok(Response::new() - .add_attribute("previous_contract_name", &contract_version.contract) - .add_attribute("previous_contract_version", &contract_version.version) - .add_attribute("new_contract_name", CONTRACT_NAME) - .add_attribute("new_contract_version", CONTRACT_VERSION)) +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Err(ContractError::Std(StdError::generic_err( + "This contract cannot be migrated.", + ))) } diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index 3a51707e..7e7f2069 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -1,5 +1,6 @@ use astroport_governance::assembly::ProposalStatus; use cosmwasm_std::{OverflowError, StdError}; +use cw_utils::PaymentError; use thiserror::Error; /// This enum describes Assembly contract errors @@ -82,6 +83,9 @@ pub enum ContractError { #[error("Voting power exceeds maximum Outpost power")] InvalidVotingPower {}, + + #[error("{0}")] + PaymentError(#[from] PaymentError), } impl From for ContractError { diff --git a/contracts/assembly/src/migration.rs b/contracts/assembly/src/migration.rs index 58e213e5..c57a7f94 100644 --- a/contracts/assembly/src/migration.rs +++ b/contracts/assembly/src/migration.rs @@ -1,166 +1,5 @@ -use crate::state::{CONFIG, PROPOSALS}; -use astroport_governance::{ - assembly::{Config, Proposal, ProposalStatus}, - astroport::asset::addr_opt_validate, -}; - use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, CosmosMsg, Decimal, DepsMut, StdResult, Uint128, Uint64}; -use cw_storage_plus::{Item, Map}; /// This structure describes a migration message. #[cw_serde] -pub struct MigrateMsg { - voting_escrow_delegator_addr: Option, - vxastro_token_addr: Option, - ibc_controller: Option, - generator_controller: Option, - hub: Option, -} - -#[cw_serde] -pub struct ProposalV130 { - /// Unique proposal ID - pub proposal_id: Uint64, - /// The address of the proposal submitter - pub submitter: Addr, - /// Status of the proposal - pub status: ProposalStatus, - /// `For` power of proposal - pub for_power: Uint128, - /// `Against` power of proposal - pub against_power: Uint128, - /// `For` votes for the proposal - pub for_voters: Vec, - /// `Against` votes for the proposal - pub against_voters: Vec, - /// Start block of proposal - pub start_block: u64, - /// Start time of proposal - pub start_time: u64, - /// End block of proposal - pub end_block: u64, - /// Delayed end block of proposal - pub delayed_end_block: u64, - /// Expiration block of proposal - pub expiration_block: u64, - /// Proposal title - pub title: String, - /// Proposal description - pub description: String, - /// Proposal link - pub link: Option, - /// Proposal messages - pub messages: Option>, - /// Amount of xASTRO deposited in order to post the proposal - pub deposit_amount: Uint128, - /// IBC channel - pub ibc_channel: Option, -} - -#[cw_serde] -pub struct ConfigV130 { - /// xASTRO token address - pub xastro_token_addr: Addr, - /// vxASTRO token address - pub vxastro_token_addr: Option, - /// Astroport IBC controller contract - pub ibc_controller: Option, - /// Builder unlock contract address - pub builder_unlock_addr: Addr, - /// Proposal voting period - pub proposal_voting_period: u64, - /// Proposal effective delay - pub proposal_effective_delay: u64, - /// Proposal expiration period - pub proposal_expiration_period: u64, - /// Proposal required deposit - pub proposal_required_deposit: Uint128, - /// Proposal required quorum - pub proposal_required_quorum: Decimal, - /// Proposal required threshold - pub proposal_required_threshold: Decimal, - /// Whitelisted links - pub whitelisted_links: Vec, -} - -pub const CONFIG_V130: Item = Item::new("config"); - -/// Migrate proposals to V1.6.0 -pub(crate) fn migrate_proposals_to_v160(deps: DepsMut, cfg: &Config) -> StdResult<()> { - let v130_proposals_interface: Map = Map::new("proposals"); - let proposals_v130 = v130_proposals_interface - .range(deps.storage, None, None, cosmwasm_std::Order::Ascending {}) - .collect::>>()?; - - for (key, proposal) in proposals_v130 { - PROPOSALS.save( - deps.storage, - key, - &Proposal { - proposal_id: proposal.proposal_id, - submitter: proposal.submitter, - status: proposal.status, - for_power: proposal.for_power, - outpost_for_power: Uint128::zero(), - against_power: proposal.against_power, - outpost_against_power: Uint128::zero(), - for_voters: proposal - .for_voters - .into_iter() - .map(|addr| addr.to_string()) - .collect(), - against_voters: proposal - .against_voters - .into_iter() - .map(|addr| addr.to_string()) - .collect(), - start_block: proposal.start_block, - start_time: proposal.start_time, - end_block: proposal.end_block, - delayed_end_block: proposal.end_block + cfg.proposal_effective_delay, - expiration_block: proposal.end_block - + cfg.proposal_effective_delay - + cfg.proposal_expiration_period, - title: proposal.title, - description: proposal.description, - link: proposal.link, - messages: proposal.messages, - deposit_amount: proposal.deposit_amount, - ibc_channel: proposal.ibc_channel, - }, - )?; - } - - Ok(()) -} - -/// Migrate contract config to V1.6.0 -pub(crate) fn migrate_config_to_160(deps: DepsMut, msg: MigrateMsg) -> StdResult { - let cfg_v130 = CONFIG_V130.load(deps.storage)?; - - let cfg = Config { - xastro_token_addr: cfg_v130.xastro_token_addr, - vxastro_token_addr: cfg_v130.vxastro_token_addr, - voting_escrow_delegator_addr: addr_opt_validate( - deps.api, - &msg.voting_escrow_delegator_addr, - )?, - ibc_controller: cfg_v130.ibc_controller, - generator_controller: addr_opt_validate(deps.api, &msg.generator_controller)?, - hub: addr_opt_validate(deps.api, &msg.hub)?, - builder_unlock_addr: cfg_v130.builder_unlock_addr, - proposal_voting_period: cfg_v130.proposal_voting_period, - proposal_effective_delay: cfg_v130.proposal_effective_delay, - proposal_expiration_period: cfg_v130.proposal_expiration_period, - proposal_required_deposit: cfg_v130.proposal_required_deposit, - proposal_required_quorum: cfg_v130.proposal_required_quorum, - proposal_required_threshold: cfg_v130.proposal_required_threshold, - whitelisted_links: cfg_v130.whitelisted_links, - guardian_addr: None, - }; - - CONFIG.save(deps.storage, &cfg)?; - - Ok(cfg) -} +pub struct MigrateMsg {} diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index b37f7bf1..580fe3b3 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -54,7 +54,9 @@ const SAFE_TEXT_CHARS: &str = "!&?#()*+'-./\""; #[cw_serde] pub struct InstantiateMsg { /// Address of xASTRO token - pub xastro_token_addr: String, + pub xastro_denom: String, + // TODO: Comment, the address that tracks xASTRO balances + pub xastro_denom_tracking_address: String, /// Address of vxASTRO token pub vxastro_token_addr: Option, /// Voting Escrow delegator address @@ -86,8 +88,15 @@ pub struct InstantiateMsg { /// This enum describes all execute functions available in the contract. #[cw_serde] pub enum ExecuteMsg { - /// Receive a message of type [`Cw20ReceiveMsg`] - Receive(Cw20ReceiveMsg), + /// Submit a new governance proposal + SubmitProposal { + title: String, + description: String, + link: Option, + messages: Option>, + /// If proposal should be executed on a remote chain this field should specify governance channel + ibc_channel: Option, + }, /// Cast a vote for an active proposal CastVote { /// Proposal identifier @@ -199,25 +208,13 @@ pub enum QueryMsg { TotalVotingPower { proposal_id: u64 }, } -/// This structure stores data for a CW20 hook message. -#[cw_serde] -pub enum Cw20HookMsg { - /// Submit a new proposal in the Assembly - SubmitProposal { - title: String, - description: String, - link: Option, - messages: Option>, - /// If proposal should be executed on a remote chain this field should specify governance channel - ibc_channel: Option, - }, -} - /// This structure stores general parameters for the Assembly contract. #[cw_serde] pub struct Config { - /// xASTRO token address - pub xastro_token_addr: Addr, + /// xASTRO token denom + pub xastro_denom: String, + // TODO: Comments + pub xastro_denom_tracking: String, /// vxASTRO token address pub vxastro_token_addr: Option, /// Voting Escrow delegator address @@ -317,8 +314,8 @@ impl Config { /// This structure stores the params used when updating the main Assembly contract params. #[cw_serde] pub struct UpdateConfig { - /// xASTRO token address - pub xastro_token_addr: Option, + /// xASTRO token denom + pub xastro_denom: Option, /// vxASTRO token address pub vxastro_token_addr: Option, /// Voting Escrow delegator address From fe2d8945f80593fdf9fce9e620a05dbcce4f4b24 Mon Sep 17 00:00:00 2001 From: Donovan Solms <4567303+donovansolms@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:03:21 +0200 Subject: [PATCH 09/47] feat(assembly): Move voters to own storage --- Cargo.lock | 1 - contracts/assembly/Cargo.toml | 1 - contracts/assembly/src/contract.rs | 78 +++++++++---------- contracts/assembly/src/state.rs | 6 +- packages/astroport-governance/src/assembly.rs | 25 +++--- 5 files changed, 58 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13e947cf..488b4a00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,7 +37,6 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw-utils 1.0.1", "cw2 0.15.1", - "cw20 0.15.1", "ibc-controller-package", "thiserror", "voting-escrow", diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 4d6afb14..7c90fac1 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -16,7 +16,6 @@ library = [] [dependencies] cw2 = "0.15" -cw20 = "0.15" cosmwasm-std = { version = "1.1", features = ["ibc3"] } cw-storage-plus = "0.15" astroport-governance = { path = "../../packages/astroport-governance" } diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index d8ae9145..c7378238 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -2,8 +2,7 @@ use cosmwasm_std::{ attr, coin, entry_point, to_binary, wasm_execute, BankMsg, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128, Uint64, WasmMsg, }; -use cw2::{get_contract_version, set_contract_version}; -use cw20::{BalanceResponse, Cw20ExecuteMsg}; +use cw2::set_contract_version; use cw_storage_plus::Bound; use cw_utils::must_pay; use std::str::FromStr; @@ -16,6 +15,7 @@ use astroport_governance::assembly::{ use crate::astroport::asset::addr_opt_validate; use astroport::tokenfactory_tracker::QueryMsg as TokenFactoryTrackerQueryMsg; +use astroport_governance::assembly::ProposalVoterResponse; use astroport_governance::builder_unlock::msg::{ AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, StateResponse, }; @@ -29,12 +29,12 @@ use astroport_governance::voting_escrow_lite::{ use crate::error::ContractError; use crate::migration::MigrateMsg; -use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT}; +use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_VOTERS}; use ibc_controller_package::ExecuteMsg as ControllerExecuteMsg; // Contract name and version used for migration. -const CONTRACT_NAME: &str = "astro-assembly"; +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // Default pagination constants @@ -208,7 +208,8 @@ pub fn submit_proposal( // Ensure that the correct token is sent. This will fail if // zero tokens are sent. - let mut deposit_amount = must_pay(&info, &config.xastro_denom)?; + // TODO: Remove mut + let deposit_amount = must_pay(&info, &config.xastro_denom)?; if deposit_amount < config.proposal_required_deposit { return Err(ContractError::InsufficientDeposit {}); @@ -236,8 +237,6 @@ pub fn submit_proposal( outpost_for_power: Uint128::zero(), against_power: Uint128::zero(), outpost_against_power: Uint128::zero(), - for_voters: Vec::new(), - against_voters: Vec::new(), start_block: env.block.height, start_time: env.block.time.seconds(), end_block: env.block.height + config.proposal_voting_period, @@ -297,9 +296,7 @@ pub fn cast_vote( return Err(ContractError::VotingPeriodEnded {}); } - if proposal.for_voters.contains(&info.sender.to_string()) - || proposal.against_voters.contains(&info.sender.to_string()) - { + if PROPOSAL_VOTERS.has(deps.storage, (proposal_id, info.sender.to_string())) { return Err(ContractError::UserAlreadyVoted {}); } @@ -312,13 +309,16 @@ pub fn cast_vote( match vote_option { ProposalVoteOption::For => { proposal.for_power = proposal.for_power.checked_add(voting_power)?; - proposal.for_voters.push(info.sender.to_string()); } ProposalVoteOption::Against => { proposal.against_power = proposal.against_power.checked_add(voting_power)?; - proposal.against_voters.push(info.sender.to_string()); } }; + PROPOSAL_VOTERS.save( + deps.storage, + (proposal_id, info.sender.to_string()), + &vote_option, + )?; PROPOSALS.save(deps.storage, proposal_id, &proposal)?; @@ -378,7 +378,7 @@ pub fn cast_outpost_vote( return Err(ContractError::VotingPeriodEnded {}); } - if proposal.for_voters.contains(&voter) || proposal.against_voters.contains(&voter) { + if PROPOSAL_VOTERS.has(deps.storage, (proposal_id, voter.clone())) { return Err(ContractError::UserAlreadyVoted {}); } @@ -395,15 +395,14 @@ pub fn cast_outpost_vote( ProposalVoteOption::For => { proposal.for_power = proposal.for_power.checked_add(voting_power)?; proposal.outpost_for_power = proposal.outpost_for_power.checked_add(voting_power)?; - proposal.for_voters.push(voter.clone()); } ProposalVoteOption::Against => { proposal.against_power = proposal.against_power.checked_add(voting_power)?; proposal.outpost_against_power = proposal.outpost_against_power.checked_add(voting_power)?; - proposal.against_voters.push(voter.clone()); } }; + PROPOSAL_VOTERS.save(deps.storage, (proposal_id, voter.clone()), &vote_option)?; // Assert that the total amount of power from Outposts is not greater than the // total amount of power that was available at the time of proposal creation @@ -602,8 +601,6 @@ pub fn submit_execute_emissions_proposal( outpost_for_power: Uint128::zero(), against_power: Uint128::zero(), outpost_against_power: Uint128::zero(), - for_voters: vec![generator_controller.to_string()], - against_voters: Vec::new(), start_block: env.block.height, start_time: env.block.time.seconds(), end_block: env.block.height, @@ -616,6 +613,11 @@ pub fn submit_execute_emissions_proposal( deposit_amount: Uint128::zero(), ibc_channel, }; + PROPOSAL_VOTERS.save( + deps.storage, + (proposal.proposal_id.u64(), generator_controller.to_string()), + &ProposalVoteOption::For, + )?; proposal.validate(config.whitelisted_links)?; @@ -933,14 +935,12 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } QueryMsg::ProposalVoters { proposal_id, - vote_option, - start, + start_after, limit, } => to_binary(&query_proposal_voters( deps, proposal_id, - vote_option, - start, + start_after, limit, )?), } @@ -972,30 +972,28 @@ pub fn query_proposals( }) } -/// Returns proposal's voters. +/// Returns a proposal's voters pub fn query_proposal_voters( deps: Deps, proposal_id: u64, - vote_option: ProposalVoteOption, - start: Option, + start_after: Option, limit: Option, -) -> StdResult> { - let limit = limit.unwrap_or(DEFAULT_VOTERS_LIMIT).min(MAX_VOTERS_LIMIT); - let start = start.unwrap_or_default(); - - let proposal = PROPOSALS.load(deps.storage, proposal_id)?; +) -> StdResult> { + let limit = limit.unwrap_or_else(|| DEFAULT_VOTERS_LIMIT.min(MAX_VOTERS_LIMIT)) as usize; + let start = start_after.map(|s| Bound::ExclusiveRaw(s.into_bytes())); - let voters = match vote_option { - ProposalVoteOption::For => proposal.for_voters, - ProposalVoteOption::Against => proposal.against_voters, - }; - - Ok(voters - .iter() - .skip(start as usize) - .take(limit as usize) - .cloned() - .collect()) + let voters = PROPOSAL_VOTERS + .prefix(proposal_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + item.map(|(address, vote_option)| ProposalVoterResponse { + address, + vote_option, + }) + }) + .collect::>>()?; + Ok(voters) } /// Returns proposal votes stored in the [`ProposalVotesResponse`] structure. diff --git a/contracts/assembly/src/state.rs b/contracts/assembly/src/state.rs index 062881b7..ad030760 100644 --- a/contracts/assembly/src/state.rs +++ b/contracts/assembly/src/state.rs @@ -1,4 +1,4 @@ -use astroport_governance::assembly::{Config, Proposal}; +use astroport_governance::assembly::{Config, Proposal, ProposalVoteOption}; use cosmwasm_std::Uint64; use cw_storage_plus::{Item, Map}; @@ -10,3 +10,7 @@ pub const PROPOSAL_COUNT: Item = Item::new("proposal_count"); /// This is a map that contains information about all proposals pub const PROPOSALS: Map = Map::new("proposals"); + +/// Contains all the voters and their vote option. A String is used for the address +/// to account for cross-chain voting +pub const PROPOSAL_VOTERS: Map<(u64, String), ProposalVoteOption> = Map::new("proposal_votes"); diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index 580fe3b3..ea3c2fa9 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -1,7 +1,6 @@ use crate::assembly::helpers::is_safe_link; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, CosmosMsg, Decimal, StdError, StdResult, Uint128, Uint64}; -use cw20::Cw20ReceiveMsg; use std::fmt::{Display, Formatter, Result}; use std::str::FromStr; @@ -183,14 +182,12 @@ pub enum QueryMsg { limit: Option, }, /// Return proposal voters of specified proposal - #[returns(Vec)] + #[returns(Vec)] ProposalVoters { /// Proposal unique id proposal_id: u64, - /// Proposal vote option - vote_option: ProposalVoteOption, - /// Id from which to start querying - start: Option, + /// Address after which to query + start_after: Option, /// The amount of proposals to return limit: Option, }, @@ -365,10 +362,10 @@ pub struct Proposal { pub against_power: Uint128, /// `Against` power of proposal cast from all Outposts pub outpost_against_power: Uint128, - /// `For` votes for the proposal - pub for_voters: Vec, - /// `Against` votes for the proposal - pub against_voters: Vec, + // /// `For` votes for the proposal + // pub for_voters: Vec, + // /// `Against` votes for the proposal + // pub against_voters: Vec, /// Start block of proposal pub start_block: u64, /// Start time of proposal @@ -518,6 +515,14 @@ pub struct ProposalListResponse { pub proposal_list: Vec, } +#[cw_serde] +pub struct ProposalVoterResponse { + /// The address of the voter + pub address: String, + /// The option address voted with + pub vote_option: ProposalVoteOption, +} + pub mod helpers { use cosmwasm_std::{StdError, StdResult}; From d384530312b403d53cda060694fd297bd23c5fba Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 23 Jan 2024 15:40:51 +0400 Subject: [PATCH 10/47] fix dependencies --- Cargo.lock | 809 ++++++++++++------ Cargo.toml | 1 + contracts/assembly/Cargo.toml | 17 +- contracts/assembly/src/contract.rs | 26 +- contracts/assembly/src/lib.rs | 4 - packages/astroport-governance/src/assembly.rs | 13 +- 6 files changed, 581 insertions(+), 289 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 488b4a00..0b411bf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ "getrandom", "once_cell", @@ -15,27 +15,26 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "astro-assembly" version = "2.0.0" dependencies = [ "anyhow", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", "astroport-governance 1.4.0", "astroport-hub", "astroport-nft", - "astroport-staking", - "astroport-token", - "astroport-xastro-token", + "astroport-staking 2.0.0", "builder-unlock", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.15.1", + "cw-multi-test 0.20.0", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.3", "cw2 0.15.1", "ibc-controller-package", "thiserror", @@ -46,53 +45,55 @@ dependencies = [ [[package]] name = "astroport" -version = "2.10.0" -source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#11e7a81d4b18a40bed916177061a549633e02b1b" +version = "2.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d102b618016b3c1f1ebb5750617a73dbd294a3c941e54b12deabc931d771bc6e" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 0.15.1", "cw20 0.15.1", - "cw3", - "itertools", + "itertools 0.10.5", "uint", ] [[package]] name = "astroport" -version = "3.6.0" +version = "3.8.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#07c45e88c139fced103c034fe426ed017bb64060" dependencies = [ - "astroport-circular-buffer 0.1.0", + "astroport-circular-buffer 0.1.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.3", "cw20 0.15.1", "cw3", - "itertools", + "itertools 0.10.5", "uint", ] [[package]] name = "astroport" -version = "3.6.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +version = "3.8.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" dependencies = [ "astroport-circular-buffer 0.1.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.3", "cw20 0.15.1", "cw3", - "itertools", + "itertools 0.10.5", "uint", ] [[package]] name = "astroport-circular-buffer" version = "0.1.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#07c45e88c139fced103c034fe426ed017bb64060" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -103,7 +104,7 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -129,16 +130,16 @@ dependencies = [ [[package]] name = "astroport-factory" -version = "1.6.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +version = "1.7.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" dependencies = [ - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.3", "cw2 0.15.1", - "itertools", + "itertools 0.10.5", "protobuf", "thiserror", ] @@ -146,14 +147,14 @@ dependencies = [ [[package]] name = "astroport-generator" version = "2.3.2" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" dependencies = [ - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.3", "cw1-whitelist", "cw2 0.15.1", "cw20 0.15.1", @@ -164,9 +165,10 @@ dependencies = [ [[package]] name = "astroport-governance" version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72806ace350e81c4e1cab7e275ef91f05bad830275d697d67ad1bd4acc6f016d" dependencies = [ - "astroport 2.10.0", + "astroport 2.9.5", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -177,7 +179,7 @@ dependencies = [ name = "astroport-governance" version = "1.4.0" dependencies = [ - "astroport 3.6.0", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", @@ -190,13 +192,13 @@ name = "astroport-hub" version = "1.0.0" dependencies = [ "anyhow", - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.16.5", "cw-storage-plus 0.15.1", - "cw2 1.1.0", + "cw2 1.1.2", "cw20 0.15.1", "schemars", "serde", @@ -206,8 +208,9 @@ dependencies = [ [[package]] name = "astroport-ibc" -version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#ffb48ebfd7dbbc010cf86c9b02bad236c456fca0" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c0ce66970f190873b30f862b0cd39fb0d8499678a1860446aa60d9618671f4" dependencies = [ "cosmwasm-schema", ] @@ -229,15 +232,15 @@ name = "astroport-outpost" version = "0.1.0" dependencies = [ "anyhow", - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", - "base64", + "base64 0.13.1", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.16.5", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", - "cw2 1.1.0", + "cw-utils 1.0.3", + "cw2 1.1.2", "cw20 0.15.1", "schemars", "semver", @@ -249,13 +252,13 @@ dependencies = [ [[package]] name = "astroport-pair" version = "1.5.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" dependencies = [ - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.3", "cw2 0.15.1", "cw20 0.15.1", "integer-sqrt", @@ -266,31 +269,45 @@ dependencies = [ [[package]] name = "astroport-staking" version = "1.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" dependencies = [ - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw-utils 1.0.1", + "cw-utils 1.0.3", "cw2 0.15.1", "cw20 0.15.1", "protobuf", "thiserror", ] +[[package]] +name = "astroport-staking" +version = "2.0.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#07c45e88c139fced103c034fe426ed017bb64060" +dependencies = [ + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "osmosis-std", + "thiserror", +] + [[package]] name = "astroport-tests" version = "1.0.0" dependencies = [ "anyhow", - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", "astroport-governance 1.4.0", "astroport-pair", - "astroport-staking", + "astroport-staking 1.1.0", "astroport-token", "astroport-whitelist", "cosmwasm-schema", @@ -308,13 +325,13 @@ version = "1.0.0" dependencies = [ "anyhow", "astro-assembly", - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-escrow-fee-distributor", "astroport-factory", "astroport-generator", "astroport-governance 1.4.0", "astroport-pair", - "astroport-staking", + "astroport-staking 1.1.0", "astroport-token", "astroport-whitelist", "cosmwasm-schema", @@ -329,9 +346,9 @@ dependencies = [ [[package]] name = "astroport-token" version = "1.1.1" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" dependencies = [ - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw2 0.15.1", @@ -343,9 +360,9 @@ dependencies = [ [[package]] name = "astroport-whitelist" version = "1.0.1" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" dependencies = [ - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", "cw1-whitelist", @@ -353,21 +370,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "astroport-xastro-token" -version = "1.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#6c753c41e72ef5ea60bfa3363657bbc6e775f72d" -dependencies = [ - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "cw20-base", - "snafu", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -380,18 +382,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64ct" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "bit-set" version = "0.5.3" @@ -415,9 +435,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "block-buffer" @@ -439,15 +459,15 @@ dependencies = [ [[package]] name = "bnum" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" +checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" [[package]] name = "builder-unlock" version = "2.0.0" dependencies = [ - "astroport 3.6.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", "astroport-token", "cosmwasm-schema", @@ -461,24 +481,15 @@ dependencies = [ [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" - -[[package]] -name = "cc" -version = "1.0.82" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" -dependencies = [ - "libc", -] +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cfg-if" @@ -486,39 +497,49 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +dependencies = [ + "num-traits", +] + [[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 = "cosmwasm-crypto" -version = "1.3.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e272708a9745dad8b591ef8a718571512130f2b39b33e3d7a27c558e3069394" +checksum = "8ed6aa9f904de106fa16443ad14ec2abe75e94ba003bb61c681c0e43d4c58d2a" dependencies = [ "digest 0.10.7", + "ecdsa 0.16.9", "ed25519-zebra", - "k256", + "k256 0.13.3", "rand_core 0.6.4", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.3.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "296db6a3caca5283425ae0cf347f4e46999ba3f6620dbea8939a0e00347831ce" +checksum = "40abec852f3d4abec6d44ead9a58b78325021a1ead1e7229c3471414e57b2e49" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.3.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63c337e097a089e5b52b5d914a7ff6613332777f38ea6d9d36e1887cd0baa72e" +checksum = "b166215fbfe93dc5575bae062aa57ae7bb41121cffe53bac33b033257949d2a9" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -529,9 +550,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.3.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766cc9e7c1762d8fc9c0265808910fcad755200cd0e624195a491dd885a61169" +checksum = "8bf12f8e20bb29d1db66b7ca590bc2f670b548d21e9be92499bc0f9022a994a8" dependencies = [ "proc-macro2", "quote", @@ -540,11 +561,12 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.3.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5e05a95fd2a420cca50f4e94eb7e70648dac64db45e90403997ebefeb143bd" +checksum = "ad011ae7447188e26e4a7dbca2fcd0fc186aa21ae5c86df0503ea44c78f9e469" dependencies = [ - "base64", + "base64 0.21.7", + "bech32", "bnum", "cosmwasm-crypto", "cosmwasm-derive", @@ -554,15 +576,16 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", - "sha2 0.10.7", + "sha2 0.10.8", + "static_assertions", "thiserror", ] [[package]] name = "cosmwasm-storage" -version = "1.3.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "800aaddd70ba915e19bf3d2d992aa3689d8767857727fdd3b414df4fd52d2aa1" +checksum = "66de2ab9db04757bcedef2b5984fbe536903ada4a8a9766717a4a71197ef34f6" dependencies = [ "cosmwasm-std", "serde", @@ -570,9 +593,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -595,6 +618,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -630,8 +665,8 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw-utils 0.15.1", "derivative", - "itertools", - "prost", + "itertools 0.10.5", + "prost 0.9.0", "schemars", "serde", "thiserror", @@ -645,17 +680,36 @@ checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" dependencies = [ "anyhow", "cosmwasm-std", - "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "derivative", - "itertools", - "k256", - "prost", + "itertools 0.10.5", + "k256 0.11.6", + "prost 0.9.0", "schemars", "serde", "thiserror", ] +[[package]] +name = "cw-multi-test" +version = "0.20.0" +source = "git+https://github.com/astroport-fi/cw-multi-test?branch=feat/bank_with_send_hooks#80ebf1aff909d5438fff093b6243c5d7cbf924b3" +dependencies = [ + "anyhow", + "bech32", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "derivative", + "itertools 0.12.0", + "prost 0.12.3", + "schemars", + "serde", + "sha2 0.10.8", + "thiserror", +] + [[package]] name = "cw-storage-plus" version = "0.15.1" @@ -669,9 +723,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" +checksum = "d5ff29294ee99373e2cd5fd21786a3c0ced99a52fec2ca347d565489c61b723c" dependencies = [ "cosmwasm-std", "schemars", @@ -695,13 +749,13 @@ dependencies = [ [[package]] name = "cw-utils" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +checksum = "1c4a657e5caacc3a0d00ee96ca8618745d050b8f757c709babafb81208d4239c" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw2 1.1.0", + "cw2 1.1.2", "schemars", "semver", "serde", @@ -752,14 +806,15 @@ dependencies = [ [[package]] name = "cw2" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" +checksum = "c6c120b24fbbf5c3bedebb97f2cc85fbfa1c3287e09223428e7e597b5293c1fa" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", + "cw-storage-plus 1.2.0", "schemars", + "semver", "serde", "thiserror", ] @@ -779,13 +834,13 @@ dependencies = [ [[package]] name = "cw20" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "011c45920f8200bd5d32d4fe52502506f64f2f75651ab408054d4cfc75ca3a9b" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils 1.0.1", + "cw-utils 1.0.3", "schemars", "serde", ] @@ -810,14 +865,14 @@ dependencies = [ [[package]] name = "cw3" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171af3d9127de6805a7dd819fb070c7d2f6c3ea85f4193f42cef259f0a7f33d5" +checksum = "2967fbd073d4b626dd9e7148e05a84a3bebd9794e71342e12351110ffbb12395" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils 1.0.1", - "cw20 1.1.0", + "cw-utils 1.0.3", + "cw20 1.1.2", "schemars", "serde", "thiserror", @@ -863,6 +918,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "derivative" version = "2.2.0" @@ -890,6 +955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -902,9 +968,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ecdsa" @@ -912,10 +978,24 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.8", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -945,46 +1025,54 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", "digest 0.10.7", - "ff", + "ff 0.12.1", "generic-array", - "group", - "pkcs8", + "group 0.12.1", + "pkcs8 0.9.0", "rand_core 0.6.4", - "sec1", + "sec1 0.3.0", "subtle", "zeroize", ] [[package]] -name = "errno" -version = "0.3.2" +name = "elliptic-curve" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys", + "base16ct 0.2.0", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1 0.7.3", + "subtle", + "zeroize", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "errno" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys", ] [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "ff" @@ -996,6 +1084,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1017,7 +1115,7 @@ dependencies = [ "astroport-generator", "astroport-governance 1.4.0", "astroport-pair", - "astroport-staking", + "astroport-staking 1.1.0", "astroport-tests", "astroport-token", "astroport-whitelist", @@ -1027,7 +1125,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw2 0.15.1", "cw20 0.15.1", - "itertools", + "itertools 0.10.5", "proptest", "thiserror", "voting-escrow", @@ -1042,7 +1140,7 @@ dependencies = [ "astroport-generator", "astroport-governance 1.4.0", "astroport-pair", - "astroport-staking", + "astroport-staking 1.1.0", "astroport-tests-lite", "astroport-token", "astroport-whitelist", @@ -1052,7 +1150,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw2 0.15.1", "cw20 0.15.1", - "itertools", + "itertools 0.10.5", "proptest", "thiserror", "voting-escrow", @@ -1066,13 +1164,14 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -1085,7 +1184,18 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] @@ -1116,8 +1226,9 @@ dependencies = [ [[package]] name = "ibc-controller-package" -version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=main#ffb48ebfd7dbbc010cf86c9b02bad236c456fca0" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcf94f5691716bfecb45e6bb6a82a5c11a392d501c2a695589c5087671f7c33" dependencies = [ "astroport-governance 1.2.0", "astroport-ibc", @@ -1143,11 +1254,29 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[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" @@ -1156,9 +1285,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", - "sha2 0.10.7", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.8", +] + +[[package]] +name = "k256" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +dependencies = [ + "cfg-if", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "once_cell", + "sha2 0.10.8", + "signature 2.2.0", ] [[package]] @@ -1169,27 +1312,27 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -1197,9 +1340,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" @@ -1207,14 +1350,53 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[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", + "prost 0.12.3", + "prost-types 0.12.3", + "schemars", + "serde", + "serde-cw-value", +] + +[[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 = "pkcs8" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.8", + "spki 0.7.3", ] [[package]] @@ -1225,22 +1407,22 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" +checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" dependencies = [ "bit-set", - "bitflags 1.3.2", - "byteorder", + "bit-vec", + "bitflags 2.4.2", "lazy_static", "num-traits", "rand", @@ -1259,7 +1441,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.9.0", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive 0.11.9", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive 0.12.3", ] [[package]] @@ -1269,12 +1471,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost 0.11.9", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost 0.12.3", +] + [[package]] name = "protobuf" version = "2.28.0" @@ -1292,9 +1538,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.32" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1346,18 +1592,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rfc6979" @@ -1365,18 +1611,28 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rustix" -version = "0.38.8" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -1397,15 +1653,15 @@ 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" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "dyn-clone", "schemars_derive", @@ -1415,9 +1671,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", @@ -1431,29 +1687,52 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.8", "generic-array", - "pkcs8", + "pkcs8 0.10.2", "subtle", "zeroize", ] [[package]] name = "semver" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.1" @@ -1465,13 +1744,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.48", ] [[package]] @@ -1487,9 +1766,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "itoa", "ryu", @@ -1511,9 +1790,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -1530,6 +1809,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "snafu" version = "0.6.10" @@ -1558,7 +1847,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.8", ] [[package]] @@ -1586,9 +1885,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1597,9 +1896,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", @@ -1610,29 +1909,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.48", ] [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "uint" @@ -1654,9 +1953,9 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" @@ -1671,7 +1970,7 @@ dependencies = [ "anyhow", "astroport-escrow-fee-distributor", "astroport-governance 1.4.0", - "astroport-staking", + "astroport-staking 1.1.0", "astroport-token", "cosmwasm-schema", "cosmwasm-std", @@ -1710,7 +2009,7 @@ version = "1.0.0" dependencies = [ "anyhow", "astroport-governance 1.4.0", - "astroport-staking", + "astroport-staking 1.1.0", "astroport-token", "cosmwasm-schema", "cosmwasm-std", @@ -1741,18 +2040,18 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "windows-sys" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -1765,48 +2064,48 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[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 1e6cec5c..3fcf3643 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] +resolver = "2" members = ["packages/*", "contracts/*"] [profile.release] diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 7c90fac1..6fdf807d 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -16,23 +16,22 @@ library = [] [dependencies] cw2 = "0.15" -cosmwasm-std = { version = "1.1", features = ["ibc3"] } +cosmwasm-std = { version = "1.5", features = ["ibc3", "cosmwasm_1_1"] } cw-storage-plus = "0.15" astroport-governance = { path = "../../packages/astroport-governance" } -ibc-controller-package = { git = "https://github.com/astroport-fi/astroport_ibc", branch = "main" } -thiserror = { version = "1.0" } -cosmwasm-schema = "1.1" -cw-utils = "1.0.1" +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } +ibc-controller-package = "1.0.0" +thiserror = "1" +cosmwasm-schema = "1.5" +cw-utils = "1" [dev-dependencies] -cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-xastro-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +cw-multi-test = { git = "https://github.com/astroport-fi/cw-multi-test", branch = "feat/bank_with_send_hooks", features = ["cosmwasm_1_1"] } astroport-hub = { path = "../hub" } voting-escrow = { path = "../voting_escrow" } voting-escrow-lite = { path = "../voting_escrow_lite" } voting-escrow-delegation = { path = "../voting_escrow_delegation" } astroport-nft = { path = "../nft" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } builder-unlock = { path = "../builder_unlock" } anyhow = "1" diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index c7378238..6cc2a903 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -1,19 +1,19 @@ use cosmwasm_std::{ - attr, coin, entry_point, to_binary, wasm_execute, BankMsg, Binary, CosmosMsg, Decimal, Deps, - DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128, Uint64, WasmMsg, + attr, coin, entry_point, to_json_binary, wasm_execute, BankMsg, Binary, CosmosMsg, Decimal, + Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128, Uint64, + WasmMsg, }; use cw2::set_contract_version; use cw_storage_plus::Bound; use cw_utils::must_pay; use std::str::FromStr; -use crate::astroport; use astroport_governance::assembly::{ helpers::validate_links, Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalListResponse, ProposalStatus, ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, }; -use crate::astroport::asset::addr_opt_validate; +use astroport::asset::addr_opt_validate; use astroport::tokenfactory_tracker::QueryMsg as TokenFactoryTrackerQueryMsg; use astroport_governance::assembly::ProposalVoterResponse; use astroport_governance::builder_unlock::msg::{ @@ -630,7 +630,7 @@ pub fn submit_execute_emissions_proposal( pub fn check_messages(env: Env, mut messages: Vec) -> Result { messages.push(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: env.contract.address.to_string(), - msg: to_binary(&ExecuteMsg::CheckMessagesPassed {})?, + msg: to_json_binary(&ExecuteMsg::CheckMessagesPassed {})?, funds: vec![], })); @@ -914,30 +914,32 @@ fn remove_outpost_votes( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), - QueryMsg::Proposals { start, limit } => to_binary(&query_proposals(deps, start, limit)?), + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::Proposals { start, limit } => { + to_json_binary(&query_proposals(deps, start, limit)?) + } QueryMsg::Proposal { proposal_id } => { - to_binary(&PROPOSALS.load(deps.storage, proposal_id)?) + to_json_binary(&PROPOSALS.load(deps.storage, proposal_id)?) } QueryMsg::ProposalVotes { proposal_id } => { - to_binary(&query_proposal_votes(deps, proposal_id)?) + to_json_binary(&query_proposal_votes(deps, proposal_id)?) } QueryMsg::UserVotingPower { user, proposal_id } => { let proposal = PROPOSALS.load(deps.storage, proposal_id)?; deps.api.addr_validate(&user)?; - to_binary(&calc_voting_power(deps, user, &proposal)?) + to_json_binary(&calc_voting_power(deps, user, &proposal)?) } QueryMsg::TotalVotingPower { proposal_id } => { let proposal = PROPOSALS.load(deps.storage, proposal_id)?; - to_binary(&calc_total_voting_power_at(deps, &proposal)?) + to_json_binary(&calc_total_voting_power_at(deps, &proposal)?) } QueryMsg::ProposalVoters { proposal_id, start_after, limit, - } => to_binary(&query_proposal_voters( + } => to_json_binary(&query_proposal_voters( deps, proposal_id, start_after, diff --git a/contracts/assembly/src/lib.rs b/contracts/assembly/src/lib.rs index fe535ffc..a4ac6126 100644 --- a/contracts/assembly/src/lib.rs +++ b/contracts/assembly/src/lib.rs @@ -3,7 +3,3 @@ pub mod error; pub mod state; mod migration; - -// During development this import could be replaced with another astroport version. -// However, in production, the astroport version should be the same for all contracts. -pub use astroport_governance::astroport; diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index ea3c2fa9..f96f0bb9 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -52,10 +52,8 @@ const SAFE_TEXT_CHARS: &str = "!&?#()*+'-./\""; /// This structure holds the parameters used for creating an Assembly contract. #[cw_serde] pub struct InstantiateMsg { - /// Address of xASTRO token - pub xastro_denom: String, - // TODO: Comment, the address that tracks xASTRO balances - pub xastro_denom_tracking_address: String, + /// Astroport xASTRO staking address. xASTRO denom and tracker contract address are queried on assembly instantiation. + pub staking_addr: String, /// Address of vxASTRO token pub vxastro_token_addr: Option, /// Voting Escrow delegator address @@ -119,10 +117,7 @@ pub enum ExecuteMsg { proposal_id: u64, }, /// Checks that proposal messages are correct. - CheckMessages { - /// messages - messages: Vec, - }, + CheckMessages(Vec), /// The last endpoint which is executed only if all proposal messages have been passed CheckMessagesPassed {}, /// Execute a successful proposal @@ -210,7 +205,7 @@ pub enum QueryMsg { pub struct Config { /// xASTRO token denom pub xastro_denom: String, - // TODO: Comments + // xASTRO denom tracking contract pub xastro_denom_tracking: String, /// vxASTRO token address pub vxastro_token_addr: Option, From 77fd2ba307119bfb35aa1c83473d3e3bb12cd5b5 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 23 Jan 2024 16:03:43 +0400 Subject: [PATCH 11/47] remove config validation on testnet --- contracts/assembly/Cargo.toml | 3 +- contracts/assembly/src/contract.rs | 2 + packages/astroport-governance/Cargo.toml | 1 - packages/astroport-governance/src/assembly.rs | 48 ++++++------------- 4 files changed, 18 insertions(+), 36 deletions(-) diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 6fdf807d..ae3d01c5 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -11,8 +11,9 @@ crate-type = ["cdylib", "rlib"] [features] backtraces = ["cosmwasm-std/backtraces"] -testnet = ["astroport-governance/testnet"] +testnet = [] library = [] +default = [] [dependencies] cw2 = "0.15" diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 6cc2a903..d80624f2 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -86,6 +86,7 @@ pub fn instantiate( guardian_addr: None, }; + #[cfg(not(feature = "testnet"))] config.validate()?; CONFIG.save(deps.storage, &config)?; @@ -762,6 +763,7 @@ pub fn update_config( config.guardian_addr = Some(deps.api.addr_validate(&guardian_addr)?); } + #[cfg(not(feature = "testnet"))] config.validate()?; CONFIG.save(deps.storage, &config)?; diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 7332ccca..f3282330 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -9,7 +9,6 @@ homepage = "https://astroport.fi" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -testnet = [] # for quicker tests, cargo test --lib # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index f96f0bb9..647c9890 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -1,42 +1,22 @@ -use crate::assembly::helpers::is_safe_link; -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, CosmosMsg, Decimal, StdError, StdResult, Uint128, Uint64}; use std::fmt::{Display, Formatter, Result}; +use std::ops::RangeInclusive; use std::str::FromStr; -#[cfg(not(feature = "testnet"))] -mod proposal_constants { - use std::ops::RangeInclusive; - - pub const MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 33; - pub const MAX_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 100; - pub const MAX_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "1"; - pub const MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "0.01"; - pub const VOTING_PERIOD_INTERVAL: RangeInclusive = 12342..=7 * 12342; - // from 0.5 to 1 day in blocks (7 seconds per block) - pub const DELAY_INTERVAL: RangeInclusive = 6171..=14400; - pub const EXPIRATION_PERIOD_INTERVAL: RangeInclusive = 12342..=100_800; - // from 10k to 60k $xASTRO - pub const DEPOSIT_INTERVAL: RangeInclusive = 10000000000..=60000000000; -} +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, CosmosMsg, Decimal, StdError, StdResult, Uint128, Uint64}; -#[cfg(feature = "testnet")] -mod proposal_constants { - use std::ops::RangeInclusive; - - pub const MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 33; - pub const MAX_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 100; - pub const MAX_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "1"; - pub const MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "0.001"; - pub const VOTING_PERIOD_INTERVAL: RangeInclusive = 200..=7 * 12342; - // from ~350 sec to 1 day in blocks (7 seconds per block) - pub const DELAY_INTERVAL: RangeInclusive = 50..=14400; - pub const EXPIRATION_PERIOD_INTERVAL: RangeInclusive = 400..=100_800; - // from 0.001 to 60k $xASTRO - pub const DEPOSIT_INTERVAL: RangeInclusive = 1000..=60000000000; -} +use crate::assembly::helpers::is_safe_link; -pub use proposal_constants::*; +pub const MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 33; +pub const MAX_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 100; +pub const MAX_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "1"; +pub const MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "0.01"; +pub const VOTING_PERIOD_INTERVAL: RangeInclusive = 12342..=7 * 12342; +// from 0.5 to 1 day in blocks (7 seconds per block) +pub const DELAY_INTERVAL: RangeInclusive = 6171..=14400; +pub const EXPIRATION_PERIOD_INTERVAL: RangeInclusive = 12342..=100_800; +// from 10k to 60k $xASTRO +pub const DEPOSIT_INTERVAL: RangeInclusive = 10000000000..=60000000000; /// Proposal validation attributes const MIN_TITLE_LENGTH: usize = 4; From 5b2a9c86e87898286ee039df1edb735cfe6f0f83 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:51:52 +0400 Subject: [PATCH 12/47] refactor; bump cosmwasm-std to 1.5 --- Cargo.lock | 10 +- contracts/assembly/Cargo.toml | 5 +- contracts/assembly/src/contract.rs | 444 +++--------------- contracts/assembly/src/lib.rs | 3 +- contracts/assembly/src/migration.rs | 5 - contracts/assembly/src/queries.rs | 134 ++++++ contracts/assembly/src/utils.rs | 115 +++++ contracts/assembly/tests/integration.rs | 47 +- .../assembly/tests/integration.vxastro-full | 46 +- contracts/builder_unlock/src/contract.rs | 22 +- contracts/builder_unlock/tests/integration.rs | 28 +- .../escrow_fee_distributor/src/contract.rs | 8 +- .../escrow_fee_distributor/src/testing.rs | 4 +- contracts/escrow_fee_distributor/src/utils.rs | 4 +- .../tests/integration.rs | 16 +- .../generator_controller/src/contract.rs | 16 +- .../generator_controller_lite/src/contract.rs | 16 +- contracts/hub/src/execute.rs | 80 ++-- contracts/hub/src/ibc.rs | 18 +- contracts/hub/src/ibc_governance.rs | 64 +-- contracts/hub/src/ibc_misc.rs | 24 +- contracts/hub/src/ibc_query.rs | 14 +- contracts/hub/src/ibc_staking.rs | 31 +- contracts/hub/src/mock.rs | 30 +- contracts/hub/src/query.rs | 12 +- contracts/hub/src/reply.rs | 9 +- contracts/outpost/Cargo.toml | 2 +- contracts/outpost/src/execute.rs | 54 ++- contracts/outpost/src/ibc.rs | 20 +- contracts/outpost/src/ibc_failure.rs | 24 +- contracts/outpost/src/ibc_mint.rs | 18 +- contracts/outpost/src/mock.rs | 19 +- contracts/outpost/src/query.rs | 8 +- contracts/voting_escrow/src/contract.rs | 36 +- contracts/voting_escrow/tests/integration.rs | 4 +- contracts/voting_escrow/tests/test_utils.rs | 16 +- .../voting_escrow_delegation/src/contract.rs | 40 +- .../tests/integration.rs | 12 +- .../tests/test_helper.rs | 8 +- contracts/voting_escrow_lite/src/execute.rs | 12 +- contracts/voting_escrow_lite/src/query.rs | 40 +- .../voting_escrow_lite/tests/integration.rs | 4 +- .../voting_escrow_lite/tests/test_utils.rs | 14 +- packages/astroport-governance/src/assembly.rs | 5 +- packages/astroport-governance/src/hub.rs | 1 + packages/astroport-governance/src/utils.rs | 20 +- packages/astroport-tests-lite/src/base.rs | 8 +- .../src/controller_helper.rs | 5 +- .../astroport-tests-lite/src/escrow_helper.rs | 12 +- packages/astroport-tests/src/base.rs | 8 +- .../astroport-tests/src/controller_helper.rs | 1 + packages/astroport-tests/src/escrow_helper.rs | 12 +- 52 files changed, 786 insertions(+), 822 deletions(-) delete mode 100644 contracts/assembly/src/migration.rs create mode 100644 contracts/assembly/src/queries.rs create mode 100644 contracts/assembly/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 0b411bf7..c84c7a43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,9 +33,9 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.20.0", - "cw-storage-plus 0.15.1", + "cw-storage-plus 1.2.0", "cw-utils 1.0.3", - "cw2 0.15.1", + "cw2 1.1.2", "ibc-controller-package", "thiserror", "voting-escrow", @@ -234,7 +234,7 @@ dependencies = [ "anyhow", "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", - "base64 0.13.1", + "base64 0.13.0", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.16.5", @@ -390,9 +390,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.13.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64" diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index ae3d01c5..0b45fb27 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -13,12 +13,11 @@ crate-type = ["cdylib", "rlib"] backtraces = ["cosmwasm-std/backtraces"] testnet = [] library = [] -default = [] [dependencies] -cw2 = "0.15" +cw2 = "1" cosmwasm-std = { version = "1.5", features = ["ibc3", "cosmwasm_1_1"] } -cw-storage-plus = "0.15" +cw-storage-plus = "1.2.0" astroport-governance = { path = "../../packages/astroport-governance" } astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } ibc-controller-package = "1.0.0" diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index d80624f2..ddf616ef 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -1,48 +1,31 @@ +use std::str::FromStr; + +use astroport::asset::addr_opt_validate; +use astroport::staking; use cosmwasm_std::{ - attr, coin, entry_point, to_json_binary, wasm_execute, BankMsg, Binary, CosmosMsg, Decimal, - Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128, Uint64, - WasmMsg, + attr, coins, entry_point, wasm_execute, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, + Response, StdError, StdResult, SubMsg, Uint128, Uint64, }; use cw2::set_contract_version; -use cw_storage_plus::Bound; use cw_utils::must_pay; -use std::str::FromStr; +use ibc_controller_package::ExecuteMsg as ControllerExecuteMsg; use astroport_governance::assembly::{ - helpers::validate_links, Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalListResponse, - ProposalStatus, ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, -}; - -use astroport::asset::addr_opt_validate; -use astroport::tokenfactory_tracker::QueryMsg as TokenFactoryTrackerQueryMsg; -use astroport_governance::assembly::ProposalVoterResponse; -use astroport_governance::builder_unlock::msg::{ - AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, StateResponse, + helpers::validate_links, Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalStatus, + ProposalVoteOption, UpdateConfig, }; use astroport_governance::utils::{ - check_contract_supports_channel, get_total_outpost_voting_power_at, WEEK, -}; -use astroport_governance::voting_escrow_delegation::QueryMsg::AdjustedBalance; -use astroport_governance::voting_escrow_lite::{ - QueryMsg as VotingEscrowQueryMsg, VotingPowerResponse, + check_contract_supports_channel, get_total_outpost_voting_power_at, }; use crate::error::ContractError; -use crate::migration::MigrateMsg; use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_VOTERS}; - -use ibc_controller_package::ExecuteMsg as ControllerExecuteMsg; +use crate::utils::{calc_total_voting_power_at, calc_voting_power}; // Contract name and version used for migration. const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -// Default pagination constants -const DEFAULT_LIMIT: u32 = 10; -const MAX_LIMIT: u32 = 30; -const DEFAULT_VOTERS_LIMIT: u32 = 100; -const MAX_VOTERS_LIMIT: u32 = 250; - /// Creates a new contract with the specified parameters in the `msg` variable. #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -59,13 +42,18 @@ pub fn instantiate( validate_links(&msg.whitelisted_links)?; - // TODO: Check that the xastro_denom_tracking_address reports the tracked_denom - // to be the same as xastro_denom + let staking_config = deps + .querier + .query_wasm_smart::(&msg.staking_addr, &staking::QueryMsg::Config {})?; + + let tracker_config = deps.querier.query_wasm_smart::( + &msg.staking_addr, + &staking::QueryMsg::TrackerConfig {}, + )?; let config = Config { - xastro_denom: msg.xastro_denom, - // TODO: Address?, check naming - xastro_denom_tracking: msg.xastro_denom_tracking_address, + xastro_denom: staking_config.xastro_denom, + xastro_denom_tracking: tracker_config.tracker_addr, vxastro_token_addr: addr_opt_validate(deps.api, &msg.vxastro_token_addr)?, voting_escrow_delegator_addr: addr_opt_validate( deps.api, @@ -165,7 +153,7 @@ pub fn execute( messages, ibc_channel, ), - ExecuteMsg::CheckMessages { messages } => check_messages(env, messages), + ExecuteMsg::CheckMessages(messages) => check_messages(env, messages), ExecuteMsg::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}), ExecuteMsg::RemoveCompletedProposal { proposal_id } => { remove_completed_proposal(deps, env, proposal_id) @@ -202,14 +190,13 @@ pub fn submit_proposal( title: String, description: String, link: Option, - messages: Option>, + messages: Vec, ibc_channel: Option, ) -> Result { let config = CONFIG.load(deps.storage)?; // Ensure that the correct token is sent. This will fail if // zero tokens are sent. - // TODO: Remove mut let deposit_amount = must_pay(&info, &config.xastro_denom)?; if deposit_amount < config.proposal_required_deposit { @@ -217,9 +204,7 @@ pub fn submit_proposal( } // Update the proposal count - let count = PROPOSAL_COUNT.update(deps.storage, |c| -> StdResult<_> { - Ok(c.checked_add(Uint64::new(1))?) - })?; + let count = PROPOSAL_COUNT.update::<_, StdError>(deps.storage, |c| Ok(c + Uint64::one()))?; // Check that controller exists and it supports this channel if let Some(ibc_channel) = &ibc_channel { @@ -289,6 +274,7 @@ pub fn cast_vote( return Err(ContractError::ProposalNotActive {}); } + // TODO: remove this restriction? if proposal.submitter == info.sender { return Err(ContractError::Unauthorized {}); } @@ -371,6 +357,7 @@ pub fn cast_outpost_vote( return Err(ContractError::ProposalNotActive {}); } + // TODO: Remove this restriction? if proposal.submitter == voter { return Err(ContractError::Unauthorized {}); } @@ -448,16 +435,10 @@ pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result= config.proposal_required_quorum @@ -471,14 +452,14 @@ pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result { - if !messages.is_empty() { - proposal.status = ProposalStatus::InProgress; - vec![CosmosMsg::Wasm(wasm_execute( - config - .ibc_controller - .ok_or(ContractError::MissingIBCController {})?, - &ControllerExecuteMsg::IbcExecuteProposal { - channel_id: channel.to_string(), - proposal_id, - messages: messages.to_vec(), - }, - vec![], - )?)] - } else { - proposal.status = ProposalStatus::Executed; - vec![] - } - } - None => { - proposal.status = ProposalStatus::Executed; - vec![] - } - }; + let mut response = Response::new().add_attributes([ + attr("action", "execute_proposal"), + attr("proposal_id", proposal_id.to_string()), + ]); + + PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - PROPOSALS.save(deps.storage, proposal_id, &proposal)?; + if let Some(channel) = &proposal.ibc_channel { + if !proposal.messages.is_empty() { + let config = CONFIG.load(deps.storage)?; + + proposal.status = ProposalStatus::InProgress; + response.messages.push(SubMsg::new(wasm_execute( + config + .ibc_controller + .ok_or(ContractError::MissingIBCController {})?, + &ControllerExecuteMsg::IbcExecuteProposal { + channel_id: channel.to_string(), + proposal_id, + messages: proposal.messages, + }, + vec![], + )?)) + } else { + proposal.status = ProposalStatus::Executed; + } } else { proposal.status = ProposalStatus::Executed; - PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - - messages = proposal.messages.unwrap_or_default() + response + .messages + .extend(proposal.messages.into_iter().map(SubMsg::new)) } - Ok(Response::new() - .add_attribute("action", "execute_proposal") - .add_attribute("proposal_id", proposal_id.to_string()) - .add_messages(messages)) + Ok(response) } /// Load and execute a special emissions proposal. This proposal is passed @@ -610,7 +583,7 @@ pub fn submit_execute_emissions_proposal( title, description, link: None, - messages: Some(messages), + messages, deposit_amount: Uint128::zero(), ibc_channel, }; @@ -629,11 +602,14 @@ pub fn submit_execute_emissions_proposal( /// Checks that proposal messages are correct. pub fn check_messages(env: Env, mut messages: Vec) -> Result { - messages.push(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: env.contract.address.to_string(), - msg: to_json_binary(&ExecuteMsg::CheckMessagesPassed {})?, - funds: vec![], - })); + messages.push( + wasm_execute( + &env.contract.address, + &ExecuteMsg::CheckMessagesPassed {}, + vec![], + )? + .into(), + ); Ok(Response::new() .add_attribute("action", "check_messages") @@ -692,10 +668,8 @@ pub fn update_config( } if let Some(voting_escrow_delegator_addr) = updated_config.voting_escrow_delegator_addr { - config.voting_escrow_delegator_addr = Some( - deps.api - .addr_validate(voting_escrow_delegator_addr.as_str())?, - ) + config.voting_escrow_delegator_addr = + Some(deps.api.addr_validate(&voting_escrow_delegator_addr)?) } if let Some(ibc_controller) = updated_config.ibc_controller { @@ -861,16 +835,10 @@ fn remove_outpost_votes( let total_voting_power = calc_total_voting_power_at(deps.as_ref(), &proposal)?; // Recalculate proposal state - let mut proposal_quorum: Decimal = Decimal::zero(); - let mut proposal_threshold: Decimal = Decimal::zero(); - - if !total_voting_power.is_zero() { - proposal_quorum = Decimal::from_ratio(total_votes, total_voting_power); - } - - if !total_votes.is_zero() { - proposal_threshold = Decimal::from_ratio(proposal.for_power, total_votes); - } + let proposal_quorum = + Decimal::checked_from_ratio(total_votes, total_voting_power).unwrap_or_default(); + let proposal_threshold = + Decimal::checked_from_ratio(proposal.for_power, total_votes).unwrap_or_default(); // Determine the proposal result proposal.status = if proposal_quorum >= config.proposal_required_quorum @@ -883,7 +851,7 @@ fn remove_outpost_votes( PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - let response = Response::new().add_attributes(vec![ + let response = Response::new().add_attributes([ attr("action", "remove_outpost_votes"), attr("proposal_id", proposal_id.to_string()), attr("proposal_result", proposal.status.to_string()), @@ -891,261 +859,3 @@ fn remove_outpost_votes( Ok(response) } - -/// Expose available contract queries. -/// -/// ## Queries -/// * **QueryMsg::Config {}** Returns core contract settings stored in the [`Config`] structure. -/// -/// * **QueryMsg::Proposals { start, limit }** Returns a [`ProposalListResponse`] according to the specified input parameters. -/// -/// * **QueryMsg::Proposal { proposal_id }** Returns a [`Proposal`] according to the specified `proposal_id`. -/// -/// * **QueryMsg::ProposalVotes { proposal_id }** Returns proposal vote counts that are stored in the [`ProposalVotesResponse`] structure. -/// -/// * **QueryMsg::UserVotingPower { user, proposal_id }** Returns user voting power for a specific proposal. -/// -/// * **QueryMsg::TotalVotingPower { proposal_id }** Returns total voting power for a specific proposal. -/// -/// * **QueryMsg::ProposalVoters { -/// proposal_id, -/// vote_option, -/// start, -/// limit, -/// }** Returns a vector of proposal voters according to the specified input parameters. -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), - QueryMsg::Proposals { start, limit } => { - to_json_binary(&query_proposals(deps, start, limit)?) - } - QueryMsg::Proposal { proposal_id } => { - to_json_binary(&PROPOSALS.load(deps.storage, proposal_id)?) - } - QueryMsg::ProposalVotes { proposal_id } => { - to_json_binary(&query_proposal_votes(deps, proposal_id)?) - } - QueryMsg::UserVotingPower { user, proposal_id } => { - let proposal = PROPOSALS.load(deps.storage, proposal_id)?; - - deps.api.addr_validate(&user)?; - - to_json_binary(&calc_voting_power(deps, user, &proposal)?) - } - QueryMsg::TotalVotingPower { proposal_id } => { - let proposal = PROPOSALS.load(deps.storage, proposal_id)?; - to_json_binary(&calc_total_voting_power_at(deps, &proposal)?) - } - QueryMsg::ProposalVoters { - proposal_id, - start_after, - limit, - } => to_json_binary(&query_proposal_voters( - deps, - proposal_id, - start_after, - limit, - )?), - } -} - -/// Returns the current proposal list. -pub fn query_proposals( - deps: Deps, - start: Option, - limit: Option, -) -> StdResult { - let proposal_count = PROPOSAL_COUNT.load(deps.storage)?; - - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let start = start.map(Bound::inclusive); - - let proposal_list = PROPOSALS - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| { - let (_, v) = item?; - Ok(v) - }) - .collect::>>()?; - - Ok(ProposalListResponse { - proposal_count, - proposal_list, - }) -} - -/// Returns a proposal's voters -pub fn query_proposal_voters( - deps: Deps, - proposal_id: u64, - start_after: Option, - limit: Option, -) -> StdResult> { - let limit = limit.unwrap_or_else(|| DEFAULT_VOTERS_LIMIT.min(MAX_VOTERS_LIMIT)) as usize; - let start = start_after.map(|s| Bound::ExclusiveRaw(s.into_bytes())); - - let voters = PROPOSAL_VOTERS - .prefix(proposal_id) - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|item| { - item.map(|(address, vote_option)| ProposalVoterResponse { - address, - vote_option, - }) - }) - .collect::>>()?; - Ok(voters) -} - -/// Returns proposal votes stored in the [`ProposalVotesResponse`] structure. -pub fn query_proposal_votes(deps: Deps, proposal_id: u64) -> StdResult { - let proposal = PROPOSALS.load(deps.storage, proposal_id)?; - - Ok(ProposalVotesResponse { - proposal_id, - for_power: proposal.for_power, - against_power: proposal.against_power, - }) -} - -/// Calculates an address' voting power at the specified block. -/// -/// * **sender** address whose voting power we calculate. -/// -/// * **proposal** proposal for which we want to compute the `sender` (voter) voting power. -pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> StdResult { - let config = CONFIG.load(deps.storage)?; - - // This is the address' xASTRO balance at the previous block (proposal.start_block - 1). - // We use the previous block because it always has an up-to-date checkpoint. - // BalanceAt will always return the balance information in the previous block, - // so we don't subtract one block from proposal.start_block. - // let xastro_amount: BalanceResponse = deps.querier.query_wasm_smart( - // config.xastro_token_addr, - // &XAstroTokenQueryMsg::BalanceAt { - // address: sender.clone(), - // block: proposal.start_block, - // }, - // )?; - - // TODO: Comment, we query the balance tracking contract for xASTRO - let xastro_amount: Uint128 = deps.querier.query_wasm_smart( - config.xastro_denom_tracking, - &TokenFactoryTrackerQueryMsg::BalanceAt { - address: sender.clone(), - timestamp: Some(Uint64::from(proposal.start_time)), - }, - )?; - - let mut total = xastro_amount; - - let locked_amount: AllocationResponse = deps.querier.query_wasm_smart( - config.builder_unlock_addr, - &BuilderUnlockQueryMsg::Allocation { - account: sender.clone(), - }, - )?; - - if !locked_amount.params.amount.is_zero() { - total = total - .checked_add(locked_amount.params.amount)? - .checked_sub(locked_amount.status.astro_withdrawn)?; - } - - if let Some(vxastro_token_addr) = config.vxastro_token_addr { - let vxastro_amount: Uint128 = - if let Some(voting_escrow_delegator_addr) = config.voting_escrow_delegator_addr { - deps.querier.query_wasm_smart( - voting_escrow_delegator_addr, - &AdjustedBalance { - account: sender.clone(), - timestamp: Some(proposal.start_time - WEEK), - }, - )? - } else { - // For vxASTRO lite, this will always be 0 - let res: VotingPowerResponse = deps.querier.query_wasm_smart( - &vxastro_token_addr, - &VotingEscrowQueryMsg::UserVotingPowerAt { - user: sender.clone(), - time: proposal.start_time - WEEK, - }, - )?; - res.voting_power - }; - - if !vxastro_amount.is_zero() { - total = total.checked_add(vxastro_amount)?; - } - - let locked_xastro: Uint128 = deps.querier.query_wasm_smart( - vxastro_token_addr, - &VotingEscrowQueryMsg::UserDepositAt { - user: sender, - timestamp: Uint64::from(proposal.start_time), - }, - )?; - - total = total.checked_add(locked_xastro)?; - } - - Ok(total) -} - -/// Calculates the total voting power at a specified block (that is relevant for a specific proposal). -/// -/// * **proposal** proposal for which we calculate the total voting power. -pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult { - let config = CONFIG.load(deps.storage)?; - - // This is the address' xASTRO balance at the previous block (proposal.start_block - 1). - // We use the previous block because it always has an up-to-date checkpoint. - // let mut total: Uint128 = deps.querier.query_wasm_smart( - // &config.xastro_token_addr, - // &XAstroTokenQueryMsg::TotalSupplyAt { - // block: proposal.start_block - 1, - // }, - // )?; - let mut total: Uint128 = deps.querier.query_wasm_smart( - config.xastro_denom_tracking, - &TokenFactoryTrackerQueryMsg::TotalSupplyAt { - timestamp: Some(Uint64::from(proposal.start_time)), - }, - )?; - - // Total amount of ASTRO locked in the initial builder's unlock schedule - let builder_state: StateResponse = deps - .querier - .query_wasm_smart(config.builder_unlock_addr, &BuilderUnlockQueryMsg::State {})?; - - if !builder_state.remaining_astro_tokens.is_zero() { - total = total.checked_add(builder_state.remaining_astro_tokens)?; - } - - if let Some(vxastro_token_addr) = config.vxastro_token_addr { - // Total vxASTRO voting power - // For vxASTRO lite, this will always be 0 - let vxastro: VotingPowerResponse = deps.querier.query_wasm_smart( - vxastro_token_addr, - &VotingEscrowQueryMsg::TotalVotingPowerAt { - time: proposal.start_time - WEEK, - }, - )?; - if !vxastro.voting_power.is_zero() { - total = total.checked_add(vxastro.voting_power)?; - } - } - - Ok(total) -} - -/// Manages contract migration. -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - Err(ContractError::Std(StdError::generic_err( - "This contract cannot be migrated.", - ))) -} diff --git a/contracts/assembly/src/lib.rs b/contracts/assembly/src/lib.rs index a4ac6126..4974f7a6 100644 --- a/contracts/assembly/src/lib.rs +++ b/contracts/assembly/src/lib.rs @@ -2,4 +2,5 @@ pub mod contract; pub mod error; pub mod state; -mod migration; +pub mod queries; +pub mod utils; diff --git a/contracts/assembly/src/migration.rs b/contracts/assembly/src/migration.rs deleted file mode 100644 index c57a7f94..00000000 --- a/contracts/assembly/src/migration.rs +++ /dev/null @@ -1,5 +0,0 @@ -use cosmwasm_schema::cw_serde; - -/// This structure describes a migration message. -#[cw_serde] -pub struct MigrateMsg {} diff --git a/contracts/assembly/src/queries.rs b/contracts/assembly/src/queries.rs new file mode 100644 index 00000000..3654c561 --- /dev/null +++ b/contracts/assembly/src/queries.rs @@ -0,0 +1,134 @@ +use cosmwasm_std::{entry_point, to_json_binary, Binary, Deps, Env, Order, StdResult}; +use cw_storage_plus::Bound; + +use astroport_governance::assembly::{ + ProposalListResponse, ProposalVoterResponse, ProposalVotesResponse, QueryMsg, +}; + +use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_VOTERS}; +use crate::utils::{calc_total_voting_power_at, calc_voting_power}; + +// Default pagination constants +const DEFAULT_LIMIT: u32 = 10; +const MAX_LIMIT: u32 = 30; +const DEFAULT_VOTERS_LIMIT: u32 = 100; +const MAX_VOTERS_LIMIT: u32 = 250; + +/// Expose available contract queries. +/// +/// ## Queries +/// * **QueryMsg::Config {}** Returns core contract settings stored in the [`Config`] structure. +/// +/// * **QueryMsg::Proposals { start, limit }** Returns a [`ProposalListResponse`] according to the specified input parameters. +/// +/// * **QueryMsg::Proposal { proposal_id }** Returns a [`Proposal`] according to the specified `proposal_id`. +/// +/// * **QueryMsg::ProposalVotes { proposal_id }** Returns proposal vote counts that are stored in the [`ProposalVotesResponse`] structure. +/// +/// * **QueryMsg::UserVotingPower { user, proposal_id }** Returns user voting power for a specific proposal. +/// +/// * **QueryMsg::TotalVotingPower { proposal_id }** Returns total voting power for a specific proposal. +/// +/// * **QueryMsg::ProposalVoters { +/// proposal_id, +/// vote_option, +/// start, +/// limit, +/// }** Returns a vector of proposal voters according to the specified input parameters. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::Proposals { start, limit } => { + to_json_binary(&query_proposals(deps, start, limit)?) + } + QueryMsg::Proposal { proposal_id } => { + to_json_binary(&PROPOSALS.load(deps.storage, proposal_id)?) + } + QueryMsg::ProposalVotes { proposal_id } => { + to_json_binary(&query_proposal_votes(deps, proposal_id)?) + } + QueryMsg::UserVotingPower { user, proposal_id } => { + let proposal = PROPOSALS.load(deps.storage, proposal_id)?; + + deps.api.addr_validate(&user)?; + + to_json_binary(&calc_voting_power(deps, user, &proposal)?) + } + QueryMsg::TotalVotingPower { proposal_id } => { + let proposal = PROPOSALS.load(deps.storage, proposal_id)?; + to_json_binary(&calc_total_voting_power_at(deps, &proposal)?) + } + QueryMsg::ProposalVoters { + proposal_id, + start_after, + limit, + } => to_json_binary(&query_proposal_voters( + deps, + proposal_id, + start_after, + limit, + )?), + } +} + +/// Returns the current proposal list. +pub fn query_proposals( + deps: Deps, + start: Option, + limit: Option, +) -> StdResult { + let proposal_count = PROPOSAL_COUNT.load(deps.storage)?; + + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start.map(Bound::inclusive); + + let proposal_list = PROPOSALS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (_, v) = item?; + Ok(v) + }) + .collect::>>()?; + + Ok(ProposalListResponse { + proposal_count, + proposal_list, + }) +} + +/// Returns a proposal's voters +pub fn query_proposal_voters( + deps: Deps, + proposal_id: u64, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or_else(|| DEFAULT_VOTERS_LIMIT.min(MAX_VOTERS_LIMIT)) as usize; + let start = start_after.map(|s| Bound::ExclusiveRaw(s.into_bytes())); + + let voters = PROPOSAL_VOTERS + .prefix(proposal_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + item.map(|(address, vote_option)| ProposalVoterResponse { + address, + vote_option, + }) + }) + .collect::>>()?; + Ok(voters) +} + +/// Returns proposal votes stored in the [`ProposalVotesResponse`] structure. +pub fn query_proposal_votes(deps: Deps, proposal_id: u64) -> StdResult { + let proposal = PROPOSALS.load(deps.storage, proposal_id)?; + + Ok(ProposalVotesResponse { + proposal_id, + for_power: proposal.for_power, + against_power: proposal.against_power, + }) +} diff --git a/contracts/assembly/src/utils.rs b/contracts/assembly/src/utils.rs new file mode 100644 index 00000000..9a1159b1 --- /dev/null +++ b/contracts/assembly/src/utils.rs @@ -0,0 +1,115 @@ +use astroport::tokenfactory_tracker; +use cosmwasm_std::{Deps, StdResult, Timestamp, Uint128, Uint64}; + +use astroport_governance::assembly::Proposal; +use astroport_governance::builder_unlock::msg::{ + AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, StateResponse, +}; +use astroport_governance::utils::WEEK; +use astroport_governance::voting_escrow_delegation::QueryMsg::AdjustedBalance; +use astroport_governance::voting_escrow_lite::{ + QueryMsg as VotingEscrowQueryMsg, VotingPowerResponse, +}; + +use crate::state::CONFIG; + +/// Calculates an address' voting power at the specified block. +/// +/// * **sender** address whose voting power we calculate. +/// +/// * **proposal** proposal for which we want to compute the `sender` (voter) voting power. +pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> StdResult { + let config = CONFIG.load(deps.storage)?; + + let xastro_amount: Uint128 = deps.querier.query_wasm_smart( + &config.xastro_denom_tracking, + &tokenfactory_tracker::QueryMsg::BalanceAt { + address: sender.clone(), + timestamp: Some(Timestamp::from_seconds(proposal.start_time).nanos()), + }, + )?; + + let mut total = xastro_amount; + + let locked_amount: AllocationResponse = deps.querier.query_wasm_smart( + config.builder_unlock_addr, + &BuilderUnlockQueryMsg::Allocation { + account: sender.clone(), + }, + )?; + + total += locked_amount.params.amount - locked_amount.status.astro_withdrawn; + + if let Some(vxastro_token_addr) = config.vxastro_token_addr { + let vxastro_amount = + if let Some(voting_escrow_delegator_addr) = config.voting_escrow_delegator_addr { + deps.querier.query_wasm_smart::( + voting_escrow_delegator_addr, + &AdjustedBalance { + account: sender.clone(), + timestamp: Some(proposal.start_time - WEEK), + }, + )? + } else { + // For vxASTRO lite, this will always be 0 + let res: VotingPowerResponse = deps.querier.query_wasm_smart( + &vxastro_token_addr, + &VotingEscrowQueryMsg::UserVotingPowerAt { + user: sender.clone(), + time: proposal.start_time - WEEK, + }, + )?; + res.voting_power + }; + + total += vxastro_amount; + + let locked_xastro: Uint128 = deps.querier.query_wasm_smart( + vxastro_token_addr, + &VotingEscrowQueryMsg::UserDepositAt { + user: sender, + timestamp: Uint64::from(proposal.start_time), + }, + )?; + + total += locked_xastro; + } + + Ok(total) +} + +/// Calculates the total voting power at a specified block (that is relevant for a specific proposal). +/// +/// * **proposal** proposal for which we calculate the total voting power. +pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult { + let config = CONFIG.load(deps.storage)?; + + let mut total: Uint128 = deps.querier.query_wasm_smart( + config.xastro_denom_tracking, + &tokenfactory_tracker::QueryMsg::TotalSupplyAt { + timestamp: Some(Timestamp::from_seconds(proposal.start_time).nanos()), + }, + )?; + + // Total amount of ASTRO locked in the initial builder's unlock schedule + let builder_state: StateResponse = deps + .querier + .query_wasm_smart(config.builder_unlock_addr, &BuilderUnlockQueryMsg::State {})?; + + total += builder_state.remaining_astro_tokens; + + if let Some(vxastro_token_addr) = config.vxastro_token_addr { + // Total vxASTRO voting power + // For vxASTRO lite, this will always be 0 + let vxastro: VotingPowerResponse = deps.querier.query_wasm_smart( + vxastro_token_addr, + &VotingEscrowQueryMsg::TotalVotingPowerAt { + time: proposal.start_time - WEEK, + }, + )?; + + total += vxastro.voting_power; + } + + Ok(total) +} diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index bcf5c039..3be73025 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -24,7 +24,7 @@ use astroport_governance::builder_unlock::{AllocationParams, Schedule}; use astroport_governance::utils::{EPOCH_START, WEEK}; use cosmwasm_std::{ testing::{mock_env, MockApi, MockStorage}, - to_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, + to_json_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, Uint64, WasmMsg, WasmQuery, }; use cw20::{BalanceResponse, Cw20ExecuteMsg, MinterResponse}; @@ -249,7 +249,7 @@ fn test_proposal_submitting() { // Try to create proposal with insufficient token deposit let submit_proposal_msg = Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from("https://some.link")), @@ -273,7 +273,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("X"), description: String::from("Description"), link: Some(String::from("https://some.link/")), @@ -298,7 +298,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from_utf8(vec![b'X'; 65]).unwrap(), description: String::from("Description"), link: Some(String::from("https://some.link/")), @@ -324,7 +324,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("X"), link: Some(String::from("https://some.link/")), @@ -349,7 +349,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from_utf8(vec![b'X'; 1025]).unwrap(), link: Some(String::from("https://some.link/")), @@ -375,7 +375,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from("X")), @@ -400,7 +400,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from_utf8(vec![b'X'; 129]).unwrap()), @@ -425,7 +425,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from("https://some1.link")), @@ -450,7 +450,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from( @@ -477,13 +477,13 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from("https://some.link/q/")), messages: Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -535,7 +535,7 @@ fn test_proposal_submitting() { proposal.messages, Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -679,7 +679,7 @@ fn test_successful_proposal() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -1142,7 +1142,7 @@ fn test_voting_power_changes() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -1276,7 +1276,7 @@ fn test_fail_outpost_vote_without_hub() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -1392,7 +1392,7 @@ fn test_outpost_vote() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -1783,7 +1783,7 @@ fn test_check_messages() { let vxastro_blacklist_msg = vec![( vxastro_addr.to_string(), - to_binary( + to_json_binary( &astroport_governance::voting_escrow_lite::ExecuteMsg::UpdateConfig { new_guardian: None, generator_controller: None, @@ -1969,7 +1969,7 @@ fn instantiate_xastro_token(router: &mut App, owner: &Addr, astro_token: &Addr) .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: staking_instance.to_string(), - msg: to_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -2145,7 +2145,7 @@ fn mint_vxastro( let msg = Cw20ExecuteMsg::Send { contract: vxastro.to_string(), amount: Uint128::from(amount), - msg: to_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), + msg: to_json_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), }; app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); @@ -2176,7 +2176,8 @@ fn create_allocations( &Cw20ExecuteMsg::Send { contract: builder_unlock_contract_addr.to_string(), amount: Uint128::from(amount), - msg: to_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }).unwrap(), + msg: to_json_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }) + .unwrap(), }, &[], ) @@ -2204,7 +2205,7 @@ fn create_proposal( &Cw20ExecuteMsg::Send { contract: assembly.to_string(), amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - msg: to_binary(&submit_proposal_msg).unwrap(), + msg: to_json_binary(&submit_proposal_msg).unwrap(), }, &[], ) @@ -2294,7 +2295,7 @@ fn cast_outpost_vote( // astro_token: Addr, // amount: Uint128, // ) -> anyhow::Result { -// let cw20_msg = to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { +// let cw20_msg = to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { // channel: "channel-1".to_string(), // sender: "remoteuser1".to_string(), // receiver: hub.to_string(), diff --git a/contracts/assembly/tests/integration.vxastro-full b/contracts/assembly/tests/integration.vxastro-full index e023f256..a0e4af10 100644 --- a/contracts/assembly/tests/integration.vxastro-full +++ b/contracts/assembly/tests/integration.vxastro-full @@ -26,7 +26,7 @@ use astroport_governance::voting_escrow_delegation::{ }; use cosmwasm_std::{ testing::{mock_env, MockApi, MockStorage}, - to_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, + to_json_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, Uint64, WasmMsg, WasmQuery, }; use cw20::{BalanceResponse, Cw20ExecuteMsg, MinterResponse}; @@ -251,7 +251,7 @@ fn test_proposal_submitting() { // Try to create proposal with insufficient token deposit let submit_proposal_msg = Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from("https://some.link")), @@ -275,7 +275,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("X"), description: String::from("Description"), link: Some(String::from("https://some.link/")), @@ -300,7 +300,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from_utf8(vec![b'X'; 65]).unwrap(), description: String::from("Description"), link: Some(String::from("https://some.link/")), @@ -326,7 +326,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("X"), link: Some(String::from("https://some.link/")), @@ -351,7 +351,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from_utf8(vec![b'X'; 1025]).unwrap(), link: Some(String::from("https://some.link/")), @@ -377,7 +377,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from("X")), @@ -402,7 +402,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from_utf8(vec![b'X'; 129]).unwrap()), @@ -427,7 +427,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from("https://some1.link")), @@ -452,7 +452,7 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from( @@ -479,13 +479,13 @@ fn test_proposal_submitting() { xastro_addr.clone(), &Cw20ExecuteMsg::Send { contract: assembly_addr.to_string(), - msg: to_binary(&Cw20HookMsg::SubmitProposal { + msg: to_json_binary(&Cw20HookMsg::SubmitProposal { title: String::from("Title"), description: String::from("Description"), link: Some(String::from("https://some.link/q/")), messages: Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -536,7 +536,7 @@ fn test_proposal_submitting() { proposal.messages, Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -677,7 +677,7 @@ fn test_successful_proposal() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -1143,7 +1143,7 @@ fn test_voting_power_changes() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -1276,7 +1276,7 @@ fn test_fail_outpost_vote_without_hub() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -1380,7 +1380,7 @@ fn test_outpost_vote() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -1819,7 +1819,7 @@ fn test_check_messages() { let vxastro_blacklist_msg = vec![( vxastro_addr.to_string(), - to_binary( + to_json_binary( &astroport_governance::voting_escrow::ExecuteMsg::UpdateConfig { new_guardian: None }, ) .unwrap(), @@ -1946,7 +1946,7 @@ fn test_delegated_vp() { Addr::unchecked("user0"), Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: assembly_addr.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_token_addr: None, vxastro_token_addr: None, voting_escrow_delegator_addr: None, @@ -2174,7 +2174,7 @@ fn instantiate_xastro_token(router: &mut App, owner: &Addr, astro_token: &Addr) .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: staking_instance.to_string(), - msg: to_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -2346,7 +2346,7 @@ fn mint_vxastro( let msg = Cw20ExecuteMsg::Send { contract: vxastro.to_string(), amount: Uint128::from(amount), - msg: to_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), + msg: to_json_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), }; app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); @@ -2389,7 +2389,7 @@ fn create_allocations( &Cw20ExecuteMsg::Send { contract: builder_unlock_contract_addr.to_string(), amount: Uint128::from(amount), - msg: to_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }).unwrap(), + msg: to_json_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }).unwrap(), }, &[], ) @@ -2417,7 +2417,7 @@ fn create_proposal( &Cw20ExecuteMsg::Send { contract: assembly.to_string(), amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - msg: to_binary(&submit_proposal_msg).unwrap(), + msg: to_json_binary(&submit_proposal_msg).unwrap(), }, &[], ) diff --git a/contracts/builder_unlock/src/contract.rs b/contracts/builder_unlock/src/contract.rs index ec603d24..9d2a2f20 100644 --- a/contracts/builder_unlock/src/contract.rs +++ b/contracts/builder_unlock/src/contract.rs @@ -1,8 +1,8 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, from_binary, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, - StdError, StdResult, Uint128, WasmMsg, + attr, from_json, to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, + Response, StdError, StdResult, Uint128, WasmMsg, }; use cw2::set_contract_version; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; @@ -150,7 +150,7 @@ fn execute_receive_cw20( info: MessageInfo, cw20_msg: Cw20ReceiveMsg, ) -> StdResult { - match from_binary(&cw20_msg.msg)? { + match from_json(&cw20_msg.msg)? { ReceiveMsg::CreateAllocations { allocations } => execute_create_allocations( deps, cw20_msg.sender, @@ -190,17 +190,17 @@ fn execute_receive_cw20( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), - QueryMsg::State {} => to_binary(&query_state(deps)?), - QueryMsg::Allocation { account } => to_binary(&query_allocation(deps, account)?), + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::State {} => to_json_binary(&query_state(deps)?), + QueryMsg::Allocation { account } => to_json_binary(&query_allocation(deps, account)?), QueryMsg::UnlockedTokens { account } => { - to_binary(&query_tokens_unlocked(deps, env, account)?) + to_json_binary(&query_tokens_unlocked(deps, env, account)?) } QueryMsg::SimulateWithdraw { account, timestamp } => { - to_binary(&query_simulate_withdraw(deps, env, account, timestamp)?) + to_json_binary(&query_simulate_withdraw(deps, env, account, timestamp)?) } QueryMsg::Allocations { start_after, limit } => { - to_binary(&query_allocations(deps, start_after, limit)?) + to_json_binary(&query_allocations(deps, start_after, limit)?) } } } @@ -304,7 +304,7 @@ fn execute_withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult StdResult { match msg { QueryMsg::UserReward { user, timestamp } => { - to_binary(&query_user_reward(deps, user, timestamp)?) + to_json_binary(&query_user_reward(deps, user, timestamp)?) } - QueryMsg::Config {} => to_binary(&query_config(deps)?), + QueryMsg::Config {} => to_json_binary(&query_config(deps)?), QueryMsg::AvailableRewardPerWeek { start_after, limit } => { - to_binary(&query_available_reward_per_week(deps, start_after, limit)?) + to_json_binary(&query_available_reward_per_week(deps, start_after, limit)?) } } } diff --git a/contracts/escrow_fee_distributor/src/testing.rs b/contracts/escrow_fee_distributor/src/testing.rs index 1fa8bd70..9311d424 100644 --- a/contracts/escrow_fee_distributor/src/testing.rs +++ b/contracts/escrow_fee_distributor/src/testing.rs @@ -2,7 +2,7 @@ use crate::contract::{instantiate, query}; use astroport_governance::escrow_fee_distributor::{ConfigResponse, InstantiateMsg, QueryMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{from_binary, Addr}; +use cosmwasm_std::{from_json, Addr}; #[test] fn proper_initialization() { @@ -21,7 +21,7 @@ fn proper_initialization() { let _res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); assert_eq!( - from_binary::(&query(deps.as_ref(), env, QueryMsg::Config {}).unwrap()) + from_json::(&query(deps.as_ref(), env, QueryMsg::Config {}).unwrap()) .unwrap(), ConfigResponse { owner: Addr::unchecked("owner"), diff --git a/contracts/escrow_fee_distributor/src/utils.rs b/contracts/escrow_fee_distributor/src/utils.rs index 25629da1..e3ee9128 100644 --- a/contracts/escrow_fee_distributor/src/utils.rs +++ b/contracts/escrow_fee_distributor/src/utils.rs @@ -1,7 +1,7 @@ use std::cmp::min; use cosmwasm_std::{ - to_binary, Addr, CosmosMsg, DepsMut, StdError, StdResult, Storage, Uint128, WasmMsg, + to_json_binary, Addr, CosmosMsg, DepsMut, StdError, StdResult, Storage, Uint128, WasmMsg, }; use cw20::Cw20ExecuteMsg; @@ -27,7 +27,7 @@ pub(crate) fn transfer_token_amount( let messages = if !amount.is_zero() { vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: contract_addr.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { + msg: to_json_binary(&Cw20ExecuteMsg::Transfer { recipient: recipient.to_string(), amount, })?, diff --git a/contracts/escrow_fee_distributor/tests/integration.rs b/contracts/escrow_fee_distributor/tests/integration.rs index de1b2ccb..af3b37d2 100644 --- a/contracts/escrow_fee_distributor/tests/integration.rs +++ b/contracts/escrow_fee_distributor/tests/integration.rs @@ -1,5 +1,5 @@ use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; -use cosmwasm_std::{attr, to_binary, Addr, StdResult, Timestamp, Uint128}; +use cosmwasm_std::{attr, to_json_binary, Addr, StdResult, Timestamp, Uint128}; use astroport_governance::utils::{get_period, EPOCH_START, WEEK}; @@ -115,7 +115,7 @@ fn test_receive_tokens() { .unwrap() .address .to_string(), - msg: to_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), amount: Uint128::from(100 * MULTIPLIER as u128), }; @@ -463,7 +463,7 @@ fn claim_max_period() { .unwrap() .address .to_string(), - msg: to_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), amount: Uint128::from(100 * MULTIPLIER as u128), }; @@ -497,7 +497,7 @@ fn claim_max_period() { .unwrap() .address .to_string(), - msg: to_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), amount: Uint128::from(100 * MULTIPLIER as u128), }; @@ -656,7 +656,7 @@ fn claim_multiple_users() { .unwrap() .address .to_string(), - msg: to_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), amount: Uint128::from(100 * MULTIPLIER as u128), }; @@ -812,7 +812,7 @@ fn claim_multiple_users() { .unwrap() .address .to_string(), - msg: to_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), amount: Uint128::from(900 * MULTIPLIER as u128), }; @@ -988,7 +988,7 @@ fn is_claim_enabled() { .unwrap() .address .to_string(), - msg: to_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), amount: Uint128::from(100 * MULTIPLIER as u128), }; @@ -1061,7 +1061,7 @@ fn is_claim_enabled() { .unwrap() .address .to_string(), - msg: to_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ReceiveTokens {}).unwrap(), amount: Uint128::from(100 * MULTIPLIER as u128), }; diff --git a/contracts/generator_controller/src/contract.rs b/contracts/generator_controller/src/contract.rs index b59cb9d1..7c4dfeea 100644 --- a/contracts/generator_controller/src/contract.rs +++ b/contracts/generator_controller/src/contract.rs @@ -6,8 +6,8 @@ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_ow #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, Fraction, MessageInfo, Order, - Response, StdError, StdResult, Uint128, WasmMsg, + to_json_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, Fraction, MessageInfo, + Order, Response, StdError, StdResult, Uint128, WasmMsg, }; use cw2::set_contract_version; use itertools::Itertools; @@ -505,7 +505,7 @@ fn tune_pools(deps: DepsMut, env: Env) -> ExecuteResult { // Set new alloc points let setup_pools_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: config.generator_addr.to_string(), - msg: to_binary(&astroport::generator::ExecuteMsg::SetupPools { + msg: to_json_binary(&astroport::generator::ExecuteMsg::SetupPools { pools: tune_info.pool_alloc_points, })?, funds: vec![], @@ -601,12 +601,12 @@ fn change_pools_limit(deps: DepsMut, info: MessageInfo, limit: u64) -> ExecuteRe #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::UserInfo { user } => to_binary(&user_info(deps, user)?), - QueryMsg::TuneInfo {} => to_binary(&TUNE_INFO.load(deps.storage)?), - QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), - QueryMsg::PoolInfo { pool_addr } => to_binary(&pool_info(deps, env, pool_addr, None)?), + QueryMsg::UserInfo { user } => to_json_binary(&user_info(deps, user)?), + QueryMsg::TuneInfo {} => to_json_binary(&TUNE_INFO.load(deps.storage)?), + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::PoolInfo { pool_addr } => to_json_binary(&pool_info(deps, env, pool_addr, None)?), QueryMsg::PoolInfoAtPeriod { pool_addr, period } => { - to_binary(&pool_info(deps, env, pool_addr, Some(period))?) + to_json_binary(&pool_info(deps, env, pool_addr, Some(period))?) } } } diff --git a/contracts/generator_controller_lite/src/contract.rs b/contracts/generator_controller_lite/src/contract.rs index 0814344c..3f89871a 100644 --- a/contracts/generator_controller_lite/src/contract.rs +++ b/contracts/generator_controller_lite/src/contract.rs @@ -10,7 +10,7 @@ use astroport_governance::astroport::asset::addr_opt_validate; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, Fraction, MessageInfo, Order, + to_json_binary, Binary, CosmosMsg, Decimal, Deps, DepsMut, Env, Fraction, MessageInfo, Order, Response, StdError, StdResult, Uint128, WasmMsg, }; use cw2::set_contract_version; @@ -750,7 +750,7 @@ fn tune_pools(deps: DepsMut, env: Env) -> ExecuteResult { // directly or via a governance proposal for Outposts let setup_pools_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: network_info.generator_address.to_string(), - msg: to_binary(&astroport::generator::ExecuteMsg::SetupPools { + msg: to_json_binary(&astroport::generator::ExecuteMsg::SetupPools { pools: pool_alloc_points.to_vec(), })?, funds: vec![], @@ -770,7 +770,7 @@ fn tune_pools(deps: DepsMut, env: Env) -> ExecuteResult { Some(ibc_channel) => { // We need to submit the setup pools message to the // Assembly as a proposal to execute on the remote chain - let proposal_msg = to_binary(&ExecuteEmissionsProposal { + let proposal_msg = to_json_binary(&ExecuteEmissionsProposal { title: format!( // Sample title: "Update emissions on the inj outpost", "Update emissions on the neutron outpost" "Update emissions on the {} outpost", @@ -900,12 +900,12 @@ fn change_pools_limit(deps: DepsMut, info: MessageInfo, limit: u64) -> ExecuteRe #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::UserInfo { user } => to_binary(&user_info(deps, user)?), - QueryMsg::TuneInfo {} => to_binary(&TUNE_INFO.load(deps.storage)?), - QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), - QueryMsg::PoolInfo { pool_addr } => to_binary(&pool_info(deps, env, pool_addr, None)?), + QueryMsg::UserInfo { user } => to_json_binary(&user_info(deps, user)?), + QueryMsg::TuneInfo {} => to_json_binary(&TUNE_INFO.load(deps.storage)?), + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::PoolInfo { pool_addr } => to_json_binary(&pool_info(deps, env, pool_addr, None)?), QueryMsg::PoolInfoAtPeriod { pool_addr, period } => { - to_binary(&pool_info(deps, env, pool_addr, Some(period))?) + to_json_binary(&pool_info(deps, env, pool_addr, Some(period))?) } } } diff --git a/contracts/hub/src/execute.rs b/contracts/hub/src/execute.rs index e0685ff1..343c6559 100644 --- a/contracts/hub/src/execute.rs +++ b/contracts/hub/src/execute.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - entry_point, from_binary, to_binary, Addr, DepsMut, Env, MessageInfo, Response, StdError, + entry_point, from_json, to_json_binary, Addr, DepsMut, Env, MessageInfo, Response, StdError, StdResult, SubMsg, WasmMsg, }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; @@ -136,7 +136,7 @@ fn receive_cw20( } // Match the CW20 template - match from_binary(&cw20_msg.msg)? { + match from_json(&cw20_msg.msg)? { Cw20HookMsg::OutpostMemo { channel, sender, @@ -226,13 +226,13 @@ fn handle_stake_instruction( let send_msg = Cw20ExecuteMsg::Send { contract: config.staking_addr.to_string(), amount: msg.amount, - msg: to_binary(&enter_msg)?, + msg: to_json_binary(&enter_msg)?, }; // Execute the message, we're using a CW20, so no funds added here let stake_msg = WasmMsg::Execute { contract_addr: config.token_addr.to_string(), - msg: to_binary(&send_msg)?, + msg: to_json_binary(&send_msg)?, funds: vec![], }; @@ -466,7 +466,7 @@ mod tests { assert_eq!( outposts, - to_binary(&vec![astroport_governance::hub::OutpostConfig { + to_json_binary(&vec![astroport_governance::hub::OutpostConfig { address: "wasm1contractaddress1".to_string(), channel: "channel-2".to_string(), cw20_ics20_channel: "channel-1".to_string(), @@ -487,7 +487,7 @@ mod tests { assert_eq!( outposts, - to_binary(&vec![astroport_governance::hub::OutpostConfig { + to_json_binary(&vec![astroport_governance::hub::OutpostConfig { address: "wasm1contractaddress2".to_string(), channel: "channel-2".to_string(), cw20_ics20_channel: "channel-2".to_string(), @@ -508,7 +508,7 @@ mod tests { assert_eq!( outposts, - to_binary(&vec![ + to_json_binary(&vec![ astroport_governance::hub::OutpostConfig { address: "wasm1contractaddress1".to_string(), channel: "channel-2".to_string(), @@ -545,7 +545,7 @@ mod tests { assert_eq!( outposts, - to_binary(&vec![astroport_governance::hub::OutpostConfig { + to_json_binary(&vec![astroport_governance::hub::OutpostConfig { address: "wasm1contractaddress2".to_string(), channel: "channel-2".to_string(), cw20_ics20_channel: "channel-2".to_string(), @@ -635,7 +635,7 @@ mod tests { // Ensure the config set during instantiation is correct assert_eq!( config, - to_binary(&astroport_governance::hub::Config { + to_json_binary(&astroport_governance::hub::Config { owner: Addr::unchecked(OWNER), assembly_addr: Addr::unchecked(ASSEMBLY), cw20_ics20_addr: Addr::unchecked(CW20ICS20), @@ -706,7 +706,7 @@ mod tests { assert_eq!( config, - to_binary(&astroport_governance::hub::Config { + to_json_binary(&astroport_governance::hub::Config { owner: Addr::unchecked(OWNER), assembly_addr: Addr::unchecked(ASSEMBLY), cw20_ics20_addr: Addr::unchecked(CW20ICS20), @@ -791,7 +791,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: "not_cw20_ics20".to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { receiver: user1.to_owned(), }) .unwrap(), @@ -809,7 +809,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { receiver: user1.to_owned(), }) .unwrap(), @@ -827,7 +827,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: Uint128::zero(), - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { receiver: user1.to_owned(), }) .unwrap(), @@ -845,7 +845,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { receiver: user1.to_owned(), }) .unwrap(), @@ -865,7 +865,7 @@ mod tests { assert_eq!( balance, - to_binary(&HubBalance { + to_json_binary(&HubBalance { balance: user1_funds }) .unwrap() @@ -878,7 +878,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user2_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { receiver: user2.to_owned(), }) .unwrap(), @@ -949,7 +949,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: MOCK_CONTRACT_ADDR.to_string(), @@ -970,7 +970,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: MOCK_CONTRACT_ADDR.to_string(), @@ -991,7 +991,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: Uint128::zero(), - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: MOCK_CONTRACT_ADDR.to_string(), @@ -1058,7 +1058,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: MOCK_CONTRACT_ADDR.to_string(), @@ -1084,7 +1084,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: MOCK_CONTRACT_ADDR.to_string(), @@ -1154,7 +1154,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: receiving_user.to_string(), @@ -1219,7 +1219,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: MOCK_CONTRACT_ADDR.to_owned(), @@ -1231,8 +1231,8 @@ mod tests { .unwrap(); // Verify that the stake message matches the expected message - let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); - let send_msg = to_binary(&Cw20ExecuteMsg::Send { + let stake_msg = to_json_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_json_binary(&Cw20ExecuteMsg::Send { contract: STAKING.to_string(), amount: user1_funds, msg: stake_msg, @@ -1271,7 +1271,7 @@ mod tests { assert_eq!(res.messages.len(), 1); // Once staked, we mint the xASTRO on the remote chain - let mint_msg = to_binary(&Outpost::MintXAstro { + let mint_msg = to_json_binary(&Outpost::MintXAstro { receiver: user1.to_string(), amount: user1_funds, }) @@ -1308,7 +1308,7 @@ mod tests { balance: user1_funds, }; - assert_eq!(balances, to_binary(&expected).unwrap()); + assert_eq!(balances, to_json_binary(&expected).unwrap()); // At this point the total channel balance must have a balance that matches the amount let total_balance = query( @@ -1324,7 +1324,7 @@ mod tests { balance: user1_funds, }; - assert_eq!(total_balance, to_binary(&total_expected).unwrap()); + assert_eq!(total_balance, to_json_binary(&total_expected).unwrap()); } // Test Cases: @@ -1379,7 +1379,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: MOCK_CONTRACT_ADDR.to_owned(), @@ -1391,8 +1391,8 @@ mod tests { .unwrap(); // Verify that the stake message matches the expected message - let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); - let send_msg = to_binary(&Cw20ExecuteMsg::Send { + let stake_msg = to_json_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_json_binary(&Cw20ExecuteMsg::Send { contract: STAKING.to_string(), amount: user1_funds, msg: stake_msg, @@ -1445,7 +1445,7 @@ mod tests { balance: user1_funds, }; - assert_eq!(balances, to_binary(&expected).unwrap()); + assert_eq!(balances, to_json_binary(&expected).unwrap()); // At this point the total channel balance must have a balance that matches the amount let total_balance = query( @@ -1461,10 +1461,10 @@ mod tests { balance: user1_funds, }; - assert_eq!(total_balance, to_binary(&total_expected).unwrap()); + assert_eq!(total_balance, to_json_binary(&total_expected).unwrap()); // Trigger a timeout on minting xASTRO remotely - let mint_msg = to_binary(&Outpost::MintXAstro { + let mint_msg = to_json_binary(&Outpost::MintXAstro { receiver: user1.to_owned(), amount: user1_funds, }) @@ -1491,8 +1491,8 @@ mod tests { assert_eq!(res.messages.len(), 1); // Verify that the unstake message matches the expected message - let unstake_msg = to_binary(&astroport::staking::Cw20HookMsg::Leave {}).unwrap(); - let send_msg = to_binary(&Cw20ExecuteMsg::Send { + let unstake_msg = to_json_binary(&astroport::staking::Cw20HookMsg::Leave {}).unwrap(); + let send_msg = to_json_binary(&Cw20ExecuteMsg::Send { contract: STAKING.to_string(), amount: user1_funds, msg: unstake_msg, @@ -1530,7 +1530,7 @@ mod tests { balance: user1_funds, }; - assert_eq!(balances, to_binary(&expected).unwrap()); + assert_eq!(balances, to_json_binary(&expected).unwrap()); // And the total must still match let total_balance = query( @@ -1546,7 +1546,7 @@ mod tests { balance: user1_funds, }; - assert_eq!(total_balance, to_binary(&total_expected).unwrap()); + assert_eq!(total_balance, to_json_binary(&total_expected).unwrap()); // Construct the reply from the staking contract that will be returned // to the contract @@ -1579,7 +1579,7 @@ mod tests { balance: Uint128::zero(), }; - assert_eq!(balances, to_binary(&expected).unwrap()); + assert_eq!(balances, to_json_binary(&expected).unwrap()); // And now it shoul be zero let total_balance = query( @@ -1595,7 +1595,7 @@ mod tests { balance: Uint128::zero(), }; - assert_eq!(total_balance, to_binary(&total_expected).unwrap()); + assert_eq!(total_balance, to_json_binary(&total_expected).unwrap()); // The rest of the unstaking flow is covered in ibc_staking tests } diff --git a/contracts/hub/src/ibc.rs b/contracts/hub/src/ibc.rs index fcbf9883..492049b3 100644 --- a/contracts/hub/src/ibc.rs +++ b/contracts/hub/src/ibc.rs @@ -1,6 +1,6 @@ use astroport::querier::query_token_balance; use cosmwasm_std::{ - entry_point, from_binary, to_binary, Deps, DepsMut, Env, Ibc3ChannelOpenResponse, + entry_point, from_json, to_json_binary, Deps, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse, IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Never, StdError, StdResult, SubMsg, @@ -107,7 +107,7 @@ pub fn ibc_packet_receive( ) -> Result { do_packet_receive(deps, env, msg).or_else(|err| { // Construct an error acknowledgement that can be handled on the Outpost - let ack_data = to_binary(&Response::new_error(err.to_string())).unwrap(); + let ack_data = to_json_binary(&Response::new_error(err.to_string())).unwrap(); Ok(IbcReceiveResponse::new() .add_attribute("action", "ibc_packet_receive") @@ -135,7 +135,7 @@ fn do_packet_receive( )?; // Parse the packet data into a Hub message - let outpost_msg: Hub = from_binary(&msg.packet.data)?; + let outpost_msg: Hub = from_json(&msg.packet.data)?; match outpost_msg { Hub::QueryProposal { id } => handle_ibc_query_proposal(deps, id), Hub::CastAssemblyVote { @@ -187,7 +187,7 @@ pub fn ibc_packet_timeout( env: Env, msg: IbcPacketTimeoutMsg, ) -> Result { - let failed_msg: Outpost = from_binary(&msg.packet.data)?; + let failed_msg: Outpost = from_json(&msg.packet.data)?; match failed_msg { Outpost::MintXAstro { receiver, amount } => { let config = CONFIG.load(deps.storage)?; @@ -490,14 +490,14 @@ mod tests { // The Hub doesn't do anything with acks, we just check that // it doesn't fail let ack = IbcAcknowledgement::new( - to_binary(&Response::Result { + to_json_binary(&Response::Result { action: None, address: None, error: None, }) .unwrap(), ); - let mint_msg = to_binary(&Outpost::MintXAstro { + let mint_msg = to_json_binary(&Outpost::MintXAstro { receiver: "user".to_owned(), amount: Uint128::one(), }) @@ -626,7 +626,7 @@ mod tests { // We don't need to test every type of Hub message as the safety check // happens in do_packet_receive which is the entrypoint for all messages // being received - let ibc_unstake_msg = to_binary(&Hub::Unstake { + let ibc_unstake_msg = to_json_binary(&Hub::Unstake { receiver: "unstaker".to_string(), amount: Uint128::from(100u128), }) @@ -635,7 +635,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); - let ack: Response = from_binary(&res.acknowledgement).unwrap(); + let ack: Response = from_json(&res.acknowledgement).unwrap(); match ack { Response::Result { error, .. } => { assert!( @@ -662,7 +662,7 @@ mod tests { let recv_packet = mock_ibc_packet("channel-55", ibc_unstake_msg.clone()); let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); - let ack: Response = from_binary(&res.acknowledgement).unwrap(); + let ack: Response = from_json(&res.acknowledgement).unwrap(); match ack { Response::Result { error, .. } => { assert!(error == Some("Unauthorized".to_string())); diff --git a/contracts/hub/src/ibc_governance.rs b/contracts/hub/src/ibc_governance.rs index 29b6e0cd..d8a1aa64 100644 --- a/contracts/hub/src/ibc_governance.rs +++ b/contracts/hub/src/ibc_governance.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_binary, Addr, DepsMut, Env, IbcReceiveResponse, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, DepsMut, Env, IbcReceiveResponse, Uint128, WasmMsg}; use astroport_governance::{ assembly::{Proposal, ProposalVoteOption}, @@ -36,7 +36,7 @@ pub fn handle_ibc_cast_assembly_vote( }; let wasm_msg = WasmMsg::Execute { contract_addr: config.assembly_addr.to_string(), - msg: to_binary(&vote_msg)?, + msg: to_json_binary(&vote_msg)?, funds: vec![], }; @@ -54,7 +54,7 @@ pub fn handle_ibc_cast_assembly_vote( } // If the vote succeeds, the ack will be sent back to the Outpost - let ack_data = to_binary(&Response::new_success( + let ack_data = to_json_binary(&Response::new_success( "cast_assembly_vote".to_owned(), voter.to_string(), ))?; @@ -89,7 +89,7 @@ pub fn handle_ibc_cast_emissions_vote( }; let msg = WasmMsg::Execute { contract_addr: config.generator_controller_addr.to_string(), - msg: to_binary(&vote_msg)?, + msg: to_json_binary(&vote_msg)?, funds: vec![], }; @@ -101,7 +101,7 @@ pub fn handle_ibc_cast_emissions_vote( } // If the vote succeeds, the ack will be sent back to the Outpost - let ack_data = to_binary(&Response::new_success( + let ack_data = to_json_binary(&Response::new_success( "cast_emissions_vote".to_owned(), voter.to_string(), ))?; @@ -122,12 +122,12 @@ pub fn handle_ibc_unlock(deps: DepsMut, user: Addr) -> Result { assert_eq!( @@ -322,7 +322,7 @@ mod tests { } // Attempt a vote with the correct voting power - let ibc_vote = to_binary(&Hub::CastAssemblyVote { + let ibc_vote = to_json_binary(&Hub::CastAssemblyVote { proposal_id, voter: Addr::unchecked(voter), vote_option, @@ -334,7 +334,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); - let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + let hub_respone: Response = from_json(&res.acknowledgement).unwrap(); match hub_respone { Response::Result { error, .. } => { assert!(error.is_none()); @@ -344,7 +344,7 @@ mod tests { assert_eq!(res.messages.len(), 1); - let assembly_msg = to_binary( + let assembly_msg = to_json_binary( &astroport_governance::assembly::ExecuteMsg::CastOutpostVote { proposal_id, vote: ProposalVoteOption::For, @@ -417,7 +417,7 @@ mod tests { .unwrap(); // Voting must fail if the channel balance in insufficient - let ibc_unstake = to_binary(&Hub::CastEmissionsVote { + let ibc_unstake = to_json_binary(&Hub::CastEmissionsVote { voter: Addr::unchecked(voter), voting_power, votes: votes.clone(), @@ -428,7 +428,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); - let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + let hub_respone: Response = from_json(&res.acknowledgement).unwrap(); match hub_respone { Response::Result { error, .. } => { assert_eq!( @@ -450,7 +450,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: user1_funds, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: user1.to_string(), receiver: MOCK_CONTRACT_ADDR.to_owned(), @@ -462,8 +462,8 @@ mod tests { .unwrap(); // Verify that the stake message matches the expected message - let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); - let send_msg = to_binary(&Cw20ExecuteMsg::Send { + let stake_msg = to_json_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_json_binary(&Cw20ExecuteMsg::Send { contract: STAKING.to_string(), amount: user1_funds, msg: stake_msg, @@ -501,7 +501,7 @@ mod tests { // We must have one IBC message assert_eq!(res.messages.len(), 1); - let ibc_vote = to_binary(&Hub::CastEmissionsVote { + let ibc_vote = to_json_binary(&Hub::CastEmissionsVote { voter: Addr::unchecked(voter), voting_power, votes: votes.clone(), @@ -512,7 +512,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); - let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + let hub_respone: Response = from_json(&res.acknowledgement).unwrap(); match hub_respone { Response::Result { error, .. } => { assert!(error.is_none(),); @@ -522,7 +522,7 @@ mod tests { assert_eq!(res.messages.len(), 1); - let generator_controller_msg = to_binary( + let generator_controller_msg = to_json_binary( &astroport_governance::generator_controller_lite::ExecuteMsg::OutpostVote { voter: voter.to_string(), voting_power, @@ -592,7 +592,7 @@ mod tests { .unwrap(); // Kick the voter - let ibc_kick_unlocked = to_binary(&Hub::KickUnlockedVoter { + let ibc_kick_unlocked = to_json_binary(&Hub::KickUnlockedVoter { voter: Addr::unchecked(voter), }) .unwrap(); @@ -601,7 +601,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); - let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + let hub_respone: Response = from_json(&res.acknowledgement).unwrap(); match hub_respone { Response::Result { error, .. } => { assert!(error.is_none()); @@ -613,7 +613,7 @@ mod tests { assert_eq!(res.messages.len(), 1); // Verify that the message matches the expected message - let controller_msg = to_binary( + let controller_msg = to_json_binary( &astroport_governance::generator_controller_lite::ExecuteMsg::KickUnlockedOutpostVoter { unlocked_voter:voter.to_string(), }, @@ -681,7 +681,7 @@ mod tests { .unwrap(); // Kick the voter - let ibc_kick_blacklisted = to_binary(&Hub::KickBlacklistedVoter { + let ibc_kick_blacklisted = to_json_binary(&Hub::KickBlacklistedVoter { voter: Addr::unchecked(voter), }) .unwrap(); @@ -690,7 +690,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); - let hub_respone: Response = from_binary(&res.acknowledgement).unwrap(); + let hub_respone: Response = from_json(&res.acknowledgement).unwrap(); match hub_respone { Response::Result { error, .. } => { assert!(error.is_none()); @@ -702,7 +702,7 @@ mod tests { assert_eq!(res.messages.len(), 1); // Verify that the message matches the expected message - let controller_msg = to_binary( + let controller_msg = to_json_binary( &astroport_governance::generator_controller_lite::ExecuteMsg::KickBlacklistedVoters { blacklisted_voters: vec![voter.to_string()], }, diff --git a/contracts/hub/src/ibc_misc.rs b/contracts/hub/src/ibc_misc.rs index 1f933e27..8d2e67e7 100644 --- a/contracts/hub/src/ibc_misc.rs +++ b/contracts/hub/src/ibc_misc.rs @@ -1,5 +1,5 @@ use astroport::cw20_ics20::TransferMsg; -use cosmwasm_std::{to_binary, Addr, DepsMut, IbcReceiveResponse, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, DepsMut, IbcReceiveResponse, WasmMsg}; use cw20::Cw20ExecuteMsg; use astroport_governance::interchain::Response; @@ -48,18 +48,18 @@ pub fn handle_ibc_withdraw_stuck_funds( let send_msg = Cw20ExecuteMsg::Send { contract: config.cw20_ics20_addr.to_string(), amount: balance, - msg: to_binary(&transfer_msg)?, + msg: to_json_binary(&transfer_msg)?, }; let msg = WasmMsg::Execute { contract_addr: config.token_addr.to_string(), - msg: to_binary(&send_msg)?, + msg: to_json_binary(&send_msg)?, funds: vec![], }; // This acknowledgement only indicates that the withdraw was processed without // error, not that the funds were successfully transferred over IBC to the user - let ack_data = to_binary(&Response::new_success( + let ack_data = to_json_binary(&Response::new_success( "withdraw_funds".to_owned(), user.to_string(), ))?; @@ -76,7 +76,7 @@ mod tests { use super::*; use astroport_governance::interchain::{self, Hub}; use cosmwasm_std::{ - from_binary, testing::mock_info, IbcPacketReceiveMsg, ReplyOn, SubMsg, Uint128, + from_json, testing::mock_info, IbcPacketReceiveMsg, ReplyOn, SubMsg, Uint128, }; use cw20::Cw20ReceiveMsg; @@ -146,7 +146,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: stuck_amount, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::TransferFailure { receiver: user.to_owned(), }) .unwrap(), @@ -155,7 +155,7 @@ mod tests { .unwrap(); // Withdraw must fail if the user has no funds stuck - let ibc_withdraw = to_binary(&Hub::WithdrawFunds { + let ibc_withdraw = to_json_binary(&Hub::WithdrawFunds { user: Addr::unchecked("not_user"), }) .unwrap(); @@ -163,7 +163,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); - let hub_respone: interchain::Response = from_binary(&res.acknowledgement).unwrap(); + let hub_respone: interchain::Response = from_json(&res.acknowledgement).unwrap(); match hub_respone { interchain::Response::Result { error, .. } => { assert!(error.is_some()); @@ -176,7 +176,7 @@ mod tests { } // Our user has funds stuck, so withdrawal must succeed - let ibc_withdraw = to_binary(&Hub::WithdrawFunds { + let ibc_withdraw = to_json_binary(&Hub::WithdrawFunds { user: Addr::unchecked(user), }) .unwrap(); @@ -185,7 +185,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); - let hub_respone: interchain::Response = from_binary(&res.acknowledgement).unwrap(); + let hub_respone: interchain::Response = from_json(&res.acknowledgement).unwrap(); match hub_respone { interchain::Response::Result { address, error, .. } => { assert!(error.is_none()); @@ -198,14 +198,14 @@ mod tests { assert_eq!(res.messages.len(), 1); // It must be a CW20-ICS20 transfer message - let ibc_transfer_msg = to_binary(&TransferMsg { + let ibc_transfer_msg = to_json_binary(&TransferMsg { remote_address: user.to_string(), channel: "channel-1".to_string(), timeout: Some(10), memo: None, }) .unwrap(); - let cw_send_msg = to_binary(&Cw20ExecuteMsg::Send { + let cw_send_msg = to_json_binary(&Cw20ExecuteMsg::Send { contract: CW20ICS20.to_string(), amount: stuck_amount, msg: ibc_transfer_msg, diff --git a/contracts/hub/src/ibc_query.rs b/contracts/hub/src/ibc_query.rs index 1010bc3b..47c3a8a3 100644 --- a/contracts/hub/src/ibc_query.rs +++ b/contracts/hub/src/ibc_query.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_binary, DepsMut, IbcReceiveResponse}; +use cosmwasm_std::{to_json_binary, DepsMut, IbcReceiveResponse}; use astroport_governance::{ assembly::Proposal, @@ -29,7 +29,7 @@ pub fn handle_ibc_query_proposal( start_time: proposal.start_time, }; - let ack_data = to_binary(&Response::QueryProposal(proposal_snapshot))?; + let ack_data = to_json_binary(&Response::QueryProposal(proposal_snapshot))?; Ok(IbcReceiveResponse::new() .set_ack(ack_data) .add_attribute("query", "proposal") @@ -40,7 +40,7 @@ pub fn handle_ibc_query_proposal( mod tests { use super::*; use astroport_governance::interchain::Hub; - use cosmwasm_std::{from_binary, testing::mock_info, Addr, IbcPacketReceiveMsg, Uint64}; + use cosmwasm_std::{from_json, testing::mock_info, Addr, IbcPacketReceiveMsg, Uint64}; use crate::{ contract::instantiate, @@ -92,13 +92,13 @@ mod tests { ) .unwrap(); - let ibc_query_proposal = to_binary(&Hub::QueryProposal { id: 1 }).unwrap(); + let ibc_query_proposal = to_json_binary(&Hub::QueryProposal { id: 1 }).unwrap(); let recv_packet = mock_ibc_packet("channel-3", ibc_query_proposal); let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); - let ack: Response = from_binary(&res.acknowledgement).unwrap(); + let ack: Response = from_json(&res.acknowledgement).unwrap(); match ack { Response::Result { error, .. } => { assert!(error.is_some()); @@ -150,13 +150,13 @@ mod tests { ) .unwrap(); - let ibc_query_proposal = to_binary(&Hub::QueryProposal { id: 1 }).unwrap(); + let ibc_query_proposal = to_json_binary(&Hub::QueryProposal { id: 1 }).unwrap(); let recv_packet = mock_ibc_packet("channel-3", ibc_query_proposal); let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); - let ack: Response = from_binary(&res.acknowledgement).unwrap(); + let ack: Response = from_json(&res.acknowledgement).unwrap(); match ack { Response::QueryProposal(proposal) => { assert_eq!(proposal.id, Uint64::from(1u64)); diff --git a/contracts/hub/src/ibc_staking.rs b/contracts/hub/src/ibc_staking.rs index d74b01f4..74b3f24d 100644 --- a/contracts/hub/src/ibc_staking.rs +++ b/contracts/hub/src/ibc_staking.rs @@ -1,6 +1,7 @@ use astroport::querier::query_token_balance; use cosmwasm_std::{ - to_binary, DepsMut, Env, IbcReceiveResponse, QuerierWrapper, Storage, SubMsg, Uint128, WasmMsg, + to_json_binary, DepsMut, Env, IbcReceiveResponse, QuerierWrapper, Storage, SubMsg, Uint128, + WasmMsg, }; use cw20::Cw20ExecuteMsg; @@ -36,7 +37,7 @@ pub fn handle_ibc_unstake( // Set the acknowledgement. This is only to indicate that the unstake // was processed without error, not that the funds were successfully - let ack_data = to_binary(&Response::new_success("unstake".to_owned(), receiver))?; + let ack_data = to_json_binary(&Response::new_success("unstake".to_owned(), receiver))?; Ok(IbcReceiveResponse::new() .set_ack(ack_data) @@ -61,13 +62,13 @@ pub fn construct_unstake_msg( let send_msg = Cw20ExecuteMsg::Send { contract: config.staking_addr.to_string(), amount, - msg: to_binary(&leave_msg)?, + msg: to_json_binary(&leave_msg)?, }; // Send the xASTRO held in the contract to the Staking contract let msg = WasmMsg::Execute { contract_addr: config.xtoken_addr.to_string(), - msg: to_binary(&send_msg)?, + msg: to_json_binary(&send_msg)?, funds: vec![], }; @@ -96,7 +97,7 @@ mod tests { use astroport::cw20_ics20::TransferMsg; use astroport_governance::{hub::HubBalance, interchain::Hub}; use cosmwasm_std::{ - from_binary, + from_json, testing::{mock_info, MOCK_CONTRACT_ADDR}, Addr, IbcPacketReceiveMsg, Reply, ReplyOn, SubMsgResponse, SubMsgResult, Uint64, }; @@ -165,7 +166,7 @@ mod tests { astroport_governance::hub::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: CW20ICS20.to_string(), amount: unstake_amount, - msg: to_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { + msg: to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { channel: "channel-1".to_string(), sender: unstaker.to_string(), receiver: MOCK_CONTRACT_ADDR.to_owned(), @@ -177,8 +178,8 @@ mod tests { .unwrap(); // Verify that the stake message matches the expected message - let stake_msg = to_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); - let send_msg = to_binary(&Cw20ExecuteMsg::Send { + let stake_msg = to_json_binary(&astroport::staking::Cw20HookMsg::Enter {}).unwrap(); + let send_msg = to_json_binary(&Cw20ExecuteMsg::Send { contract: STAKING.to_string(), amount: unstake_amount, msg: stake_msg, @@ -216,7 +217,7 @@ mod tests { // We must have one IBC message assert_eq!(res.messages.len(), 1); - let ibc_unstake = to_binary(&Hub::Unstake { + let ibc_unstake = to_json_binary(&Hub::Unstake { receiver: unstaker.to_owned(), amount: unstake_amount, }) @@ -226,7 +227,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); - let ack: Response = from_binary(&res.acknowledgement).unwrap(); + let ack: Response = from_json(&res.acknowledgement).unwrap(); match ack { Response::Result { error, .. } => { assert!(error.is_none()); @@ -238,8 +239,8 @@ mod tests { assert_eq!(res.messages.len(), 1); // Verify that the unstake message matches the expected message - let unstake_msg = to_binary(&astroport::staking::Cw20HookMsg::Leave {}).unwrap(); - let send_msg = to_binary(&Cw20ExecuteMsg::Send { + let unstake_msg = to_json_binary(&astroport::staking::Cw20HookMsg::Leave {}).unwrap(); + let send_msg = to_json_binary(&Cw20ExecuteMsg::Send { contract: STAKING.to_string(), amount: unstake_amount, msg: unstake_msg, @@ -278,14 +279,14 @@ mod tests { assert_eq!(res.messages.len(), 1); // Contruct the CW20-ICS20 ASTRO token transfer we expect to see - let transfer_msg = to_binary(&TransferMsg { + let transfer_msg = to_json_binary(&TransferMsg { channel: "channel-1".to_string(), remote_address: unstaker.to_string(), timeout: Some(10), memo: None, }) .unwrap(); - let send_msg = to_binary(&Cw20ExecuteMsg::Send { + let send_msg = to_json_binary(&Cw20ExecuteMsg::Send { contract: CW20ICS20.to_string(), amount: unstake_amount, msg: transfer_msg, @@ -324,6 +325,6 @@ mod tests { balance: Uint128::zero(), }; - assert_eq!(balances, to_binary(&expected).unwrap()); + assert_eq!(balances, to_json_binary(&expected).unwrap()); } } diff --git a/contracts/hub/src/mock.rs b/contracts/hub/src/mock.rs index b5ae25a9..372b16b2 100644 --- a/contracts/hub/src/mock.rs +++ b/contracts/hub/src/mock.rs @@ -1,17 +1,17 @@ use std::cell::Cell; #[cfg(test)] -use cosmwasm_std::{from_binary, Uint64}; +use cosmwasm_std::{from_json, Uint64}; use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - to_binary, Addr, Binary, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, - IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, ListChannelsResponse, MessageInfo, OwnedDeps, - Timestamp, Uint128, + to_json_binary, Addr, Binary, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, + IbcChannelOpenMsg, IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, ListChannelsResponse, + MessageInfo, OwnedDeps, Timestamp, Uint128, }; use cosmwasm_std::testing::MOCK_CONTRACT_ADDR; use cosmwasm_std::{ - from_slice, Empty, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, + Empty, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, }; use cw20::BalanceResponse as Cw20BalanceResponse; @@ -54,7 +54,7 @@ pub struct WasmMockQuerier { impl Querier for WasmMockQuerier { fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { // MockQuerier doesn't support Custom, so we ignore it completely - let request: QueryRequest = match from_slice(bin_request) { + let request: QueryRequest = match from_json(bin_request) { Ok(v) => v, Err(e) => { return SystemResult::Err(SystemError::InvalidRequest { @@ -72,14 +72,14 @@ impl WasmMockQuerier { match &request { QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { if contract_addr == STAKING { - match from_binary(msg).unwrap() { + match from_json(msg).unwrap() { astroport::staking::QueryMsg::Config {} => { let config = astroport::staking::ConfigResponse { deposit_token_addr: Addr::unchecked("astro"), share_token_addr: Addr::unchecked("xastro"), }; - SystemResult::Ok(to_binary(&config).into()) + SystemResult::Ok(to_json_binary(&config).into()) } _ => { panic!("DO NOT ENTER HERE") @@ -98,7 +98,7 @@ impl WasmMockQuerier { .checked_add(Uint128::from(100u128)) .unwrap(), ); - return SystemResult::Ok(to_binary(&response).into()); + return SystemResult::Ok(to_json_binary(&response).into()); } if contract_addr == XASTRO_TOKEN { // Manually increase the ASTRO balance every query @@ -112,12 +112,12 @@ impl WasmMockQuerier { .checked_add(Uint128::from(100u128)) .unwrap(), ); - return SystemResult::Ok(to_binary(&response).into()); + return SystemResult::Ok(to_json_binary(&response).into()); } if contract_addr != ASSEMBLY { return SystemResult::Err(SystemError::Unknown {}); } - match from_binary(msg).unwrap() { + match from_json(msg).unwrap() { astroport_governance::assembly::QueryMsg::Proposal { proposal_id } => { let proposal = astroport_governance::assembly::Proposal { proposal_id: Uint64::from(proposal_id), @@ -127,8 +127,6 @@ impl WasmMockQuerier { outpost_against_power: Uint128::zero(), against_power: Uint128::zero(), outpost_for_power: Uint128::zero(), - for_voters: vec![], - against_voters: vec![], start_block: 1, start_time: 1571797419, end_block: 5, @@ -137,11 +135,11 @@ impl WasmMockQuerier { title: "Test title".to_string(), description: "Test description".to_string(), link: None, - messages: None, + messages: vec![], deposit_amount: Uint128::one(), ibc_channel: None, }; - SystemResult::Ok(to_binary(&proposal).into()) + SystemResult::Ok(to_json_binary(&proposal).into()) } _ => { panic!("DO NOT ENTER HERE") @@ -206,7 +204,7 @@ impl WasmMockQuerier { ), ], }; - SystemResult::Ok(to_binary(&response).into()) + SystemResult::Ok(to_json_binary(&response).into()) // if contract_addr != "cw20_ics20" { // return SystemResult::Err(SystemError::Unknown {}); // } diff --git a/contracts/hub/src/query.rs b/contracts/hub/src/query.rs index 3474f837..f85be3d7 100644 --- a/contracts/hub/src/query.rs +++ b/contracts/hub/src/query.rs @@ -1,6 +1,6 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{to_binary, Addr, Binary, Deps, Env, Order, StdResult, Uint128}; +use cosmwasm_std::{to_json_binary, Addr, Binary, Deps, Env, Order, StdResult, Uint128}; use cw_storage_plus::Bound; use crate::state::{channel_balance_at, total_balance_at, CONFIG, OUTPOSTS, USER_FUNDS}; @@ -24,13 +24,13 @@ use astroport_governance::{ #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), QueryMsg::UserFunds { user } => query_user_funds(deps, user), QueryMsg::Outposts { start_after, limit } => query_outposts(deps, start_after, limit), - QueryMsg::ChannelBalanceAt { channel, timestamp } => to_binary(&HubBalance { + QueryMsg::ChannelBalanceAt { channel, timestamp } => to_json_binary(&HubBalance { balance: channel_balance_at(deps.storage, &channel, timestamp.u64())?, }), - QueryMsg::TotalChannelBalancesAt { timestamp } => to_binary(&HubBalance { + QueryMsg::TotalChannelBalancesAt { timestamp } => to_json_binary(&HubBalance { balance: total_balance_at(deps.storage, timestamp.u64())?, }), } @@ -58,7 +58,7 @@ fn query_outposts( } }) .collect(); - to_binary(&outposts) + to_json_binary(&outposts) } /// Return the amount of ASTRO this address has held on the Hub due to IBC @@ -68,5 +68,5 @@ fn query_user_funds(deps: Deps, user: Addr) -> StdResult { .load(deps.storage, &user) .unwrap_or(Uint128::zero()); - to_binary(&HubBalance { balance: funds }) + to_json_binary(&HubBalance { balance: funds }) } diff --git a/contracts/hub/src/reply.rs b/contracts/hub/src/reply.rs index e03307f8..966611d8 100644 --- a/contracts/hub/src/reply.rs +++ b/contracts/hub/src/reply.rs @@ -1,6 +1,7 @@ use astroport::{cw20_ics20::TransferMsg, querier::query_token_balance}; use cosmwasm_std::{ - entry_point, to_binary, CosmosMsg, DepsMut, Env, IbcMsg, Reply, Response, SubMsgResult, WasmMsg, + entry_point, to_json_binary, CosmosMsg, DepsMut, Env, IbcMsg, Reply, Response, SubMsgResult, + WasmMsg, }; use cw20::Cw20ExecuteMsg; @@ -61,7 +62,7 @@ fn handle_stake_reply(deps: DepsMut, env: Env, reply: Reply) -> Result Result execute_remote_unstake(deps, env, cw20_msg), } } @@ -154,7 +154,7 @@ fn execute_remote_unstake( let burn_msg = Cw20ExecuteMsg::Burn { amount: msg.amount }; let wasm_msg = WasmMsg::Execute { contract_addr: config.xastro_token_addr.to_string(), - msg: to_binary(&burn_msg)?, + msg: to_json_binary(&burn_msg)?, funds: vec![], }; @@ -169,7 +169,7 @@ fn execute_remote_unstake( }; let hub_unstake_msg: CosmosMsg = CosmosMsg::Ibc(IbcMsg::SendPacket { channel_id: hub_channel.clone(), - data: to_binary(&unstake)?, + data: to_json_binary(&unstake)?, timeout: env .block .time @@ -277,7 +277,7 @@ fn cast_assembly_vote( }; let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { channel_id: hub_channel, - data: to_binary(&cast_vote)?, + data: to_json_binary(&cast_vote)?, timeout: env .block .time @@ -321,7 +321,7 @@ fn cast_assembly_vote( let query_proposal = Hub::QueryProposal { id: proposal_id }; let hub_query_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { channel_id: hub_channel, - data: to_binary(&query_proposal)?, + data: to_json_binary(&query_proposal)?, timeout: env .block .time @@ -369,7 +369,7 @@ fn cast_emissions_vote( }; let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { channel_id: hub_channel, - data: to_binary(&cast_vote)?, + data: to_json_binary(&cast_vote)?, timeout: env .block .time @@ -410,7 +410,7 @@ fn kick_unlocked( }; let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { channel_id: hub_channel, - data: to_binary(&kick_unlocked)?, + data: to_json_binary(&kick_unlocked)?, timeout: env .block .time @@ -451,7 +451,7 @@ fn kick_blacklisted( }; let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { channel_id: hub_channel, - data: to_binary(&kick_blacklisted)?, + data: to_json_binary(&kick_blacklisted)?, timeout: env .block .time @@ -486,7 +486,7 @@ fn withdraw_hub_funds( }; let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { channel_id: hub_channel, - data: to_binary(&withdraw)?, + data: to_json_binary(&withdraw)?, timeout: env .block .time @@ -570,7 +570,8 @@ mod tests { astroport_governance::outpost::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: user.to_string(), amount: user_funds, - msg: to_binary(&astroport_governance::outpost::Cw20HookMsg::Unstake {}).unwrap(), + msg: to_json_binary(&astroport_governance::outpost::Cw20HookMsg::Unstake {}) + .unwrap(), }), ) .unwrap_err(); @@ -585,13 +586,14 @@ mod tests { astroport_governance::outpost::ExecuteMsg::Receive(Cw20ReceiveMsg { sender: user.to_string(), amount: user_funds, - msg: to_binary(&astroport_governance::outpost::Cw20HookMsg::Unstake {}).unwrap(), + msg: to_json_binary(&astroport_governance::outpost::Cw20HookMsg::Unstake {}) + .unwrap(), }), ) .unwrap(); // Build the expected message - let ibc_message = to_binary(&Hub::Unstake { + let ibc_message = to_json_binary(&Hub::Unstake { receiver: user.to_string(), amount: user_funds, }) @@ -609,7 +611,7 @@ mod tests { reply_on: ReplyOn::Never, msg: WasmMsg::Execute { contract_addr: XASTRO_TOKEN.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Burn { amount: user_funds }).unwrap(), + msg: to_json_binary(&Cw20ExecuteMsg::Burn { amount: user_funds }).unwrap(), funds: vec![], } .into(), @@ -685,7 +687,7 @@ mod tests { // Ensure the config set during instantiation is still there assert_eq!( config, - to_binary(&astroport_governance::outpost::Config { + to_json_binary(&astroport_governance::outpost::Config { owner: Addr::unchecked(OWNER), xastro_token_addr: Addr::unchecked(XASTRO_TOKEN), vxastro_token_addr: Addr::unchecked(VXASTRO_TOKEN), @@ -721,7 +723,7 @@ mod tests { // connection assert_eq!( config, - to_binary(&astroport_governance::outpost::Config { + to_json_binary(&astroport_governance::outpost::Config { owner: Addr::unchecked(OWNER), xastro_token_addr: Addr::unchecked(XASTRO_TOKEN), vxastro_token_addr: Addr::unchecked(VXASTRO_TOKEN), @@ -757,7 +759,7 @@ mod tests { // connection assert_eq!( config, - to_binary(&astroport_governance::outpost::Config { + to_json_binary(&astroport_governance::outpost::Config { owner: Addr::unchecked(OWNER), xastro_token_addr: Addr::unchecked(XASTRO_TOKEN), vxastro_token_addr: Addr::unchecked(VXASTRO_TOKEN), @@ -793,7 +795,7 @@ mod tests { // connection assert_eq!( config, - to_binary(&astroport_governance::outpost::Config { + to_json_binary(&astroport_governance::outpost::Config { owner: Addr::unchecked(OWNER), xastro_token_addr: Addr::unchecked(XASTRO_TOKEN), vxastro_token_addr: Addr::unchecked(VXASTRO_TOKEN), @@ -866,7 +868,7 @@ mod tests { .unwrap(); // Wrap the query - let ibc_message = to_binary(&Hub::QueryProposal { id: proposal_id }).unwrap(); + let ibc_message = to_json_binary(&Hub::QueryProposal { id: proposal_id }).unwrap(); // Ensure a query is emitted assert_eq!( @@ -909,7 +911,7 @@ mod tests { .unwrap(); // Build the expected message - let ibc_message = to_binary(&Hub::CastAssemblyVote { + let ibc_message = to_json_binary(&Hub::CastAssemblyVote { proposal_id, voter: Addr::unchecked(user), vote_option: astroport_governance::assembly::ProposalVoteOption::For, @@ -961,7 +963,7 @@ mod tests { ) .unwrap(); - assert_eq!(vote_data, to_binary(&ProposalVoteOption::For).unwrap()); + assert_eq!(vote_data, to_json_binary(&ProposalVoteOption::For).unwrap()); } // Test Cases: @@ -1023,7 +1025,7 @@ mod tests { .unwrap(); // Build the expected message - let ibc_message = to_binary(&Hub::CastEmissionsVote { + let ibc_message = to_json_binary(&Hub::CastEmissionsVote { voter: Addr::unchecked(user), votes, voting_power: Uint128::from(voting_power), @@ -1120,7 +1122,7 @@ mod tests { .unwrap(); // Build the expected message - let ibc_message = to_binary(&Hub::KickUnlockedVoter { + let ibc_message = to_json_binary(&Hub::KickUnlockedVoter { voter: Addr::unchecked(user), }) .unwrap(); @@ -1215,7 +1217,7 @@ mod tests { .unwrap(); // Build the expected message - let ibc_message = to_binary(&Hub::KickBlacklistedVoter { + let ibc_message = to_json_binary(&Hub::KickBlacklistedVoter { voter: Addr::unchecked(user), }) .unwrap(); @@ -1295,7 +1297,7 @@ mod tests { .unwrap(); // Build the expected message - let ibc_message = to_binary(&Hub::WithdrawFunds { + let ibc_message = to_json_binary(&Hub::WithdrawFunds { user: Addr::unchecked(user), }) .unwrap(); diff --git a/contracts/outpost/src/ibc.rs b/contracts/outpost/src/ibc.rs index 35b2e14e..291fb650 100644 --- a/contracts/outpost/src/ibc.rs +++ b/contracts/outpost/src/ibc.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - ensure, entry_point, from_binary, to_binary, CosmosMsg, Deps, DepsMut, Env, + ensure, entry_point, from_json, to_json_binary, CosmosMsg, Deps, DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcChannelOpenResponse, IbcMsg, IbcOrder, IbcPacketAckMsg, IbcPacketReceiveMsg, IbcPacketTimeoutMsg, IbcReceiveResponse, Never, StdError, StdResult, @@ -109,7 +109,7 @@ pub fn ibc_packet_receive( ) -> Result { do_packet_receive(deps, env, msg).or_else(|err| { // Construct an error acknowledgement that can be handled on the Hub - let ack_data = to_binary(&Response::new_error(err.to_string())).unwrap(); + let ack_data = to_json_binary(&Response::new_error(err.to_string())).unwrap(); Ok(IbcReceiveResponse::new() .add_attribute("action", "ibc_packet_receive") @@ -137,7 +137,7 @@ fn do_packet_receive( )?; // Parse the packet data into a Hub message - let hub_msg: Outpost = from_binary(&msg.packet.data)?; + let hub_msg: Outpost = from_json(&msg.packet.data)?; match hub_msg { Outpost::MintXAstro { receiver, amount } => handle_ibc_xastro_mint(deps, receiver, amount), } @@ -155,7 +155,7 @@ pub fn ibc_packet_timeout( // to failed messages. // We look at the original packet to determine what failed and take // the appropriate action - let failed_msg: Hub = from_binary(&msg.packet.data)?; + let failed_msg: Hub = from_json(&msg.packet.data)?; response = handle_failed_messages(deps, failed_msg, response)?; Ok(response) @@ -169,7 +169,7 @@ pub fn ibc_packet_ack( ) -> Result { let mut response = IbcBasicResponse::new().add_attribute("action", "ibc_packet_ack"); - let ack: Result = from_binary(&msg.acknowledgement.data); + let ack: Result = from_json(&msg.acknowledgement.data); match ack { Ok(hub_response) => { match hub_response { @@ -210,7 +210,7 @@ pub fn ibc_packet_ack( }; let hub_msg = CosmosMsg::Ibc(IbcMsg::SendPacket { channel_id: hub_channel, - data: to_binary(&cast_vote)?, + data: to_json_binary(&cast_vote)?, timeout: env .block .time @@ -262,7 +262,7 @@ pub fn ibc_packet_ack( .add_attribute("ack_error", err.to_string()); // Handle the possible failures - let original: Hub = from_binary(&msg.original_packet.data)?; + let original: Hub = from_json(&msg.original_packet.data)?; response = handle_failed_messages(deps, original, response)?; } } @@ -564,8 +564,8 @@ mod tests { start_time: 1689942949u64, }); - let ack = IbcAcknowledgement::new(to_binary(&proposal_response).unwrap()); - let mint_msg = to_binary(&Outpost::MintXAstro { + let ack = IbcAcknowledgement::new(to_json_binary(&proposal_response).unwrap()); + let mint_msg = to_json_binary(&Outpost::MintXAstro { receiver: "user".to_owned(), amount: Uint128::one(), }) @@ -591,7 +591,7 @@ mod tests { assert_eq!(res.messages.len(), 1); // Build the expected message - let ibc_message = to_binary(&Hub::CastAssemblyVote { + let ibc_message = to_json_binary(&Hub::CastAssemblyVote { proposal_id, voter: Addr::unchecked(user), vote_option: astroport_governance::assembly::ProposalVoteOption::For, diff --git a/contracts/outpost/src/ibc_failure.rs b/contracts/outpost/src/ibc_failure.rs index 1ccd9747..7fd9e6ca 100644 --- a/contracts/outpost/src/ibc_failure.rs +++ b/contracts/outpost/src/ibc_failure.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_binary, DepsMut, IbcBasicResponse, WasmMsg}; +use cosmwasm_std::{to_json_binary, DepsMut, IbcBasicResponse, WasmMsg}; use astroport_governance::{interchain::Hub, voting_escrow_lite}; @@ -61,7 +61,7 @@ pub fn handle_failed_messages( let msg = WasmMsg::Execute { contract_addr: config.vxastro_token_addr.to_string(), - msg: to_binary(&relock_msg)?, + msg: to_json_binary(&relock_msg)?, funds: vec![], }; @@ -89,8 +89,8 @@ mod tests { use cosmwasm_std::{ attr, testing::{mock_info, MOCK_CONTRACT_ADDR}, - to_binary, Addr, IbcEndpoint, IbcPacket, IbcPacketTimeoutMsg, ReplyOn, StdError, SubMsg, - Uint128, WasmMsg, + to_json_binary, Addr, IbcEndpoint, IbcPacket, IbcPacketTimeoutMsg, ReplyOn, StdError, + SubMsg, Uint128, WasmMsg, }; use super::*; @@ -147,7 +147,7 @@ mod tests { .unwrap(); // Attempt to get timeout from different contract - let original_unstake_msg = to_binary(&Hub::Unstake { + let original_unstake_msg = to_json_binary(&Hub::Unstake { receiver: user.to_string(), amount, }) @@ -175,7 +175,7 @@ mod tests { assert_eq!(res.messages.len(), 1); // Verify that the mint message matches the expected message - let xastro_mint_msg = to_binary(&cw20::Cw20ExecuteMsg::Mint { + let xastro_mint_msg = to_json_binary(&cw20::Cw20ExecuteMsg::Mint { recipient: user.to_string(), amount, }) @@ -241,7 +241,7 @@ mod tests { .unwrap(); // Construct the original message - let original_msg = to_binary(&Hub::CastAssemblyVote { + let original_msg = to_json_binary(&Hub::CastAssemblyVote { proposal_id, voter: Addr::unchecked(user), vote_option: astroport_governance::assembly::ProposalVoteOption::For, @@ -327,7 +327,7 @@ mod tests { .unwrap(); // Construct the original message - let original_msg = to_binary(&Hub::CastEmissionsVote { + let original_msg = to_json_binary(&Hub::CastEmissionsVote { voter: Addr::unchecked(user), voting_power, votes, @@ -411,7 +411,7 @@ mod tests { .unwrap(); // Construct the original message - let original_msg = to_binary(&Hub::QueryProposal { id: proposal_id }).unwrap(); + let original_msg = to_json_binary(&Hub::QueryProposal { id: proposal_id }).unwrap(); // Authorised channels let packet = IbcPacket::new( original_msg, @@ -512,7 +512,7 @@ mod tests { .unwrap(); // Construct the original message - let original_msg = to_binary(&Hub::KickUnlockedVoter { + let original_msg = to_json_binary(&Hub::KickUnlockedVoter { voter: Addr::unchecked(user), }) .unwrap(); @@ -560,7 +560,7 @@ mod tests { reply_on: ReplyOn::Never, msg: WasmMsg::Execute { contract_addr: VXASTRO_TOKEN.to_string(), - msg: to_binary( + msg: to_json_binary( &astroport_governance::voting_escrow_lite::ExecuteMsg::Relock { user: user.to_string() } @@ -614,7 +614,7 @@ mod tests { .unwrap(); // Construct the original message - let original_msg = to_binary(&Hub::WithdrawFunds { + let original_msg = to_json_binary(&Hub::WithdrawFunds { user: Addr::unchecked(user), }) .unwrap(); diff --git a/contracts/outpost/src/ibc_mint.rs b/contracts/outpost/src/ibc_mint.rs index 3f21510c..06c078d8 100644 --- a/contracts/outpost/src/ibc_mint.rs +++ b/contracts/outpost/src/ibc_mint.rs @@ -1,5 +1,5 @@ use astroport_governance::interchain::Response; -use cosmwasm_std::{to_binary, Deps, DepsMut, IbcReceiveResponse, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Deps, DepsMut, IbcReceiveResponse, Uint128, WasmMsg}; use cw20::Cw20ExecuteMsg; use crate::{error::ContractError, state::CONFIG}; @@ -18,7 +18,7 @@ pub fn handle_ibc_xastro_mint( let msg = mint_xastro_msg(deps.as_ref(), recipient.clone(), amount)?; // If the minting succeeds, the ack will be sent back to the Hub - let ack_data = to_binary(&Response::new_success( + let ack_data = to_json_binary(&Response::new_success( "mint_xastro".to_owned(), recipient.to_string(), ))?; @@ -44,7 +44,7 @@ pub fn mint_xastro_msg( let mint_msg = Cw20ExecuteMsg::Mint { recipient, amount }; Ok(WasmMsg::Execute { contract_addr: config.xastro_token_addr.to_string(), - msg: to_binary(&mint_msg)?, + msg: to_json_binary(&mint_msg)?, funds: vec![], }) } @@ -53,7 +53,7 @@ pub fn mint_xastro_msg( mod tests { use astroport_governance::interchain::Outpost; use cosmwasm_std::{ - from_binary, testing::mock_info, Addr, IbcPacketReceiveMsg, ReplyOn, SubMsg, Uint128, + from_json, testing::mock_info, Addr, IbcPacketReceiveMsg, ReplyOn, SubMsg, Uint128, }; use super::*; @@ -107,7 +107,7 @@ mod tests { ) .unwrap(); - let ibc_mint = to_binary(&Outpost::MintXAstro { + let ibc_mint = to_json_binary(&Outpost::MintXAstro { receiver: receiver.to_string(), amount, }) @@ -118,7 +118,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); - let ack: Response = from_binary(&res.acknowledgement).unwrap(); + let ack: Response = from_json(&res.acknowledgement).unwrap(); match ack { Response::Result { error, .. } => { assert!(error == Some("Unauthorized".to_string())); @@ -130,7 +130,7 @@ mod tests { let recv_packet = mock_ibc_packet(&format!("wasm.{}", HUB), "channel-7", ibc_mint.clone()); let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env.clone(), msg).unwrap(); - let ack: Response = from_binary(&res.acknowledgement).unwrap(); + let ack: Response = from_json(&res.acknowledgement).unwrap(); match ack { Response::Result { error, .. } => { assert!(error == Some("Unauthorized".to_string())); @@ -143,7 +143,7 @@ mod tests { let msg = IbcPacketReceiveMsg::new(recv_packet, Addr::unchecked("relayer")); let res = ibc_packet_receive(deps.as_mut(), env, msg).unwrap(); - let ack: Response = from_binary(&res.acknowledgement).unwrap(); + let ack: Response = from_json(&res.acknowledgement).unwrap(); match ack { Response::Result { error, .. } => { assert!(error.is_none()); @@ -155,7 +155,7 @@ mod tests { assert_eq!(res.messages.len(), 1); // Verify that the mint message matches the expected message - let xastro_mint_msg = to_binary(&cw20::Cw20ExecuteMsg::Mint { + let xastro_mint_msg = to_json_binary(&cw20::Cw20ExecuteMsg::Mint { recipient: receiver.to_string(), amount, }) diff --git a/contracts/outpost/src/mock.rs b/contracts/outpost/src/mock.rs index b8bb7e9b..b6d77e83 100644 --- a/contracts/outpost/src/mock.rs +++ b/contracts/outpost/src/mock.rs @@ -1,15 +1,14 @@ #[cfg(test)] -use cosmwasm_std::from_binary; use cosmwasm_std::{ testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, - to_binary, Binary, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, + to_json_binary, Binary, DepsMut, Env, IbcChannel, IbcChannelConnectMsg, IbcChannelOpenMsg, IbcEndpoint, IbcOrder, IbcPacket, IbcQuery, ListChannelsResponse, MessageInfo, OwnedDeps, Timestamp, Uint128, }; use cosmwasm_std::testing::MOCK_CONTRACT_ADDR; use cosmwasm_std::{ - from_slice, Empty, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, + from_json, Empty, Querier, QuerierResult, QueryRequest, SystemError, SystemResult, WasmQuery, }; use crate::ibc::{ibc_channel_connect, ibc_channel_open, IBC_APP_VERSION}; @@ -45,7 +44,7 @@ pub struct WasmMockQuerier { impl Querier for WasmMockQuerier { fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { // MockQuerier doesn't support Custom, so we ignore it completely - let request: QueryRequest = match from_slice(bin_request) { + let request: QueryRequest = match from_json(bin_request) { Ok(v) => v, Err(e) => { return SystemResult::Err(SystemError::InvalidRequest { @@ -63,7 +62,7 @@ impl WasmMockQuerier { match &request { QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { if contract_addr == XASTRO_TOKEN { - match from_binary(msg).unwrap() { + match from_json(msg).unwrap() { astroport::xastro_outpost_token::QueryMsg::BalanceAt { address: _, timestamp: _, @@ -71,14 +70,14 @@ impl WasmMockQuerier { let balance = astroport::token::BalanceResponse { balance: Uint128::from(1000u128), }; - SystemResult::Ok(to_binary(&balance).into()) + SystemResult::Ok(to_json_binary(&balance).into()) } _ => { panic!("DO NOT ENTER HERE") } } } else { - match from_binary(msg).unwrap() { + match from_json(msg).unwrap() { astroport_governance::voting_escrow_lite::QueryMsg::UserDepositAt { user:_, timestamp:_, @@ -86,7 +85,7 @@ impl WasmMockQuerier { let balance = astroport::token::BalanceResponse { balance: Uint128::zero(), }; - SystemResult::Ok(to_binary(&balance).into()) + SystemResult::Ok(to_json_binary(&balance).into()) } astroport_governance::voting_escrow_lite::QueryMsg::UserEmissionsVotingPower { user:_, @@ -94,7 +93,7 @@ impl WasmMockQuerier { let balance = astroport_governance::voting_escrow_lite::VotingPowerResponse { voting_power: Uint128::from(1000u128), }; - SystemResult::Ok(to_binary(&balance).into()) + SystemResult::Ok(to_json_binary(&balance).into()) } _ => { panic!("DO NOT ENTER HERE") @@ -133,7 +132,7 @@ impl WasmMockQuerier { ), ], }; - SystemResult::Ok(to_binary(&response).into()) + SystemResult::Ok(to_json_binary(&response).into()) } _ => self.base.handle_query(request), } diff --git a/contracts/outpost/src/query.rs b/contracts/outpost/src/query.rs index a0192a73..5cd0b865 100644 --- a/contracts/outpost/src/query.rs +++ b/contracts/outpost/src/query.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{entry_point, to_binary, Addr, Binary, Deps, Env, StdResult, Uint128}; +use cosmwasm_std::{entry_point, to_json_binary, Addr, Binary, Deps, Env, StdResult, Uint128}; use astroport::xastro_outpost_token::get_voting_power_at_time; use astroport_governance::outpost::QueryMsg; @@ -14,10 +14,10 @@ use crate::state::{CONFIG, VOTES}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), QueryMsg::ProposalVoted { proposal_id, user } => { let user_address = deps.api.addr_validate(&user)?; - to_binary(&VOTES.load(deps.storage, (&user_address, proposal_id))?) + to_json_binary(&VOTES.load(deps.storage, (&user_address, proposal_id))?) } } } @@ -151,7 +151,7 @@ mod tests { ) .unwrap(); - assert_eq!(vote_data, to_binary(&ProposalVoteOption::For).unwrap()); + assert_eq!(vote_data, to_json_binary(&ProposalVoteOption::For).unwrap()); // Check that we receive an error when querying a vote that doesn't exist let err = query( diff --git a/contracts/voting_escrow/src/contract.rs b/contracts/voting_escrow/src/contract.rs index 3bfd6dc8..b63f2af3 100644 --- a/contracts/voting_escrow/src/contract.rs +++ b/contracts/voting_escrow/src/contract.rs @@ -5,7 +5,7 @@ use astroport_governance::astroport::DecimalCheckedOps; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, from_binary, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + attr, from_json, to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Storage, Uint128, WasmMsg, }; use cw2::set_contract_version; @@ -405,7 +405,7 @@ fn receive_cw20( let sender = Addr::unchecked(cw20_msg.sender); blacklist_check(deps.storage, &sender)?; - match from_binary(&cw20_msg.msg)? { + match from_json(&cw20_msg.msg)? { Cw20HookMsg::CreateLock { time } => create_lock(deps, env, sender, cw20_msg.amount, time), Cw20HookMsg::ExtendLockAmount {} => deposit_for(deps, env, cw20_msg.amount, sender), Cw20HookMsg::DepositFor { user } => { @@ -508,7 +508,7 @@ fn withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> Result StdResult { match msg { QueryMsg::CheckVotersAreBlacklisted { voters } => { - to_binary(&check_voters_are_blacklisted(deps, voters)?) + to_json_binary(&check_voters_are_blacklisted(deps, voters)?) } QueryMsg::BlacklistedVoters { start_after, limit } => { - to_binary(&get_blacklisted_voters(deps, start_after, limit)?) + to_json_binary(&get_blacklisted_voters(deps, start_after, limit)?) } - QueryMsg::TotalVotingPower {} => to_binary(&get_total_voting_power(deps, env, None)?), + QueryMsg::TotalVotingPower {} => to_json_binary(&get_total_voting_power(deps, env, None)?), QueryMsg::UserVotingPower { user } => { - to_binary(&get_user_voting_power(deps, env, user, None)?) + to_json_binary(&get_user_voting_power(deps, env, user, None)?) } QueryMsg::TotalVotingPowerAt { time } => { - to_binary(&get_total_voting_power(deps, env, Some(time))?) + to_json_binary(&get_total_voting_power(deps, env, Some(time))?) } QueryMsg::TotalVotingPowerAtPeriod { period } => { - to_binary(&get_total_voting_power_at_period(deps, env, period)?) + to_json_binary(&get_total_voting_power_at_period(deps, env, period)?) } QueryMsg::UserVotingPowerAt { user, time } => { - to_binary(&get_user_voting_power(deps, env, user, Some(time))?) + to_json_binary(&get_user_voting_power(deps, env, user, Some(time))?) } QueryMsg::UserVotingPowerAtPeriod { user, period } => { - to_binary(&get_user_voting_power_at_period(deps, user, period)?) + to_json_binary(&get_user_voting_power_at_period(deps, user, period)?) } - QueryMsg::LockInfo { user } => to_binary(&get_user_lock_info(deps, env, user)?), + QueryMsg::LockInfo { user } => to_json_binary(&get_user_lock_info(deps, env, user)?), QueryMsg::UserDepositAtHeight { user, height } => { - to_binary(&get_user_deposit_at_height(deps, user, height)?) + to_json_binary(&get_user_deposit_at_height(deps, user, height)?) } QueryMsg::Config {} => { let config = CONFIG.load(deps.storage)?; - to_binary(&ConfigResponse { + to_json_binary(&ConfigResponse { owner: config.owner.to_string(), guardian_addr: config.guardian_addr, deposit_token_addr: config.deposit_token_addr.to_string(), @@ -762,10 +762,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { logo_urls_whitelist: config.logo_urls_whitelist, }) } - QueryMsg::Balance { address } => to_binary(&get_user_balance(deps, env, address)?), - QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps, env)?), - QueryMsg::MarketingInfo {} => to_binary(&query_marketing_info(deps)?), - QueryMsg::DownloadLogo {} => to_binary(&query_download_logo(deps)?), + QueryMsg::Balance { address } => to_json_binary(&get_user_balance(deps, env, address)?), + QueryMsg::TokenInfo {} => to_json_binary(&query_token_info(deps, env)?), + QueryMsg::MarketingInfo {} => to_json_binary(&query_marketing_info(deps)?), + QueryMsg::DownloadLogo {} => to_json_binary(&query_download_logo(deps)?), } } diff --git a/contracts/voting_escrow/tests/integration.rs b/contracts/voting_escrow/tests/integration.rs index c9b21223..a86a4c54 100644 --- a/contracts/voting_escrow/tests/integration.rs +++ b/contracts/voting_escrow/tests/integration.rs @@ -1,5 +1,5 @@ use astroport::token as astro; -use cosmwasm_std::{attr, to_binary, Addr, Fraction, StdError, Uint128}; +use cosmwasm_std::{attr, to_json_binary, Addr, Fraction, StdError, Uint128}; use cw20::{Cw20ExecuteMsg, Logo, LogoInfo, MarketingInfoResponse, MinterResponse}; use cw_multi_test::{next_block, ContractWrapper, Executor}; use voting_escrow::astroport; @@ -215,7 +215,7 @@ fn random_token_lock() { let cw20msg = Cw20ExecuteMsg::Send { contract: helper.voting_instance.to_string(), amount: Uint128::from(10_u128), - msg: to_binary(&Cw20HookMsg::CreateLock { time: WEEK }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time: WEEK }).unwrap(), }; let err = router .execute_contract(Addr::unchecked("user"), random_token, &cw20msg, &[]) diff --git a/contracts/voting_escrow/tests/test_utils.rs b/contracts/voting_escrow/tests/test_utils.rs index b4507709..0a30d974 100644 --- a/contracts/voting_escrow/tests/test_utils.rs +++ b/contracts/voting_escrow/tests/test_utils.rs @@ -7,7 +7,9 @@ use astroport_governance::voting_escrow::{ UpdateMarketingInfo, VotingPowerResponse, }; use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; -use cosmwasm_std::{attr, to_binary, Addr, QueryRequest, StdResult, Timestamp, Uint128, WasmQuery}; +use cosmwasm_std::{ + attr, to_json_binary, Addr, QueryRequest, StdResult, Timestamp, Uint128, WasmQuery, +}; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, Logo, MinterResponse}; use cw_multi_test::{App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor}; use voting_escrow::astroport; @@ -88,7 +90,7 @@ impl Helper { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: staking_instance.to_string(), - msg: to_binary(&xastro::QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&xastro::QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -181,7 +183,7 @@ impl Helper { let to_addr = Addr::unchecked(to); let msg = Cw20ExecuteMsg::Send { contract: self.staking_instance.to_string(), - msg: to_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), + msg: to_json_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), amount: Uint128::from(amount), }; router @@ -230,7 +232,7 @@ impl Helper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -251,7 +253,7 @@ impl Helper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -271,7 +273,7 @@ impl Helper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -293,7 +295,7 @@ impl Helper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::DepositFor { + msg: to_json_binary(&Cw20HookMsg::DepositFor { user: to.to_string(), }) .unwrap(), diff --git a/contracts/voting_escrow_delegation/src/contract.rs b/contracts/voting_escrow_delegation/src/contract.rs index 3b503e1f..1cd69c11 100644 --- a/contracts/voting_escrow_delegation/src/contract.rs +++ b/contracts/voting_escrow_delegation/src/contract.rs @@ -1,33 +1,33 @@ -use astroport_governance::utils::{calc_voting_power, get_period, get_periods_count}; -use astroport_governance::voting_escrow::{get_voting_power, get_voting_power_at, MAX_LIMIT}; use std::marker::PhantomData; -use crate::error::ContractError; -use crate::state::{CONFIG, DELEGATED, OWNERSHIP_PROPOSAL, TOKENS}; -use astroport_governance::astroport::common::{ - claim_ownership, drop_ownership_proposal, propose_new_owner, -}; -use astroport_governance::voting_escrow_delegation::{ - Config, ExecuteMsg, InstantiateMsg, QueryMsg, -}; - #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Reply, ReplyOn, + attr, to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Reply, ReplyOn, Response, StdError, StdResult, SubMsg, Uint128, WasmMsg, }; use cw2::set_contract_version; use cw721::NftInfoResponse; +use cw721_base::helpers as cw721_helpers; +use cw721_base::msg::{ExecuteMsg as ExecuteMsgNFT, InstantiateMsg as InstantiateMsgNFT}; +use cw721_base::{Extension, MintMsg}; use cw_utils::parse_reply_instantiate_data; +use astroport_governance::astroport::common::{ + claim_ownership, drop_ownership_proposal, propose_new_owner, +}; +use astroport_governance::utils::{calc_voting_power, get_period, get_periods_count}; +use astroport_governance::voting_escrow::{get_voting_power, get_voting_power_at, MAX_LIMIT}; +use astroport_governance::voting_escrow_delegation::{ + Config, ExecuteMsg, InstantiateMsg, QueryMsg, +}; + +use crate::error::ContractError; use crate::helpers::{ calc_delegation, calc_extend_delegation, calc_not_delegated_vp, calc_total_delegated_vp, validate_parameters, }; -use cw721_base::helpers as cw721_helpers; -use cw721_base::msg::{ExecuteMsg as ExecuteMsgNFT, InstantiateMsg as InstantiateMsgNFT}; -use cw721_base::{Extension, MintMsg}; +use crate::state::{CONFIG, DELEGATED, OWNERSHIP_PROPOSAL, TOKENS}; // Version info for contract migration. const CONTRACT_NAME: &str = "voting-escrow-delegation"; @@ -59,7 +59,7 @@ pub fn instantiate( msg: WasmMsg::Instantiate { admin: Some(config.owner.to_string()), code_id: msg.nft_code_id, - msg: to_binary(&InstantiateMsgNFT { + msg: to_json_binary(&InstantiateMsgNFT { name: TOKEN_NAME.to_string(), symbol: TOKEN_SYMBOL.to_string(), minter: env.contract.address.to_string(), @@ -242,7 +242,7 @@ pub fn create_delegation( ]) .add_submessage(SubMsg::new(WasmMsg::Execute { contract_addr: cfg.nft_addr.to_string(), - msg: to_binary(&ExecuteMsgNFT::::Mint(MintMsg::< + msg: to_json_binary(&ExecuteMsgNFT::::Mint(MintMsg::< Extension, > { token_id, @@ -357,12 +357,12 @@ fn update_config( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), QueryMsg::AdjustedBalance { account, timestamp } => { - to_binary(&adjusted_balance(deps, env, account, timestamp)?) + to_json_binary(&adjusted_balance(deps, env, account, timestamp)?) } QueryMsg::DelegatedVotingPower { account, timestamp } => { - to_binary(&delegated_vp(deps, env, account, timestamp)?) + to_json_binary(&delegated_vp(deps, env, account, timestamp)?) } } } diff --git a/contracts/voting_escrow_delegation/tests/integration.rs b/contracts/voting_escrow_delegation/tests/integration.rs index 9851580b..b9b6147c 100644 --- a/contracts/voting_escrow_delegation/tests/integration.rs +++ b/contracts/voting_escrow_delegation/tests/integration.rs @@ -1,7 +1,7 @@ use astroport_governance::utils::WEEK; use astroport_governance::voting_escrow_delegation::Config; use astroport_governance::voting_escrow_delegation::QueryMsg; -use cosmwasm_std::{to_binary, Addr, Empty, QueryRequest, Uint128, WasmQuery}; +use cosmwasm_std::{to_json_binary, Addr, Empty, QueryRequest, Uint128, WasmQuery}; use cw721_base::{ExecuteMsg as ExecuteMsgNFT, Extension, MintMsg, QueryMsg as QueryMsgNFT}; use cw_multi_test::Executor; @@ -25,7 +25,7 @@ fn config() { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: helper.delegation_instance.to_string(), - msg: to_binary(&QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -43,7 +43,7 @@ fn mint() { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: helper.nft_instance.to_string(), - msg: to_binary(&QueryMsgNFT::::ContractInfo {}).unwrap(), + msg: to_json_binary(&QueryMsgNFT::::ContractInfo {}).unwrap(), })) .unwrap(); assert_eq!("Delegated VP NFT", resp.name); @@ -84,7 +84,7 @@ fn mint() { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: helper.nft_instance.to_string(), - msg: to_binary(&QueryMsgNFT::::NumTokens {}).unwrap(), + msg: to_json_binary(&QueryMsgNFT::::NumTokens {}).unwrap(), })) .unwrap(); assert_eq!(1, resp.count); @@ -93,7 +93,7 @@ fn mint() { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: helper.nft_instance.to_string(), - msg: to_binary(&QueryMsgNFT::::Tokens { + msg: to_json_binary(&QueryMsgNFT::::Tokens { owner: USER.to_string(), start_after: None, limit: None, @@ -137,7 +137,7 @@ fn mint() { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: helper.nft_instance.to_string(), - msg: to_binary(&QueryMsgNFT::::Tokens { + msg: to_json_binary(&QueryMsgNFT::::Tokens { owner: USER.to_string(), start_after: None, limit: None, diff --git a/contracts/voting_escrow_delegation/tests/test_helper.rs b/contracts/voting_escrow_delegation/tests/test_helper.rs index 278f0dad..32da8d92 100644 --- a/contracts/voting_escrow_delegation/tests/test_helper.rs +++ b/contracts/voting_escrow_delegation/tests/test_helper.rs @@ -3,7 +3,7 @@ use astroport_governance::utils::EPOCH_START; use astroport_governance::voting_escrow_delegation::Config; use astroport_governance::voting_escrow_delegation::{InstantiateMsg, QueryMsg}; use astroport_tests::escrow_helper::EscrowHelper; -use cosmwasm_std::{to_binary, Addr, Empty, QueryRequest, StdResult, Uint128, WasmQuery}; +use cosmwasm_std::{to_json_binary, Addr, Empty, QueryRequest, StdResult, Uint128, WasmQuery}; use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; use astroport_governance::voting_escrow_delegation::ExecuteMsg; @@ -62,7 +62,7 @@ impl Helper { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: delegation_addr.to_string(), - msg: to_binary(&QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -151,7 +151,7 @@ impl Helper { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: self.delegation_instance.to_string(), - msg: to_binary(&QueryMsg::AdjustedBalance { + msg: to_json_binary(&QueryMsg::AdjustedBalance { account: user.to_string(), timestamp, }) @@ -169,7 +169,7 @@ impl Helper { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: self.delegation_instance.to_string(), - msg: to_binary(&QueryMsg::DelegatedVotingPower { + msg: to_json_binary(&QueryMsg::DelegatedVotingPower { account: user.to_string(), timestamp, }) diff --git a/contracts/voting_escrow_lite/src/execute.rs b/contracts/voting_escrow_lite/src/execute.rs index 66487a42..d4b5cfe4 100644 --- a/contracts/voting_escrow_lite/src/execute.rs +++ b/contracts/voting_escrow_lite/src/execute.rs @@ -5,8 +5,8 @@ use astroport_governance::{generator_controller_lite, outpost}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, from_binary, to_binary, Addr, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, - StdResult, Storage, Uint128, WasmMsg, + attr, from_json, to_json_binary, Addr, CosmosMsg, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Storage, Uint128, WasmMsg, }; use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; use cw20_base::contract::{execute_update_marketing, execute_upload_logo}; @@ -143,7 +143,7 @@ fn receive_cw20( let sender = Addr::unchecked(cw20_msg.sender); blacklist_check(deps.storage, &sender)?; - match from_binary(&cw20_msg.msg)? { + match from_json(&cw20_msg.msg)? { Cw20HookMsg::CreateLock { .. } => create_lock(deps, env, sender, cw20_msg.amount), Cw20HookMsg::ExtendLockAmount {} => deposit_for(deps, env, cw20_msg.amount, sender), Cw20HookMsg::DepositFor { user } => { @@ -255,7 +255,7 @@ fn unlock(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result Result StdResult { match msg { QueryMsg::CheckVotersAreBlacklisted { voters } => { - to_binary(&check_voters_are_blacklisted(deps, voters)?) + to_json_binary(&check_voters_are_blacklisted(deps, voters)?) } QueryMsg::BlacklistedVoters { start_after, limit } => { - to_binary(&get_blacklisted_voters(deps, start_after, limit)?) + to_json_binary(&get_blacklisted_voters(deps, start_after, limit)?) } - QueryMsg::TotalVotingPower {} => to_binary(&VotingPowerResponse { + QueryMsg::TotalVotingPower {} => to_json_binary(&VotingPowerResponse { voting_power: Uint128::zero(), }), - QueryMsg::TotalVotingPowerAt { .. } => to_binary(&VotingPowerResponse { + QueryMsg::TotalVotingPowerAt { .. } => to_json_binary(&VotingPowerResponse { voting_power: Uint128::zero(), }), - QueryMsg::TotalVotingPowerAtPeriod { .. } => to_binary(&VotingPowerResponse { + QueryMsg::TotalVotingPowerAtPeriod { .. } => to_json_binary(&VotingPowerResponse { voting_power: Uint128::zero(), }), - QueryMsg::UserVotingPower { .. } => to_binary(&VotingPowerResponse { + QueryMsg::UserVotingPower { .. } => to_json_binary(&VotingPowerResponse { voting_power: Uint128::zero(), }), - QueryMsg::UserVotingPowerAt { .. } => to_binary(&VotingPowerResponse { + QueryMsg::UserVotingPowerAt { .. } => to_json_binary(&VotingPowerResponse { voting_power: Uint128::zero(), }), - QueryMsg::UserVotingPowerAtPeriod { .. } => to_binary(&VotingPowerResponse { + QueryMsg::UserVotingPowerAtPeriod { .. } => to_json_binary(&VotingPowerResponse { voting_power: Uint128::zero(), }), QueryMsg::TotalEmissionsVotingPower {} => { - to_binary(&get_total_emissions_voting_power(deps, env, None)?) + to_json_binary(&get_total_emissions_voting_power(deps, env, None)?) } QueryMsg::TotalEmissionsVotingPowerAt { time } => { - to_binary(&get_total_emissions_voting_power(deps, env, Some(time))?) + to_json_binary(&get_total_emissions_voting_power(deps, env, Some(time))?) } QueryMsg::UserEmissionsVotingPower { user } => { - to_binary(&get_user_emissions_voting_power(deps, env, user, None)?) + to_json_binary(&get_user_emissions_voting_power(deps, env, user, None)?) } - QueryMsg::UserEmissionsVotingPowerAt { user, time } => to_binary( + QueryMsg::UserEmissionsVotingPowerAt { user, time } => to_json_binary( &get_user_emissions_voting_power(deps, env, user, Some(time))?, ), - QueryMsg::LockInfo { user } => to_binary(&get_user_lock_info(deps, env, user)?), + QueryMsg::LockInfo { user } => to_json_binary(&get_user_lock_info(deps, env, user)?), QueryMsg::UserDepositAt { user, timestamp } => { - to_binary(&get_user_deposit_at_time(deps, user, timestamp)?) + to_json_binary(&get_user_deposit_at_time(deps, user, timestamp)?) } QueryMsg::Config {} => { let config = CONFIG.load(deps.storage)?; - to_binary(&config) + to_json_binary(&config) } - QueryMsg::Balance { address } => to_binary(&get_user_balance(deps, env, address)?), - QueryMsg::TokenInfo {} => to_binary(&query_token_info(deps, env)?), - QueryMsg::MarketingInfo {} => to_binary(&query_marketing_info(deps)?), - QueryMsg::DownloadLogo {} => to_binary(&query_download_logo(deps)?), + QueryMsg::Balance { address } => to_json_binary(&get_user_balance(deps, env, address)?), + QueryMsg::TokenInfo {} => to_json_binary(&query_token_info(deps, env)?), + QueryMsg::MarketingInfo {} => to_json_binary(&query_marketing_info(deps)?), + QueryMsg::DownloadLogo {} => to_json_binary(&query_download_logo(deps)?), } } diff --git a/contracts/voting_escrow_lite/tests/integration.rs b/contracts/voting_escrow_lite/tests/integration.rs index 1f31431d..0a82b18b 100644 --- a/contracts/voting_escrow_lite/tests/integration.rs +++ b/contracts/voting_escrow_lite/tests/integration.rs @@ -1,5 +1,5 @@ use astroport::token as astro; -use cosmwasm_std::{attr, to_binary, Addr, StdError, Uint128, Uint64}; +use cosmwasm_std::{attr, to_json_binary, Addr, StdError, Uint128, Uint64}; use cw20::{Cw20ExecuteMsg, Logo, LogoInfo, MarketingInfoResponse, MinterResponse}; use cw_multi_test::{next_block, ContractWrapper, Executor}; use voting_escrow_lite::astroport; @@ -174,7 +174,7 @@ fn random_token_lock() { let cw20msg = Cw20ExecuteMsg::Send { contract: helper.voting_instance.to_string(), amount: Uint128::from(10_u128), - msg: to_binary(&Cw20HookMsg::CreateLock { time: WEEK }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time: WEEK }).unwrap(), }; let err = router .execute_contract(Addr::unchecked("user"), random_token, &cw20msg, &[]) diff --git a/contracts/voting_escrow_lite/tests/test_utils.rs b/contracts/voting_escrow_lite/tests/test_utils.rs index 015af405..5dc10557 100644 --- a/contracts/voting_escrow_lite/tests/test_utils.rs +++ b/contracts/voting_escrow_lite/tests/test_utils.rs @@ -7,7 +7,7 @@ use astroport_governance::voting_escrow_lite::{ }; use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; use cosmwasm_std::{ - attr, to_binary, Addr, QueryRequest, StdResult, Timestamp, Uint128, Uint64, WasmQuery, + attr, to_json_binary, Addr, QueryRequest, StdResult, Timestamp, Uint128, Uint64, WasmQuery, }; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, Logo, MinterResponse}; use cw_multi_test::{App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor}; @@ -88,7 +88,7 @@ impl Helper { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: staking_instance.to_string(), - msg: to_binary(&xastro::QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&xastro::QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -184,7 +184,7 @@ impl Helper { let to_addr = Addr::unchecked(to); let msg = Cw20ExecuteMsg::Send { contract: self.staking_instance.to_string(), - msg: to_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), + msg: to_json_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), amount: Uint128::from(amount), }; router @@ -233,7 +233,7 @@ impl Helper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -254,7 +254,7 @@ impl Helper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -274,7 +274,7 @@ impl Helper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -308,7 +308,7 @@ impl Helper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::DepositFor { + msg: to_json_binary(&Cw20HookMsg::DepositFor { user: to.to_string(), }) .unwrap(), diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index 647c9890..c5b81a20 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -70,7 +70,8 @@ pub enum ExecuteMsg { title: String, description: String, link: Option, - messages: Option>, + #[serde(default)] + messages: Vec, /// If proposal should be executed on a remote chain this field should specify governance channel ibc_channel: Option, }, @@ -358,7 +359,7 @@ pub struct Proposal { /// Proposal link pub link: Option, /// Proposal messages - pub messages: Option>, + pub messages: Vec, /// Amount of xASTRO deposited in order to post the proposal pub deposit_amount: Uint128, /// IBC channel diff --git a/packages/astroport-governance/src/hub.rs b/packages/astroport-governance/src/hub.rs index 95fd59eb..632e0c04 100644 --- a/packages/astroport-governance/src/hub.rs +++ b/packages/astroport-governance/src/hub.rs @@ -96,6 +96,7 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + // TODO: change to track by nano seconds /// Returns the current balance of xASTRO minted via a specific Outpost channel #[returns(HubBalance)] ChannelBalanceAt { channel: String, timestamp: Uint64 }, diff --git a/packages/astroport-governance/src/utils.rs b/packages/astroport-governance/src/utils.rs index d4395d8d..aeea36b3 100644 --- a/packages/astroport-governance/src/utils.rs +++ b/packages/astroport-governance/src/utils.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - to_binary, Addr, Decimal, Fraction, IbcQuery, ListChannelsResponse, OverflowError, - QuerierWrapper, QueryRequest, StdError, StdResult, Uint128, Uint256, Uint64, WasmQuery, + Addr, Decimal, Fraction, IbcQuery, ListChannelsResponse, OverflowError, QuerierWrapper, + QueryRequest, StdError, StdResult, Uint128, Uint256, Uint64, }; use crate::hub::HubBalance; @@ -147,8 +147,10 @@ pub fn check_contract_supports_channel( .iter() .find(|channel| &channel.endpoint.channel_id == given_channel) .map(|_| ()) - .ok_or_else(|| StdError::GenericErr { - msg: format!("The contract does not have channel {0}", given_channel), + .ok_or_else(|| { + StdError::generic_err(format!( + "The contract does not have channel {given_channel}" + )) }) } @@ -164,11 +166,11 @@ pub fn get_total_outpost_voting_power_at( contract: &Addr, timestamp: u64, ) -> Result { - let response: HubBalance = querier.query(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: contract.to_string(), - msg: to_binary(&crate::hub::QueryMsg::TotalChannelBalancesAt { + let response: HubBalance = querier.query_wasm_smart( + contract, + &crate::hub::QueryMsg::TotalChannelBalancesAt { timestamp: Uint64::from(timestamp), - })?, - }))?; + }, + )?; Ok(response.balance) } diff --git a/packages/astroport-tests-lite/src/base.rs b/packages/astroport-tests-lite/src/base.rs index d8381e36..85c08e65 100644 --- a/packages/astroport-tests-lite/src/base.rs +++ b/packages/astroport-tests-lite/src/base.rs @@ -7,7 +7,7 @@ use astroport_governance::voting_escrow_lite::{ Cw20HookMsg, ExecuteMsg, InstantiateMsg as AstroVotingEscrowInstantiateMsg, QueryMsg, VotingPowerResponse, }; -use cosmwasm_std::{attr, to_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; +use cosmwasm_std::{attr, to_json_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; use anyhow::Result; @@ -131,7 +131,7 @@ impl BaseAstroportTestPackage { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: self.staking.clone().unwrap().address.to_string(), - msg: to_binary(&staking::QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&staking::QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -219,7 +219,7 @@ impl BaseAstroportTestPackage { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_escrow.clone().unwrap().address.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract(user, self.get_staking_xastro(router), &cw20msg, &[]) @@ -235,7 +235,7 @@ impl BaseAstroportTestPackage { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_escrow.clone().unwrap().address.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), }; router.execute_contract( Addr::unchecked(user), diff --git a/packages/astroport-tests-lite/src/controller_helper.rs b/packages/astroport-tests-lite/src/controller_helper.rs index edfbc9ce..bc202b61 100644 --- a/packages/astroport-tests-lite/src/controller_helper.rs +++ b/packages/astroport-tests-lite/src/controller_helper.rs @@ -62,6 +62,7 @@ impl ControllerHelper { maker_fee_bps: 10, is_disabled: false, is_generator_disabled: false, + permissioned: false, }], token_code_id: escrow_helper.astro_token_code_id, fee_address: None, @@ -115,13 +116,13 @@ impl ControllerHelper { let assembly_contract = Box::new(ContractWrapper::new_with_empty( astro_assembly::contract::execute, astro_assembly::contract::instantiate, - astro_assembly::contract::query, + astro_assembly::queries::query, )); let assembly_code = router.store_code(assembly_contract); let assembly_default_instantiate_msg = astroport_governance::assembly::InstantiateMsg { - xastro_token_addr: escrow_helper.xastro_token.to_string(), + staking_addr: escrow_helper.staking_instance.to_string(), vxastro_token_addr: None, voting_escrow_delegator_addr: None, ibc_controller: None, diff --git a/packages/astroport-tests-lite/src/escrow_helper.rs b/packages/astroport-tests-lite/src/escrow_helper.rs index f1da02e8..68f60e64 100644 --- a/packages/astroport-tests-lite/src/escrow_helper.rs +++ b/packages/astroport-tests-lite/src/escrow_helper.rs @@ -3,7 +3,7 @@ use astroport::{staking as xastro, token as astro}; use astroport_governance::voting_escrow_lite::{ Cw20HookMsg, ExecuteMsg, InstantiateMsg, LockInfoResponse, QueryMsg, VotingPowerResponse, }; -use cosmwasm_std::{attr, to_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; +use cosmwasm_std::{attr, to_json_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; @@ -83,7 +83,7 @@ impl EscrowHelper { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: staking_instance.to_string(), - msg: to_binary(&xastro::QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&xastro::QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -144,7 +144,7 @@ impl EscrowHelper { let to_addr = Addr::unchecked(to); let msg = Cw20ExecuteMsg::Send { contract: self.staking_instance.to_string(), - msg: to_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), + msg: to_json_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), amount: Uint128::from(amount), }; router @@ -177,7 +177,7 @@ impl EscrowHelper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.escrow_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -197,7 +197,7 @@ impl EscrowHelper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.escrow_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -227,7 +227,7 @@ impl EscrowHelper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.escrow_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::DepositFor { + msg: to_json_binary(&Cw20HookMsg::DepositFor { user: to.to_string(), }) .unwrap(), diff --git a/packages/astroport-tests/src/base.rs b/packages/astroport-tests/src/base.rs index 60ad8418..3403be3c 100644 --- a/packages/astroport-tests/src/base.rs +++ b/packages/astroport-tests/src/base.rs @@ -7,7 +7,7 @@ use astroport_governance::voting_escrow::{ Cw20HookMsg, ExecuteMsg, InstantiateMsg as AstroVotingEscrowInstantiateMsg, QueryMsg, VotingPowerResponse, }; -use cosmwasm_std::{attr, to_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; +use cosmwasm_std::{attr, to_json_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; use anyhow::Result; @@ -131,7 +131,7 @@ impl BaseAstroportTestPackage { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: self.staking.clone().unwrap().address.to_string(), - msg: to_binary(&staking::QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&staking::QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -217,7 +217,7 @@ impl BaseAstroportTestPackage { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_escrow.clone().unwrap().address.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract(user, self.get_staking_xastro(router), &cw20msg, &[]) @@ -233,7 +233,7 @@ impl BaseAstroportTestPackage { let cw20msg = Cw20ExecuteMsg::Send { contract: self.voting_escrow.clone().unwrap().address.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), }; router.execute_contract( Addr::unchecked(user), diff --git a/packages/astroport-tests/src/controller_helper.rs b/packages/astroport-tests/src/controller_helper.rs index e6fcddfe..2878ddfd 100644 --- a/packages/astroport-tests/src/controller_helper.rs +++ b/packages/astroport-tests/src/controller_helper.rs @@ -52,6 +52,7 @@ impl ControllerHelper { maker_fee_bps: 10, is_disabled: false, is_generator_disabled: false, + permissioned: false, }], token_code_id: escrow_helper.astro_token_code_id, fee_address: None, diff --git a/packages/astroport-tests/src/escrow_helper.rs b/packages/astroport-tests/src/escrow_helper.rs index f4eecad1..36d38d90 100644 --- a/packages/astroport-tests/src/escrow_helper.rs +++ b/packages/astroport-tests/src/escrow_helper.rs @@ -3,7 +3,7 @@ use astroport::{staking as xastro, token as astro}; use astroport_governance::voting_escrow::{ Cw20HookMsg, ExecuteMsg, InstantiateMsg, LockInfoResponse, QueryMsg, VotingPowerResponse, }; -use cosmwasm_std::{attr, to_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; +use cosmwasm_std::{attr, to_json_binary, Addr, QueryRequest, StdResult, Uint128, WasmQuery}; use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; use cw_multi_test::{App, AppResponse, ContractWrapper, Executor}; @@ -83,7 +83,7 @@ impl EscrowHelper { .wrap() .query::(&QueryRequest::Wasm(WasmQuery::Smart { contract_addr: staking_instance.to_string(), - msg: to_binary(&xastro::QueryMsg::Config {}).unwrap(), + msg: to_json_binary(&xastro::QueryMsg::Config {}).unwrap(), })) .unwrap(); @@ -142,7 +142,7 @@ impl EscrowHelper { let to_addr = Addr::unchecked(to); let msg = Cw20ExecuteMsg::Send { contract: self.staking_instance.to_string(), - msg: to_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), + msg: to_json_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), amount: Uint128::from(amount), }; router @@ -175,7 +175,7 @@ impl EscrowHelper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.escrow_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), + msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -195,7 +195,7 @@ impl EscrowHelper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.escrow_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), + msg: to_json_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), }; router.execute_contract( Addr::unchecked(user), @@ -216,7 +216,7 @@ impl EscrowHelper { let cw20msg = Cw20ExecuteMsg::Send { contract: self.escrow_instance.to_string(), amount: Uint128::from(amount), - msg: to_binary(&Cw20HookMsg::DepositFor { + msg: to_json_binary(&Cw20HookMsg::DepositFor { user: to.to_string(), }) .unwrap(), From b56587142256a0fc170901fb1a2e471d5381ba3b Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 25 Jan 2024 12:29:44 +0400 Subject: [PATCH 13/47] prepare testing suite for assembly --- Cargo.lock | 22 +- contracts/assembly/Cargo.toml | 3 +- contracts/assembly/src/utils.rs | 9 +- contracts/assembly/tests/common/helper.rs | 263 ++ contracts/assembly/tests/common/mod.rs | 2 + contracts/assembly/tests/common/stargate.rs | 124 + contracts/assembly/tests/integration.rs | 4639 ++++++++++--------- packages/astroport-governance/src/hub.rs | 1 - 8 files changed, 2736 insertions(+), 2327 deletions(-) create mode 100644 contracts/assembly/tests/common/helper.rs create mode 100644 contracts/assembly/tests/common/mod.rs create mode 100644 contracts/assembly/tests/common/stargate.rs diff --git a/Cargo.lock b/Cargo.lock index c84c7a43..2fac102f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,6 +29,7 @@ dependencies = [ "astroport-hub", "astroport-nft", "astroport-staking 2.0.0", + "astroport-tokenfactory-tracker", "builder-unlock", "cosmwasm-schema", "cosmwasm-std", @@ -37,8 +38,8 @@ dependencies = [ "cw-utils 1.0.3", "cw2 1.1.2", "ibc-controller-package", + "osmosis-std", "thiserror", - "voting-escrow", "voting-escrow-delegation", "voting-escrow-lite", ] @@ -61,7 +62,7 @@ dependencies = [ [[package]] name = "astroport" version = "3.8.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#07c45e88c139fced103c034fe426ed017bb64060" +source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" dependencies = [ "astroport-circular-buffer 0.1.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", "cosmwasm-schema", @@ -93,7 +94,7 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#07c45e88c139fced103c034fe426ed017bb64060" +source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -285,7 +286,7 @@ dependencies = [ [[package]] name = "astroport-staking" version = "2.0.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#07c45e88c139fced103c034fe426ed017bb64060" +source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" dependencies = [ "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", "cosmwasm-std", @@ -357,6 +358,19 @@ dependencies = [ "snafu", ] +[[package]] +name = "astroport-tokenfactory-tracker" +version = "1.0.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" +dependencies = [ + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "thiserror", +] + [[package]] name = "astroport-whitelist" version = "1.0.1" diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 0b45fb27..599f1a7c 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -27,11 +27,12 @@ cw-utils = "1" [dev-dependencies] cw-multi-test = { git = "https://github.com/astroport-fi/cw-multi-test", branch = "feat/bank_with_send_hooks", features = ["cosmwasm_1_1"] } +osmosis-std = "0.21" astroport-hub = { path = "../hub" } -voting-escrow = { path = "../voting_escrow" } voting-escrow-lite = { path = "../voting_escrow_lite" } voting-escrow-delegation = { path = "../voting_escrow_delegation" } astroport-nft = { path = "../nft" } astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } +astroport-tokenfactory-tracker = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } builder-unlock = { path = "../builder_unlock" } anyhow = "1" diff --git a/contracts/assembly/src/utils.rs b/contracts/assembly/src/utils.rs index 9a1159b1..7a968be3 100644 --- a/contracts/assembly/src/utils.rs +++ b/contracts/assembly/src/utils.rs @@ -1,5 +1,5 @@ use astroport::tokenfactory_tracker; -use cosmwasm_std::{Deps, StdResult, Timestamp, Uint128, Uint64}; +use cosmwasm_std::{Deps, StdResult, Uint128, Uint64}; use astroport_governance::assembly::Proposal; use astroport_governance::builder_unlock::msg::{ @@ -25,7 +25,7 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std &config.xastro_denom_tracking, &tokenfactory_tracker::QueryMsg::BalanceAt { address: sender.clone(), - timestamp: Some(Timestamp::from_seconds(proposal.start_time).nanos()), + timestamp: Some(proposal.start_time), }, )?; @@ -51,11 +51,13 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std }, )? } else { + // TODO: why? // For vxASTRO lite, this will always be 0 let res: VotingPowerResponse = deps.querier.query_wasm_smart( &vxastro_token_addr, &VotingEscrowQueryMsg::UserVotingPowerAt { user: sender.clone(), + // TODO: remove - WEEK time: proposal.start_time - WEEK, }, )?; @@ -87,7 +89,7 @@ pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult< let mut total: Uint128 = deps.querier.query_wasm_smart( config.xastro_denom_tracking, &tokenfactory_tracker::QueryMsg::TotalSupplyAt { - timestamp: Some(Timestamp::from_seconds(proposal.start_time).nanos()), + timestamp: Some(proposal.start_time), }, )?; @@ -98,6 +100,7 @@ pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult< total += builder_state.remaining_astro_tokens; + // TODO: remove it since it is always 0? if let Some(vxastro_token_addr) = config.vxastro_token_addr { // Total vxASTRO voting power // For vxASTRO lite, this will always be 0 diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs new file mode 100644 index 00000000..f726f28b --- /dev/null +++ b/contracts/assembly/tests/common/helper.rs @@ -0,0 +1,263 @@ +#![allow(dead_code)] + +use anyhow::Result as AnyResult; +use astroport::staking; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{ + coins, Addr, Coin, Decimal, DepsMut, Empty, Env, GovMsg, IbcMsg, IbcQuery, MemoryStorage, + MessageInfo, Response, StdResult, Uint128, +}; +use cw_multi_test::{ + App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, + Executor, FailingModule, StakeKeeper, WasmKeeper, TOKEN_FACTORY_MODULE, +}; + +use astroport_governance::assembly; +use astroport_governance::assembly::{ + DELAY_INTERVAL, DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, + MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE, MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, + VOTING_PERIOD_INTERVAL, +}; + +use crate::common::stargate::StargateKeeper; + +fn staking_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + astroport_staking::contract::execute, + astroport_staking::contract::instantiate, + astroport_staking::contract::query, + ) + .with_reply_empty(astroport_staking::contract::reply), + ) +} + +fn tracker_contract() -> Box> { + Box::new( + ContractWrapper::new_with_empty( + |_: DepsMut, _: Env, _: MessageInfo, _: Empty| -> StdResult { + unimplemented!() + }, + astroport_tokenfactory_tracker::contract::instantiate, + astroport_tokenfactory_tracker::query::query, + ) + .with_sudo_empty(astroport_tokenfactory_tracker::contract::sudo), + ) +} + +fn assembly_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + astro_assembly::contract::execute, + astro_assembly::contract::instantiate, + astro_assembly::queries::query, + )) +} + +fn builder_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + builder_unlock::contract::execute, + builder_unlock::contract::instantiate, + builder_unlock::contract::query, + )) +} + +pub type CustomizedApp = App< + BankKeeper, + MockApi, + MemoryStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + FailingModule, + FailingModule, + StargateKeeper, +>; + +pub struct Helper { + pub app: CustomizedApp, + pub owner: Addr, + pub staking: Addr, + pub assembly: Addr, + pub builder_unlock: Addr, + pub xastro_denom: String, +} + +pub const ASTRO_DENOM: &str = "factory/assembly/ASTRO"; + +impl Helper { + pub fn new(owner: &Addr) -> AnyResult { + let mut app = BasicAppBuilder::new() + .with_stargate(StargateKeeper::default()) + .build(|router, _, storage| { + router + .bank + .init_balance(storage, owner, coins(u128::MAX, ASTRO_DENOM)) + .unwrap() + }); + + let staking_code_id = app.store_code(staking_contract()); + let tracker_code_id = app.store_code(tracker_contract()); + let assembly_code_id = app.store_code(assembly_contract()); + + let msg = astroport::staking::InstantiateMsg { + deposit_token_denom: ASTRO_DENOM.to_string(), + tracking_admin: owner.to_string(), + tracking_code_id: tracker_code_id, + token_factory_addr: TOKEN_FACTORY_MODULE.to_string(), + }; + let staking = app + .instantiate_contract( + staking_code_id, + owner.clone(), + &msg, + &[], + String::from("Astroport Staking"), + None, + ) + .unwrap(); + let staking::Config { xastro_denom, .. } = app + .wrap() + .query_wasm_smart(&staking, &staking::QueryMsg::Config {}) + .unwrap(); + + let builder_unlock_code_id = app.store_code(builder_contract()); + + let msg = astroport_governance::builder_unlock::msg::InstantiateMsg { + owner: owner.to_string(), + astro_token: ASTRO_DENOM.to_string(), + max_allocations_amount: Uint128::new(300_000_000_000000), + }; + + let builder_unlock = app + .instantiate_contract( + builder_unlock_code_id, + owner.clone(), + &msg, + &[], + "Builder Unlock contract".to_string(), + Some(owner.to_string()), + ) + .unwrap(); + + let msg = assembly::InstantiateMsg { + staking_addr: staking.to_string(), + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller_addr: None, + hub_addr: None, + builder_unlock_addr: builder_unlock.to_string(), + proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), + proposal_effective_delay: *DELAY_INTERVAL.start(), + proposal_expiration_period: *EXPIRATION_PERIOD_INTERVAL.start(), + proposal_required_deposit: (*DEPOSIT_INTERVAL.start()).into(), + proposal_required_quorum: MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE.to_string(), + proposal_required_threshold: Decimal::from_atomics( + MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, + 2, + ) + .unwrap() + .to_string(), + whitelisted_links: vec!["https://some.link/".to_string()], + }; + let assembly = app + .instantiate_contract( + assembly_code_id, + owner.clone(), + &msg, + &[], + String::from("Astroport Assembly"), + None, + ) + .unwrap(); + + Ok(Self { + app, + owner: owner.clone(), + staking, + assembly, + builder_unlock, + xastro_denom, + }) + } + + pub fn give_astro(&mut self, amount: u128, recipient: &Addr) { + self.app + .send_tokens( + self.owner.clone(), + recipient.clone(), + &coins(amount, ASTRO_DENOM), + ) + .unwrap(); + } + + pub fn stake(&mut self, sender: &Addr, amount: u128) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.staking.clone(), + &staking::ExecuteMsg::Enter {}, + &coins(amount, ASTRO_DENOM), + ) + } + + pub fn unstake(&mut self, sender: &Addr, amount: u128) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.staking.clone(), + &staking::ExecuteMsg::Leave {}, + &coins(amount, &self.xastro_denom), + ) + } + + pub fn query_balance(&self, sender: &Addr, denom: &str) -> StdResult { + self.app + .wrap() + .query_balance(sender, denom) + .map(|c| c.amount) + } + + pub fn staking_xastro_balance_at( + &self, + sender: &Addr, + timestamp: Option, + ) -> StdResult { + self.app.wrap().query_wasm_smart( + &self.staking, + &staking::QueryMsg::BalanceAt { + address: sender.to_string(), + timestamp, + }, + ) + } + + pub fn query_xastro_supply_at(&self, timestamp: Option) -> StdResult { + self.app.wrap().query_wasm_smart( + &self.staking, + &staking::QueryMsg::TotalSupplyAt { timestamp }, + ) + } + + pub fn mint_coin(&mut self, to: &Addr, coin: Coin) { + // .init_balance() erases previous balance thus I use such hack and create intermediate "denom admin" + let denom_admin = Addr::unchecked(format!("{}_admin", &coin.denom)); + self.app + .init_modules(|router, _, storage| { + router + .bank + .init_balance(storage, &denom_admin, vec![coin.clone()]) + }) + .unwrap(); + + self.app + .send_tokens(denom_admin, to.clone(), &[coin]) + .unwrap(); + } + + pub fn next_block(&mut self, time: u64) { + self.app.update_block(|block| { + block.time = block.time.plus_seconds(time); + block.height += 1 + }); + } +} diff --git a/contracts/assembly/tests/common/mod.rs b/contracts/assembly/tests/common/mod.rs new file mode 100644 index 00000000..cb854e7a --- /dev/null +++ b/contracts/assembly/tests/common/mod.rs @@ -0,0 +1,2 @@ +pub mod helper; +pub mod stargate; diff --git a/contracts/assembly/tests/common/stargate.rs b/contracts/assembly/tests/common/stargate.rs new file mode 100644 index 00000000..b8282c92 --- /dev/null +++ b/contracts/assembly/tests/common/stargate.rs @@ -0,0 +1,124 @@ +use std::fmt::Debug; + +use anyhow::Result as AnyResult; +use cosmwasm_schema::schemars::JsonSchema; +use cosmwasm_schema::serde::de::DeserializeOwned; +use cosmwasm_std::{ + coin, Addr, Api, BankMsg, Binary, BlockInfo, CustomQuery, Querier, Storage, SubMsgResponse, +}; +use cw_multi_test::{AppResponse, BankSudo, CosmosRouter, Stargate}; +use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ + MsgBurn, MsgCreateDenom, MsgCreateDenomResponse, MsgMint, MsgSetBeforeSendHook, + MsgSetDenomMetadata, +}; + +#[derive(Default)] +pub struct StargateKeeper {} + +impl Stargate for StargateKeeper { + fn execute( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + sender: Addr, + type_url: String, + value: Binary, + ) -> AnyResult + where + ExecC: Debug + Clone + PartialEq + JsonSchema + DeserializeOwned + 'static, + QueryC: CustomQuery + DeserializeOwned + 'static, + { + match type_url.as_str() { + MsgCreateDenom::TYPE_URL => { + let tf_msg: MsgCreateDenom = value.try_into()?; + let submsg_response = SubMsgResponse { + events: vec![], + data: Some( + MsgCreateDenomResponse { + new_token_denom: format!( + "factory/{}/{}", + tf_msg.sender, tf_msg.subdenom + ), + } + .into(), + ), + }; + Ok(submsg_response.into()) + } + MsgMint::TYPE_URL => { + let tf_msg: MsgMint = value.try_into()?; + let mint_coins = tf_msg + .amount + .expect("Empty amount in tokenfactory MsgMint!"); + let cw_coin = coin(mint_coins.amount.parse()?, mint_coins.denom); + let bank_sudo = BankSudo::Mint { + to_address: tf_msg.mint_to_address.clone(), + amount: vec![cw_coin.clone()], + }; + + router.sudo(api, storage, block, bank_sudo.into()) + } + MsgBurn::TYPE_URL => { + let tf_msg: MsgBurn = value.try_into()?; + let burn_coins = tf_msg + .amount + .expect("Empty amount in tokenfactory MsgBurn!"); + let cw_coin = coin(burn_coins.amount.parse()?, burn_coins.denom); + let burn_msg = BankMsg::Burn { + amount: vec![cw_coin.clone()], + }; + + router.execute( + api, + storage, + block, + Addr::unchecked(&tf_msg.sender), + burn_msg.into(), + ) + } + MsgSetDenomMetadata::TYPE_URL => { + // TODO: Implement this if needed + Ok(AppResponse::default()) + } + MsgSetBeforeSendHook::TYPE_URL => { + let tf_msg: MsgSetBeforeSendHook = value.try_into()?; + + let bank_sudo = BankSudo::SetHook { + denom: tf_msg.denom, + contract_addr: tf_msg.cosmwasm_address, + }; + + router.sudo(api, storage, block, bank_sudo.into()) + } + _ => Err(anyhow::anyhow!( + "Unexpected exec msg {type_url} from {sender:?}", + )), + } + } + + fn query( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _path: String, + _data: Binary, + ) -> AnyResult { + unimplemented!("Stargate queries are not implemented") + // match path.as_str() { + // "/osmosis.poolmanager.v1beta1.Query/Params" => { + // Ok(to_json_binary(&poolmanager::v1beta1::ParamsResponse { + // params: Some(poolmanager::v1beta1::Params { + // pool_creation_fee: vec![coin(1000_000000, "uosmo").into()], + // taker_fee_params: None, + // authorized_quote_denoms: vec![], + // }), + // })?) + // } + // _ => Err(anyhow::anyhow!("Unexpected stargate query request {path}")), + // } + } +} diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 3be73025..eeb7b100 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -1,2330 +1,2333 @@ -use astro_assembly::astroport; -use astroport::{ - token::InstantiateMsg as TokenInstantiateMsg, xastro_token::QueryMsg as XAstroQueryMsg, -}; -use astroport_governance::assembly::{ - Config, Cw20HookMsg, ExecuteMsg, InstantiateMsg, Proposal, ProposalListResponse, - ProposalStatus, ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, - DEPOSIT_INTERVAL, VOTING_PERIOD_INTERVAL, -}; -use cosmwasm_std::coins; - -use std::str::FromStr; - -use astroport_governance::voting_escrow_lite::{ - Cw20HookMsg as VXAstroCw20HookMsg, InstantiateMsg as VXAstroInstantiateMsg, -}; - -use astroport_governance::hub::InstantiateMsg as HubInstantiateMsg; - -use astroport_governance::builder_unlock::msg::{ - InstantiateMsg as BuilderUnlockInstantiateMsg, ReceiveMsg as BuilderUnlockReceiveMsg, -}; -use astroport_governance::builder_unlock::{AllocationParams, Schedule}; -use astroport_governance::utils::{EPOCH_START, WEEK}; -use cosmwasm_std::{ - testing::{mock_env, MockApi, MockStorage}, - to_json_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, - Uint64, WasmMsg, WasmQuery, -}; -use cw20::{BalanceResponse, Cw20ExecuteMsg, MinterResponse}; -use cw_multi_test::{ - next_block, App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor, -}; - -const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); -const PROPOSAL_EFFECTIVE_DELAY: u64 = 12_342; -const PROPOSAL_EXPIRATION_PERIOD: u64 = 86_399; -const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); -const PROPOSAL_REQUIRED_QUORUM: &str = "0.50"; -const PROPOSAL_REQUIRED_THRESHOLD: &str = "0.60"; - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_contract_instantiation() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - // Instantiate needed contracts - let token_addr = instantiate_astro_token(&mut app, &owner); - let (_, xastro_token_addr) = instantiate_xastro_token(&mut app, &owner, &token_addr); - let vxastro_token_addr = instantiate_vxastro_token(&mut app, &owner, &xastro_token_addr); - let builder_unlock_addr = instantiate_builder_unlock_contract(&mut app, &owner, &token_addr); - - let assembly_contract = Box::new(ContractWrapper::new_with_empty( - astro_assembly::contract::execute, - astro_assembly::contract::instantiate, - astro_assembly::contract::query, - )); - - let assembly_code = app.store_code(assembly_contract); - - let assembly_default_instantiate_msg = InstantiateMsg { - xastro_token_addr: xastro_token_addr.to_string(), - vxastro_token_addr: Some(vxastro_token_addr.to_string()), - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller_addr: None, - hub_addr: None, - builder_unlock_addr: builder_unlock_addr.to_string(), - proposal_voting_period: PROPOSAL_VOTING_PERIOD, - proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, - proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, - proposal_required_deposit: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), - proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), - whitelisted_links: vec!["https://some.link/".to_string()], - }; - - // Try to instantiate assembly with wrong threshold - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_required_threshold: "0.3".to_string(), - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" - ); - - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_required_threshold: "1.1".to_string(), - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" - ); - - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_required_quorum: "1.1".to_string(), - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The required quorum for a proposal cannot be lower than 1% or higher than 100%" - ); - - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_expiration_period: 500, - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The expiration period for a proposal cannot be lower than 12342 or higher than 100800" - ); - - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_effective_delay: 400, - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The effective delay for a proposal cannot be lower than 6171 or higher than 14400" - ); - - let assembly_instance = app - .instantiate_contract( - assembly_code, - owner.clone(), - &assembly_default_instantiate_msg, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap(); - - let res: Config = app - .wrap() - .query_wasm_smart(assembly_instance, &QueryMsg::Config {}) - .unwrap(); - - assert_eq!(res.xastro_token_addr, xastro_token_addr); - assert_eq!(res.builder_unlock_addr, builder_unlock_addr); - assert_eq!(res.proposal_voting_period, PROPOSAL_VOTING_PERIOD); - assert_eq!(res.proposal_effective_delay, PROPOSAL_EFFECTIVE_DELAY); - assert_eq!(res.proposal_expiration_period, PROPOSAL_EXPIRATION_PERIOD); - assert_eq!( - res.proposal_required_deposit, - Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) - ); - assert_eq!( - res.proposal_required_quorum, - Decimal::from_str(PROPOSAL_REQUIRED_QUORUM).unwrap() - ); - assert_eq!( - res.proposal_required_threshold, - Decimal::from_str(PROPOSAL_REQUIRED_THRESHOLD).unwrap() - ); - assert_eq!( - res.whitelisted_links, - vec!["https://some.link/".to_string(),] - ); -} - -#[test] -fn test_proposal_submitting() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - let user = Addr::unchecked("user1"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = - instantiate_contracts(&mut app, owner, false, false); - - let proposals: ProposalListResponse = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposals { - start: None, - limit: None, - }, - ) - .unwrap(); - - assert_eq!(proposals.proposal_count, Uint64::from(0u32)); - assert_eq!(proposals.proposal_list, vec![]); - - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user, - PROPOSAL_REQUIRED_DEPOSIT, - ); - - check_token_balance(&mut app, &xastro_addr, &user, PROPOSAL_REQUIRED_DEPOSIT); - - // Try to create proposal with insufficient token deposit - let submit_proposal_msg = Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from("https://some.link")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT - 1), - }; - - let err = app - .execute_contract(user.clone(), xastro_addr.clone(), &submit_proposal_msg, &[]) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Insufficient token deposit!"); - - // Try to create a proposal with wrong title - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("X"), - description: String::from("Description"), - link: Some(String::from("https://some.link/")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Title too short!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from_utf8(vec![b'X'; 65]).unwrap(), - description: String::from("Description"), - link: Some(String::from("https://some.link/")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Title too long!" - ); - - // Try to create a proposal with wrong description - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("X"), - link: Some(String::from("https://some.link/")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Description too short!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from_utf8(vec![b'X'; 1025]).unwrap(), - link: Some(String::from("https://some.link/")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Description too long!" - ); - - // Try to create a proposal with wrong link - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from("X")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Link too short!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from_utf8(vec![b'X'; 129]).unwrap()), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Link too long!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from("https://some1.link")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Link is not whitelisted!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from( - "https://some.link/", - )), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Link is not properly formatted or contains unsafe characters!" - ); - - // Valid proposal submission - app.execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from("https://some.link/q/")), - messages: Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - guardian_addr: None, - }))) - .unwrap(), - funds: vec![], - })]), - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.proposal_id, Uint64::from(1u64)); - assert_eq!(proposal.submitter, user); - assert_eq!(proposal.status, ProposalStatus::Active); - assert_eq!(proposal.for_power, Uint128::zero()); - assert_eq!(proposal.against_power, Uint128::zero()); - assert_eq!(proposal.start_block, 12_345); - assert_eq!(proposal.end_block, 12_345 + PROPOSAL_VOTING_PERIOD); - assert_eq!(proposal.title, String::from("Title")); - assert_eq!(proposal.description, String::from("Description")); - assert_eq!(proposal.link, Some(String::from("https://some.link/q/"))); - assert_eq!( - proposal.messages, - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - guardian_addr: None, - }))) - .unwrap(), - funds: vec![], - })]) - ); - assert_eq!( - proposal.deposit_amount, - Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) - ) -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_successful_proposal() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let ( - token_addr, - staking_instance, - xastro_addr, - vxastro_addr, - builder_unlock_addr, - assembly_addr, - _, - _, - ) = instantiate_contracts(&mut app, owner, false, false); - - // Init voting power for users - let balances: Vec<(&str, u128, u128)> = vec![ - ("user0", PROPOSAL_REQUIRED_DEPOSIT, 0), // proposal submitter - ("user1", 20, 80), - ("user2", 100, 100), - ("user3", 300, 100), - ("user4", 200, 50), - ("user5", 0, 90), - ("user6", 100, 200), - ("user7", 30, 0), - ("user8", 80, 100), - ("user9", 50, 0), - ("user10", 0, 90), - ("user11", 500, 0), - ("user12", 10000_000000, 0), - ]; - - let default_allocation_params = AllocationParams { - amount: Uint128::zero(), - unlock_schedule: Schedule { - start_time: 12_345, - cliff: 5, - duration: 500, - percent_at_cliff: None, - }, - proposed_receiver: None, - }; - - let locked_balances = vec![ - ( - "user1".to_string(), - AllocationParams { - amount: Uint128::from(80u32), - ..default_allocation_params.clone() - }, - ), - ( - "user4".to_string(), - AllocationParams { - amount: Uint128::from(50u32), - ..default_allocation_params.clone() - }, - ), - ( - "user7".to_string(), - AllocationParams { - amount: Uint128::from(100u32), - ..default_allocation_params.clone() - }, - ), - ( - "user10".to_string(), - AllocationParams { - amount: Uint128::from(30u32), - ..default_allocation_params - }, - ), - ]; - - for (addr, xastro, vxastro) in balances { - if xastro > 0 { - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked(addr), - xastro, - ); - } - - if vxastro > 0 { - mint_vxastro( - &mut app, - &staking_instance, - xastro_addr.clone(), - &vxastro_addr, - Addr::unchecked(addr), - vxastro, - ); - } - } - - create_allocations(&mut app, token_addr, builder_unlock_addr, locked_balances); - - // Skip period - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create default proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(PROPOSAL_VOTING_PERIOD + 1000), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: Some(vec![ - "https://some1.link/".to_string(), - "https://some2.link/".to_string(), - ]), - whitelist_remove: Some(vec!["https://some.link/".to_string()]), - guardian_addr: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - - let votes: Vec<(&str, ProposalVoteOption, u128)> = vec![ - ("user1", ProposalVoteOption::For, 180u128), - ("user2", ProposalVoteOption::For, 200u128), - ("user3", ProposalVoteOption::For, 400u128), - ("user4", ProposalVoteOption::For, 300u128), - ("user5", ProposalVoteOption::For, 90u128), - ("user6", ProposalVoteOption::For, 300u128), - ("user7", ProposalVoteOption::For, 130u128), - ("user8", ProposalVoteOption::Against, 180u128), - ("user9", ProposalVoteOption::Against, 50u128), - ("user10", ProposalVoteOption::Against, 120u128), - ("user11", ProposalVoteOption::Against, 500u128), - ("user12", ProposalVoteOption::For, 10000_000000u128), - ]; - - check_total_vp(&mut app, &assembly_addr, 1, 20000002450); - - for (addr, option, expected_vp) in votes { - let sender = Addr::unchecked(addr); - - check_user_vp(&mut app, &assembly_addr, &sender, 1, expected_vp); - - cast_vote(&mut app, assembly_addr.clone(), 1, sender, option).unwrap(); - } - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - let proposal_votes: ProposalVotesResponse = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::ProposalVotes { proposal_id: 1 }, - ) - .unwrap(); - - let proposal_for_voters: Vec = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::ProposalVoters { - proposal_id: 1, - vote_option: ProposalVoteOption::For, - start: None, - limit: None, - }, - ) - .unwrap(); - - let proposal_against_voters: Vec = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::ProposalVoters { - proposal_id: 1, - vote_option: ProposalVoteOption::Against, - start: None, - limit: None, - }, - ) - .unwrap(); - - // Check proposal votes - assert_eq!(proposal.for_power, Uint128::from(10000001600u128)); - assert_eq!(proposal.against_power, Uint128::from(850u32)); - - assert_eq!(proposal_votes.for_power, Uint128::from(10000001600u128)); - assert_eq!(proposal_votes.against_power, Uint128::from(850u32)); - - assert_eq!( - proposal_for_voters, - vec![ - Addr::unchecked("user1"), - Addr::unchecked("user2"), - Addr::unchecked("user3"), - Addr::unchecked("user4"), - Addr::unchecked("user5"), - Addr::unchecked("user6"), - Addr::unchecked("user7"), - Addr::unchecked("user12"), - ] - ); - assert_eq!( - proposal_against_voters, - vec![ - Addr::unchecked("user8"), - Addr::unchecked("user9"), - Addr::unchecked("user10"), - Addr::unchecked("user11") - ] - ); - - // Skip voting period - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); - }); - - // Try to vote after voting period - let err = cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user11"), - ProposalVoteOption::Against, - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Voting period ended!"); - - // Try to execute the proposal before end_proposal - let err = app - .execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Proposal not passed!"); - - // Check the successful completion of the proposal - check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - check_token_balance( - &mut app, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.status, ProposalStatus::Passed); - - // Try to end proposal again - let err = app - .execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Proposal not active!"); - - // Try to execute the proposal before the delay - let err = app - .execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Proposal delay not ended!"); - - // Skip blocks - app.update_block(|bi| { - bi.height += PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - // Try to execute the proposal after the delay - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let config: Config = app - .wrap() - .query_wasm_smart(assembly_addr.to_string(), &QueryMsg::Config {}) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.to_string(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - // Check execution result - assert_eq!(config.proposal_voting_period, PROPOSAL_VOTING_PERIOD + 1000); - assert_eq!( - config.whitelisted_links, - vec![ - "https://some1.link/".to_string(), - "https://some2.link/".to_string(), - ] - ); - assert_eq!(proposal.status, ProposalStatus::Executed); - - // Try to remove proposal before expiration period - let err = app - .execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Proposal not completed!"); - - // Remove expired proposal - app.update_block(|bi| { - bi.height += PROPOSAL_EXPIRATION_PERIOD + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + 1)); - }); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let res: ProposalListResponse = app - .wrap() - .query_wasm_smart( - assembly_addr.to_string(), - &QueryMsg::Proposals { - start: None, - limit: None, - }, - ) - .unwrap(); - - assert_eq!(res.proposal_list, vec![]); - // proposal_count should not be changed after removing a proposal - assert_eq!(res.proposal_count, Uint64::from(1u32)); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_successful_emissions_proposal() { - use cosmwasm_std::{coins, BankMsg}; - - let mut app = mock_app(); - let owner = Addr::unchecked("generator_controller"); - - let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); - - // Provide some funds to the Assembly contract to use in the proposal messages - app.init_modules(|router, _, storage| { - router.bank.init_balance( - storage, - &Addr::unchecked(assembly_addr.clone()), - coins(1000, "uluna"), - ) - }) - .unwrap(); - - let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { - title: "Emissions Test title".to_string(), - description: "Emissions Test description".to_string(), - // Sample message to use as we don't have IBC or the Generator to set emissions on - messages: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: "generator_controller".into(), - amount: coins(1, "uluna"), - })], - ibc_channel: None, - }; - - app.execute_contract( - Addr::unchecked("generator_controller"), - assembly_addr.clone(), - &emissions_proposal_msg, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart(assembly_addr, &QueryMsg::Proposal { proposal_id: 1 }) - .unwrap(); - - assert_eq!(proposal.status, ProposalStatus::Executed); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_no_generator_controller_emissions_proposal() { - let mut app = mock_app(); - let owner = Addr::unchecked("generator_controller"); - - let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); - let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { - title: "Emissions Test title!".to_string(), - description: "Emissions Test description!".to_string(), - messages: vec![], - ibc_channel: None, - }; - - let err = app - .execute_contract( - Addr::unchecked("generator_controller"), - assembly_addr, - &emissions_proposal_msg, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Sender is not the Generator controller installed in the assembly" - ); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_empty_messages_emissions_proposal() { - let mut app = mock_app(); - let owner = Addr::unchecked("generator_controller"); - - let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); - let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { - title: "Emissions Test title!".to_string(), - description: "Emissions Test description!".to_string(), - messages: vec![], - ibc_channel: None, - }; - - let err = app - .execute_contract( - Addr::unchecked("generator_controller"), - assembly_addr, - &emissions_proposal_msg, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "The proposal has no messages to execute" - ); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_unauthorised_emissions_proposal() { - use cosmwasm_std::BankMsg; - - let mut app = mock_app(); - let owner = Addr::unchecked("generator_controller"); - - let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); - let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { - title: "Emissions Test title!".to_string(), - description: "Emissions Test description!".to_string(), - // Sample message to use as we don't have IBC or the Generator to set emissions on - messages: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: "generator_controller".into(), - amount: coins(1, "uluna"), - })], - ibc_channel: None, - }; - - let err = app - .execute_contract( - Addr::unchecked("not_generator_controller"), - assembly_addr, - &emissions_proposal_msg, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Unauthorized"); -} - -#[test] -fn test_voting_power_changes() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = - instantiate_contracts(&mut app, owner, false, false); - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - // Mint tokens for casting votes at start block - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user1"), - 40000_000000, - ); - - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - guardian_addr: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user2"), - 5000_000000, - ); - - app.update_block(next_block); - - // user1 can vote as he had voting power before the proposal submitting. - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user1"), - ProposalVoteOption::For, - ) - .unwrap(); - // Should panic, because user2 doesn't have any voting power. - let err = cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user2"), - ProposalVoteOption::Against, - ) - .unwrap_err(); - - // user2 doesn't have voting power and doesn't affect on total voting power(total supply at) - // total supply = 5000 - assert_eq!( - err.root_cause().to_string(), - "You don't have any voting power!" - ); - - app.update_block(next_block); - - // Skip voting period and delay - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi - .time - .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - // End proposal - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - // Check proposal votes - assert_eq!(proposal.for_power, Uint128::from(40000_000000u128)); - assert_eq!(proposal.against_power, Uint128::zero()); - // Should be passed, as total_voting_power=5000, for_votes=40000. - // So user2 didn't affect the result. Because he had to have xASTRO before the vote was submitted. - assert_eq!(proposal.status, ProposalStatus::Passed); -} - -#[test] -fn test_fail_outpost_vote_without_hub() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = - instantiate_contracts(&mut app, owner, false, false); - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - // Mint tokens for casting votes at start block - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user1"), - 40000_000000, - ); - - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - guardian_addr: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user2"), - 5000_000000, - ); - - app.update_block(next_block); - - // user1 can not vote from an Outpost due to no Hub contract set - let err = cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("invalid_contract"), - Addr::unchecked("user1"), - ProposalVoteOption::For, - Uint128::from(100u64), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Sender is not the Hub installed in the assembly" - ); -} - -#[test] -fn test_outpost_vote() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (astro_token, staking_instance, xastro_addr, _, _, assembly_addr, _, hub_addr) = - instantiate_contracts(&mut app, owner.clone(), false, true); - - let user1_voting_power = 10_000_000_000; - let user2_voting_power = 5_000_000_000; - let remote_user1_voting_power = 80_000_000_000u128; - // let remote_user2_voting_power = 3_000_000_000u128; - - let hub_addr = hub_addr.unwrap(); - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - // Mint tokens for casting votes at start block - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user1"), - user1_voting_power, - ); - - // Mint tokens for casting votes against vote at start block - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user2"), - user2_voting_power, - ); - - // Mint ASTRO to stake - mint_tokens( - &mut app, - &owner, - &astro_token, - &Addr::unchecked("cw20ics20"), - 1_000_000_000_000u128, - ); - - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - guardian_addr: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - - app.update_block(next_block); - - // Outpost votes won't be accepted from other addresses - let err = cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("other_contract"), - Addr::unchecked("remote1"), - ProposalVoteOption::For, - Uint128::from(remote_user1_voting_power), - ) - .unwrap_err(); - assert_eq!(err.root_cause().to_string(), "Unauthorized"); - - // Attempts to vote with no xASTRO minted on Outposts - let err = cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - hub_addr, - Addr::unchecked("remote1"), - ProposalVoteOption::For, - Uint128::from(remote_user1_voting_power), - ) - .unwrap_err(); - assert_eq!( - err.root_cause().to_string(), - "Voting power exceeds maximum Outpost power" - ); - - // Note: Due to cw-multitest not supporting IBC messages we can no longer - // test voting with Outpost voting power - - // app.execute_contract( - // owner, - // hub_addr.clone(), - // &astroport_governance::hub::ExecuteMsg::AddOutpost { - // outpost_addr: "outpost1".to_string(), - // outpost_channel: "channel-3".to_string(), - // cw20_ics20_channel: "channel-1".to_string(), - // }, - // &[], - // ) - // .unwrap_err(); - - // Stake some ASTRO from an Outpost - // stake_remote_astro( - // &mut app, - // Addr::unchecked("cw20ics20".to_string()), - // hub_addr.clone(), - // astro_token, - // Uint128::from(remote_user1_voting_power), - // ) - // .unwrap_err(); - - // Continue normally - // cast_outpost_vote( - // &mut app, - // assembly_addr.clone(), - // 1, - // hub_addr.clone(), - // Addr::unchecked("remote1"), - // ProposalVoteOption::For, - // Uint128::from(remote_user1_voting_power), - // ) - // .unwrap(); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_block_height_selection() { - // Block height is 12345 after app initialization - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - let user1 = Addr::unchecked("user1"); - let user2 = Addr::unchecked("user2"); - let user3 = Addr::unchecked("user3"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = - instantiate_contracts(&mut app, owner, false, false); - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user1, - 6000_000001, - ); - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user2, - 4000_000000, - ); - - // Skip to the next period - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - None, - ); - - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - user1, - ProposalVoteOption::For, - ) - .unwrap(); - - // Mint huge amount of xASTRO. These tokens cannot affect on total supply in proposal 1 because - // they were minted after proposal.start_block - 1 - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user3, - 100000_000000, - ); - // Mint more xASTRO to user2, who will vote against the proposal, what is enough to make proposal unsuccessful. - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user2, - 3000_000000, - ); - // Total voting power should be 20k xASTRO (proposal minimum deposit 10k + 4k + 6k users VP) - check_total_vp(&mut app, &assembly_addr, 1, 20000_000001); - - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - user2, - ProposalVoteOption::Against, - ) - .unwrap(); - - // Skip voting period - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi - .time - .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - // End proposal - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.for_power, Uint128::new(6000_000001)); - // Against power is 4000, as user2's balance was increased after proposal.start_block - 1 - // at which everyone's voting power are considered. - assert_eq!(proposal.against_power, Uint128::new(4000_000000)); - // Proposal is passed, as the total supply was increased after proposal.start_block - 1. - assert_eq!(proposal.status, ProposalStatus::Passed); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_unsuccessful_proposal() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = - instantiate_contracts(&mut app, owner, false, false); - - // Init voting power for users - let xastro_balances: Vec<(&str, u128)> = vec![ - ("user0", PROPOSAL_REQUIRED_DEPOSIT), // proposal submitter - ("user1", 100), - ("user2", 200), - ("user3", 400), - ("user4", 250), - ("user5", 90), - ("user6", 300), - ("user7", 30), - ("user8", 180), - ("user9", 50), - ("user10", 90), - ("user11", 500), - ]; - - for (addr, xastro) in xastro_balances { - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked(addr), - xastro, - ); - } - - // Skip period - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - None, - ); - - let expected_voting_power: Vec<(&str, ProposalVoteOption)> = vec![ - ("user1", ProposalVoteOption::For), - ("user2", ProposalVoteOption::For), - ("user3", ProposalVoteOption::For), - ("user4", ProposalVoteOption::Against), - ("user5", ProposalVoteOption::Against), - ("user6", ProposalVoteOption::Against), - ("user7", ProposalVoteOption::Against), - ("user8", ProposalVoteOption::Against), - ("user9", ProposalVoteOption::Against), - ("user10", ProposalVoteOption::Against), - ]; - - for (addr, option) in expected_voting_power { - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked(addr), - option, - ) - .unwrap(); - } - - // Skip voting period - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); - }); - - // Check balance of submitter before and after proposal completion - check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - check_token_balance( - &mut app, - &xastro_addr, - &Addr::unchecked("user0"), - 10000_000000, - ); - - // Check proposal status - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.status, ProposalStatus::Rejected); - - // Remove expired proposal - app.update_block(|bi| { - bi.height += PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi - .time - .plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let res: ProposalListResponse = app - .wrap() - .query_wasm_smart( - assembly_addr.to_string(), - &QueryMsg::Proposals { - start: None, - limit: None, - }, - ) - .unwrap(); - - assert_eq!(res.proposal_list, vec![]); - // proposal_count should not be changed after removing - assert_eq!(res.proposal_count, Uint64::from(1u32)); -} +// use std::str::FromStr; +// +// use astroport::{ +// token::InstantiateMsg as TokenInstantiateMsg, xastro_token::QueryMsg as XAstroQueryMsg, +// }; +// use cosmwasm_std::coins; +// use cosmwasm_std::{ +// testing::{mock_env, MockApi, MockStorage}, +// to_json_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, +// Uint64, WasmMsg, WasmQuery, +// }; +// use cw_multi_test::{ +// next_block, App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor, +// }; +// +// use astroport_governance::assembly::{ +// Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalListResponse, ProposalStatus, +// ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, DEPOSIT_INTERVAL, +// VOTING_PERIOD_INTERVAL, +// }; +// use astroport_governance::builder_unlock::msg::{ +// InstantiateMsg as BuilderUnlockInstantiateMsg, ReceiveMsg as BuilderUnlockReceiveMsg, +// }; +// use astroport_governance::builder_unlock::{AllocationParams, Schedule}; +// use astroport_governance::hub::InstantiateMsg as HubInstantiateMsg; +// use astroport_governance::utils::{EPOCH_START, WEEK}; +// use astroport_governance::voting_escrow_lite::{ +// Cw20HookMsg as VXAstroCw20HookMsg, InstantiateMsg as VXAstroInstantiateMsg, +// }; +// +// mod common; +// +// const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); +// const PROPOSAL_EFFECTIVE_DELAY: u64 = 12_342; +// const PROPOSAL_EXPIRATION_PERIOD: u64 = 86_399; +// const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); +// const PROPOSAL_REQUIRED_QUORUM: &str = "0.50"; +// const PROPOSAL_REQUIRED_THRESHOLD: &str = "0.60"; +// + +mod common; + +use crate::common::helper::Helper; +use cosmwasm_std::Addr; #[test] -fn test_check_messages() { - let mut app = mock_app(); +fn test_new_suite() { let owner = Addr::unchecked("owner"); - let (_, _, _, vxastro_addr, _, assembly_addr, _, _) = - instantiate_contracts(&mut app, owner, false, false); - - change_owner(&mut app, &vxastro_addr, &assembly_addr); - let user = Addr::unchecked("user"); - let into_check_msg = |msgs: Vec<(String, Binary)>| { - let messages = msgs - .into_iter() - .map(|(contract_addr, msg)| { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds: vec![], - }) - }) - .collect(); - ExecuteMsg::CheckMessages { messages } - }; - - let config_before: astroport_governance::voting_escrow_lite::Config = app - .wrap() - .query_wasm_smart( - &vxastro_addr, - &astroport_governance::voting_escrow_lite::QueryMsg::Config {}, - ) - .unwrap(); - - let vxastro_blacklist_msg = vec![( - vxastro_addr.to_string(), - to_json_binary( - &astroport_governance::voting_escrow_lite::ExecuteMsg::UpdateConfig { - new_guardian: None, - generator_controller: None, - outpost: None, - }, - ) - .unwrap(), - )]; - let err = app - .execute_contract( - user, - assembly_addr.clone(), - &into_check_msg(vxastro_blacklist_msg), - &[], - ) - .unwrap_err(); - assert_eq!( - &err.root_cause().to_string(), - "Messages check passed. Nothing was committed to the blockchain" - ); - - let config_after: astroport_governance::voting_escrow_lite::Config = app - .wrap() - .query_wasm_smart( - &vxastro_addr, - &astroport_governance::voting_escrow_lite::QueryMsg::Config {}, - ) - .unwrap(); - assert_eq!(config_before, config_after); -} - -fn mock_app() -> App { - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(EPOCH_START); - let api = MockApi::default(); - let bank = BankKeeper::new(); - let storage = MockStorage::new(); - - AppBuilder::new() - .with_api(api) - .with_block(env.block) - .with_bank(bank) - .with_storage(storage) - .build(|_, _, _| {}) -} - -fn instantiate_contracts( - router: &mut App, - owner: Addr, - with_generator_controller: bool, - with_hub: bool, -) -> ( - Addr, - Addr, - Addr, - Addr, - Addr, - Addr, - Option, - Option, -) { - let token_addr = instantiate_astro_token(router, &owner); - let (staking_addr, xastro_token_addr) = instantiate_xastro_token(router, &owner, &token_addr); - let vxastro_token_addr = instantiate_vxastro_token(router, &owner, &xastro_token_addr); - let builder_unlock_addr = instantiate_builder_unlock_contract(router, &owner, &token_addr); - - // If we want to test immediate proposals we need to set the address - // for the generator controller. Deploying the generator controller in this - // test would require deploying factory, tokens and pools. That test is - // better suited in the generator controller itself. Thus, we use the owner - // address as the generator controller address to test immediate proposals. - let mut generator_controller_addr = None; - - if with_generator_controller { - generator_controller_addr = Some(owner.to_string()); - } - - let mut hub_addr = None; - - if with_hub { - hub_addr = Some(instantiate_hub( - router, - &owner, - &Addr::unchecked("contract6".to_string()), - &staking_addr, - )); - } - - let assembly_addr = instantiate_assembly_contract( - router, - &owner, - &xastro_token_addr, - &vxastro_token_addr, - &builder_unlock_addr, - None, - generator_controller_addr, - hub_addr.clone(), - ); - - ( - token_addr, - staking_addr, - xastro_token_addr, - vxastro_token_addr, - builder_unlock_addr, - assembly_addr, - None, - hub_addr, - ) -} - -fn instantiate_astro_token(router: &mut App, owner: &Addr) -> Addr { - let astro_token_contract = Box::new(ContractWrapper::new_with_empty( - astroport_token::contract::execute, - astroport_token::contract::instantiate, - astroport_token::contract::query, - )); - - let astro_token_code_id = router.store_code(astro_token_contract); - - let msg = TokenInstantiateMsg { - name: String::from("Astro token"), - symbol: String::from("ASTRO"), - decimals: 6, - initial_balances: vec![], - mint: Some(MinterResponse { - minter: owner.to_string(), - cap: None, - }), - marketing: None, - }; - - router - .instantiate_contract( - astro_token_code_id, - owner.clone(), - &msg, - &[], - String::from("ASTRO"), - None, - ) - .unwrap() -} - -fn instantiate_xastro_token(router: &mut App, owner: &Addr, astro_token: &Addr) -> (Addr, Addr) { - let xastro_contract = Box::new(ContractWrapper::new_with_empty( - astroport_xastro_token::contract::execute, - astroport_xastro_token::contract::instantiate, - astroport_xastro_token::contract::query, - )); - - let xastro_code_id = router.store_code(xastro_contract); - - let staking_contract = Box::new( - ContractWrapper::new_with_empty( - astroport_staking::contract::execute, - astroport_staking::contract::instantiate, - astroport_staking::contract::query, - ) - .with_reply_empty(astroport_staking::contract::reply), - ); - - let staking_code_id = router.store_code(staking_contract); - - let msg = astroport::staking::InstantiateMsg { - owner: owner.to_string(), - token_code_id: xastro_code_id, - deposit_token_addr: astro_token.to_string(), - marketing: None, - }; - let staking_instance = router - .instantiate_contract( - staking_code_id, - owner.clone(), - &msg, - &[], - String::from("xASTRO"), - None, - ) - .unwrap(); - - let res = router - .wrap() - .query::(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: staking_instance.to_string(), - msg: to_json_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), - })) - .unwrap(); - - (staking_instance, res.share_token_addr) -} - -fn instantiate_vxastro_token(router: &mut App, owner: &Addr, xastro: &Addr) -> Addr { - let vxastro_token_contract = Box::new(ContractWrapper::new_with_empty( - voting_escrow_lite::execute::execute, - voting_escrow_lite::contract::instantiate, - voting_escrow_lite::query::query, - )); - - let vxastro_token_code_id = router.store_code(vxastro_token_contract); - - let msg = VXAstroInstantiateMsg { - owner: owner.to_string(), - guardian_addr: Some(owner.to_string()), - deposit_token_addr: xastro.to_string(), - generator_controller_addr: None, - outpost_addr: None, - marketing: None, - logo_urls_whitelist: vec![], - }; - - router - .instantiate_contract( - vxastro_token_code_id, - owner.clone(), - &msg, - &[], - String::from("vxASTRO"), - None, - ) - .unwrap() -} - -fn instantiate_hub( - router: &mut App, - owner: &Addr, - assembly_addr: &Addr, - staking_addr: &Addr, -) -> Addr { - let hub_contract = Box::new( - ContractWrapper::new_with_empty( - astroport_hub::execute::execute, - astroport_hub::contract::instantiate, - astroport_hub::query::query, - ) - .with_reply(astroport_hub::reply::reply), - ); - - let hub_code_id = router.store_code(hub_contract); - - let msg = HubInstantiateMsg { - owner: owner.to_string(), - assembly_addr: assembly_addr.to_string(), - cw20_ics20_addr: "cw20ics20".to_string(), - generator_controller_addr: "unknown".to_string(), - ibc_timeout_seconds: 60, - staking_addr: staking_addr.to_string(), - }; - - router - .instantiate_contract( - hub_code_id, - owner.clone(), - &msg, - &[], - String::from("Hub"), - None, - ) - .unwrap() -} - -fn instantiate_builder_unlock_contract(router: &mut App, owner: &Addr, astro_token: &Addr) -> Addr { - let builder_unlock_contract = Box::new(ContractWrapper::new_with_empty( - builder_unlock::contract::execute, - builder_unlock::contract::instantiate, - builder_unlock::contract::query, - )); - - let builder_unlock_code_id = router.store_code(builder_unlock_contract); - - let msg = BuilderUnlockInstantiateMsg { - owner: owner.to_string(), - astro_token: astro_token.to_string(), - max_allocations_amount: Uint128::new(300_000_000_000_000u128), - }; - - router - .instantiate_contract( - builder_unlock_code_id, - owner.clone(), - &msg, - &[], - "Builder Unlock contract".to_string(), - Some(owner.to_string()), - ) - .unwrap() -} - -#[allow(clippy::too_many_arguments)] -fn instantiate_assembly_contract( - router: &mut App, - owner: &Addr, - xastro: &Addr, - vxastro: &Addr, - builder: &Addr, - delegator: Option, - generator_controller_addr: Option, - hub_addr: Option, -) -> Addr { - let assembly_contract = Box::new(ContractWrapper::new_with_empty( - astro_assembly::contract::execute, - astro_assembly::contract::instantiate, - astro_assembly::contract::query, - )); - - let assembly_code = router.store_code(assembly_contract); - - let hub: Option = hub_addr.as_ref().map(|s| s.to_string()); - - let msg = InstantiateMsg { - xastro_token_addr: xastro.to_string(), - vxastro_token_addr: Some(vxastro.to_string()), - voting_escrow_delegator_addr: delegator, - ibc_controller: None, - generator_controller_addr, - hub_addr: hub, - builder_unlock_addr: builder.to_string(), - proposal_voting_period: PROPOSAL_VOTING_PERIOD, - proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, - proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, - proposal_required_deposit: Uint128::new(PROPOSAL_REQUIRED_DEPOSIT), - proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), - proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), - whitelisted_links: vec!["https://some.link/".to_string()], - }; - - router - .instantiate_contract( - assembly_code, - owner.clone(), - &msg, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap() -} - -fn mint_tokens(app: &mut App, minter: &Addr, token: &Addr, recipient: &Addr, amount: u128) { - let msg = Cw20ExecuteMsg::Mint { - recipient: recipient.to_string(), - amount: Uint128::from(amount), - }; - - app.execute_contract(minter.clone(), token.to_owned(), &msg, &[]) - .unwrap(); -} - -fn mint_vxastro( - app: &mut App, - staking_instance: &Addr, - xastro: Addr, - vxastro: &Addr, - recipient: Addr, - amount: u128, -) { - mint_tokens(app, staking_instance, &xastro, &recipient, amount); - - let msg = Cw20ExecuteMsg::Send { - contract: vxastro.to_string(), - amount: Uint128::from(amount), - msg: to_json_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), - }; - - app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); -} - -fn create_allocations( - app: &mut App, - token: Addr, - builder_unlock_contract_addr: Addr, - allocations: Vec<(String, AllocationParams)>, -) { - let amount = allocations - .iter() - .map(|params| params.1.amount.u128()) - .sum(); - mint_tokens( - app, - &Addr::unchecked("owner"), - &token, - &Addr::unchecked("owner"), - amount, - ); - - app.execute_contract( - Addr::unchecked("owner"), - Addr::unchecked(token.to_string()), - &Cw20ExecuteMsg::Send { - contract: builder_unlock_contract_addr.to_string(), - amount: Uint128::from(amount), - msg: to_json_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }) - .unwrap(), - }, - &[], - ) - .unwrap(); -} - -fn create_proposal( - app: &mut App, - token: &Addr, - assembly: &Addr, - submitter: Addr, - msgs: Option>, -) { - let submit_proposal_msg = Cw20HookMsg::SubmitProposal { - title: "Test title!".to_string(), - description: "Test description!".to_string(), - link: None, - messages: msgs, - ibc_channel: None, - }; - - app.execute_contract( - submitter, - token.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly.to_string(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - msg: to_json_binary(&submit_proposal_msg).unwrap(), - }, - &[], - ) - .unwrap(); -} - -fn check_token_balance(app: &mut App, token: &Addr, address: &Addr, expected: u128) { - let msg = XAstroQueryMsg::Balance { - address: address.to_string(), - }; - let res: StdResult = app.wrap().query_wasm_smart(token, &msg); - assert_eq!(res.unwrap().balance, Uint128::from(expected)); -} - -fn check_user_vp(app: &mut App, assembly: &Addr, address: &Addr, proposal_id: u64, expected: u128) { - let res: Uint128 = app - .wrap() - .query_wasm_smart( - assembly.to_string(), - &QueryMsg::UserVotingPower { - user: address.to_string(), - proposal_id, - }, - ) - .unwrap(); - - assert_eq!(res.u128(), expected); -} - -fn check_total_vp(app: &mut App, assembly: &Addr, proposal_id: u64, expected: u128) { - let res: Uint128 = app - .wrap() - .query_wasm_smart( - assembly.to_string(), - &QueryMsg::TotalVotingPower { proposal_id }, - ) - .unwrap(); - - assert_eq!(res.u128(), expected); + let mut helper = Helper::new(&owner).unwrap(); } -fn cast_vote( - app: &mut App, - assembly: Addr, - proposal_id: u64, - sender: Addr, - option: ProposalVoteOption, -) -> anyhow::Result { - app.execute_contract( - sender, - assembly, - &ExecuteMsg::CastVote { - proposal_id, - vote: option, - }, - &[], - ) -} - -fn cast_outpost_vote( - app: &mut App, - assembly: Addr, - proposal_id: u64, - sender: Addr, - voter: Addr, - option: ProposalVoteOption, - voting_power: Uint128, -) -> anyhow::Result { - app.execute_contract( - sender, - assembly, - &ExecuteMsg::CastOutpostVote { - proposal_id, - voter: voter.to_string(), - vote: option, - voting_power, - }, - &[], - ) -} - -// Add back once cw-multitest supports IBC -// fn stake_remote_astro( -// app: &mut App, -// sender: Addr, -// hub: Addr, -// astro_token: Addr, -// amount: Uint128, -// ) -> anyhow::Result { -// let cw20_msg = to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { -// channel: "channel-1".to_string(), -// sender: "remoteuser1".to_string(), -// receiver: hub.to_string(), -// memo: "{\"stake\":{}}".to_string(), +// #[test] +// fn test_contract_instantiation() { +// let mut app = mock_app(); +// +// let owner = Addr::unchecked("owner"); +// +// // Instantiate needed contracts +// let token_addr = instantiate_astro_token(&mut app, &owner); +// let (staking_addr, xastro_token_addr) = instantiate_xastro_token(&mut app, &owner, &token_addr); +// let vxastro_token_addr = instantiate_vxastro_token(&mut app, &owner, &xastro_token_addr); +// let builder_unlock_addr = instantiate_builder_unlock_contract(&mut app, &owner, &token_addr); +// +// let assembly_contract = Box::new(ContractWrapper::new_with_empty( +// astro_assembly::contract::execute, +// astro_assembly::contract::instantiate, +// astro_assembly::queries::query, +// )); +// +// let assembly_code = app.store_code(assembly_contract); +// +// let assembly_default_instantiate_msg = InstantiateMsg { +// staking_addr: staking_addr.to_string(), +// vxastro_token_addr: Some(vxastro_token_addr.to_string()), +// voting_escrow_delegator_addr: None, +// ibc_controller: None, +// generator_controller_addr: None, +// hub_addr: None, +// builder_unlock_addr: builder_unlock_addr.to_string(), +// proposal_voting_period: PROPOSAL_VOTING_PERIOD, +// proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, +// proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, +// proposal_required_deposit: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), +// proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), +// whitelisted_links: vec!["https://some.link/".to_string()], +// }; +// +// // Try to instantiate assembly with wrong threshold +// let err = app +// .instantiate_contract( +// assembly_code, +// owner.clone(), +// &InstantiateMsg { +// proposal_required_threshold: "0.3".to_string(), +// ..assembly_default_instantiate_msg.clone() +// }, +// &[], +// "Assembly".to_string(), +// Some(owner.to_string()), +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" +// ); +// +// let err = app +// .instantiate_contract( +// assembly_code, +// owner.clone(), +// &InstantiateMsg { +// proposal_required_threshold: "1.1".to_string(), +// ..assembly_default_instantiate_msg.clone() +// }, +// &[], +// "Assembly".to_string(), +// Some(owner.to_string()), +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" +// ); +// +// let err = app +// .instantiate_contract( +// assembly_code, +// owner.clone(), +// &InstantiateMsg { +// proposal_required_quorum: "1.1".to_string(), +// ..assembly_default_instantiate_msg.clone() +// }, +// &[], +// "Assembly".to_string(), +// Some(owner.to_string()), +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: The required quorum for a proposal cannot be lower than 1% or higher than 100%" +// ); +// +// let err = app +// .instantiate_contract( +// assembly_code, +// owner.clone(), +// &InstantiateMsg { +// proposal_expiration_period: 500, +// ..assembly_default_instantiate_msg.clone() +// }, +// &[], +// "Assembly".to_string(), +// Some(owner.to_string()), +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: The expiration period for a proposal cannot be lower than 12342 or higher than 100800" +// ); +// +// let err = app +// .instantiate_contract( +// assembly_code, +// owner.clone(), +// &InstantiateMsg { +// proposal_effective_delay: 400, +// ..assembly_default_instantiate_msg.clone() +// }, +// &[], +// "Assembly".to_string(), +// Some(owner.to_string()), +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: The effective delay for a proposal cannot be lower than 6171 or higher than 14400" +// ); +// +// let assembly_instance = app +// .instantiate_contract( +// assembly_code, +// owner.clone(), +// &assembly_default_instantiate_msg, +// &[], +// "Assembly".to_string(), +// Some(owner.to_string()), +// ) +// .unwrap(); +// +// let res: Config = app +// .wrap() +// .query_wasm_smart(assembly_instance, &QueryMsg::Config {}) +// .unwrap(); +// +// assert_eq!(res.xastro_token_addr, xastro_token_addr); +// assert_eq!(res.builder_unlock_addr, builder_unlock_addr); +// assert_eq!(res.proposal_voting_period, PROPOSAL_VOTING_PERIOD); +// assert_eq!(res.proposal_effective_delay, PROPOSAL_EFFECTIVE_DELAY); +// assert_eq!(res.proposal_expiration_period, PROPOSAL_EXPIRATION_PERIOD); +// assert_eq!( +// res.proposal_required_deposit, +// Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) +// ); +// assert_eq!( +// res.proposal_required_quorum, +// Decimal::from_str(PROPOSAL_REQUIRED_QUORUM).unwrap() +// ); +// assert_eq!( +// res.proposal_required_threshold, +// Decimal::from_str(PROPOSAL_REQUIRED_THRESHOLD).unwrap() +// ); +// assert_eq!( +// res.whitelisted_links, +// vec!["https://some.link/".to_string(),] +// ); +// } +// +// #[test] +// fn test_proposal_submitting() { +// let mut app = mock_app(); +// +// let owner = Addr::unchecked("owner"); +// let user = Addr::unchecked("user1"); +// +// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = +// instantiate_contracts(&mut app, owner, false, false); +// +// let proposals: ProposalListResponse = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::Proposals { +// start: None, +// limit: None, +// }, +// ) +// .unwrap(); +// +// assert_eq!(proposals.proposal_count, Uint64::from(0u32)); +// assert_eq!(proposals.proposal_list, vec![]); +// +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &user, +// PROPOSAL_REQUIRED_DEPOSIT, +// ); +// +// check_token_balance(&mut app, &xastro_addr, &user, PROPOSAL_REQUIRED_DEPOSIT); +// +// // Try to create proposal with insufficient token deposit +// let submit_proposal_msg = Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("Title"), +// description: String::from("Description"), +// link: Some(String::from("https://some.link")), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT - 1), +// }; +// +// let err = app +// .execute_contract(user.clone(), xastro_addr.clone(), &submit_proposal_msg, &[]) +// .unwrap_err(); +// +// assert_eq!(err.root_cause().to_string(), "Insufficient token deposit!"); +// +// // Try to create a proposal with wrong title +// let err = app +// .execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("X"), +// description: String::from("Description"), +// link: Some(String::from("https://some.link/")), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: Title too short!" +// ); +// +// let err = app +// .execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from_utf8(vec![b'X'; 65]).unwrap(), +// description: String::from("Description"), +// link: Some(String::from("https://some.link/")), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: Title too long!" +// ); +// +// // Try to create a proposal with wrong description +// let err = app +// .execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("Title"), +// description: String::from("X"), +// link: Some(String::from("https://some.link/")), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: Description too short!" +// ); +// +// let err = app +// .execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("Title"), +// description: String::from_utf8(vec![b'X'; 1025]).unwrap(), +// link: Some(String::from("https://some.link/")), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: Description too long!" +// ); +// +// // Try to create a proposal with wrong link +// let err = app +// .execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("Title"), +// description: String::from("Description"), +// link: Some(String::from("X")), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: Link too short!" +// ); +// +// let err = app +// .execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("Title"), +// description: String::from("Description"), +// link: Some(String::from_utf8(vec![b'X'; 129]).unwrap()), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: Link too long!" +// ); +// +// let err = app +// .execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("Title"), +// description: String::from("Description"), +// link: Some(String::from("https://some1.link")), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: Link is not whitelisted!" +// ); +// +// let err = app +// .execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("Title"), +// description: String::from("Description"), +// link: Some(String::from( +// "https://some.link/", +// )), +// messages: None, +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Generic error: Link is not properly formatted or contains unsafe characters!" +// ); +// +// // Valid proposal submission +// app.execute_contract( +// user.clone(), +// xastro_addr.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly_addr.to_string(), +// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { +// title: String::from("Title"), +// description: String::from("Description"), +// link: Some(String::from("https://some.link/q/")), +// messages: Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: assembly_addr.to_string(), +// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { +// xastro_token_addr: None, +// vxastro_token_addr: None, +// voting_escrow_delegator_addr: None, +// ibc_controller: None, +// generator_controller: None, +// hub: None, +// builder_unlock_addr: None, +// proposal_voting_period: Some(750), +// proposal_effective_delay: None, +// proposal_expiration_period: None, +// proposal_required_deposit: None, +// proposal_required_quorum: None, +// proposal_required_threshold: None, +// whitelist_add: None, +// whitelist_remove: None, +// guardian_addr: None, +// }))) +// .unwrap(), +// funds: vec![], +// })]), +// ibc_channel: None, +// }) +// .unwrap(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// }, +// &[], +// ) +// .unwrap(); +// +// let proposal: Proposal = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::Proposal { proposal_id: 1 }, +// ) +// .unwrap(); +// +// assert_eq!(proposal.proposal_id, Uint64::from(1u64)); +// assert_eq!(proposal.submitter, user); +// assert_eq!(proposal.status, ProposalStatus::Active); +// assert_eq!(proposal.for_power, Uint128::zero()); +// assert_eq!(proposal.against_power, Uint128::zero()); +// assert_eq!(proposal.start_block, 12_345); +// assert_eq!(proposal.end_block, 12_345 + PROPOSAL_VOTING_PERIOD); +// assert_eq!(proposal.title, String::from("Title")); +// assert_eq!(proposal.description, String::from("Description")); +// assert_eq!(proposal.link, Some(String::from("https://some.link/q/"))); +// assert_eq!( +// proposal.messages, +// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: assembly_addr.to_string(), +// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { +// xastro_token_addr: None, +// vxastro_token_addr: None, +// voting_escrow_delegator_addr: None, +// ibc_controller: None, +// generator_controller: None, +// hub: None, +// builder_unlock_addr: None, +// proposal_voting_period: Some(750), +// proposal_effective_delay: None, +// proposal_expiration_period: None, +// proposal_required_deposit: None, +// proposal_required_quorum: None, +// proposal_required_threshold: None, +// whitelist_add: None, +// whitelist_remove: None, +// guardian_addr: None, +// }))) +// .unwrap(), +// funds: vec![], +// })]) +// ); +// assert_eq!( +// proposal.deposit_amount, +// Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) +// ) +// } +// +// #[test] +// fn test_successful_proposal() { +// let mut app = mock_app(); +// +// let owner = Addr::unchecked("owner"); +// +// let ( +// token_addr, +// staking_instance, +// xastro_addr, +// vxastro_addr, +// builder_unlock_addr, +// assembly_addr, +// _, +// _, +// ) = instantiate_contracts(&mut app, owner, false, false); +// +// // Init voting power for users +// let balances: Vec<(&str, u128, u128)> = vec![ +// ("user0", PROPOSAL_REQUIRED_DEPOSIT, 0), // proposal submitter +// ("user1", 20, 80), +// ("user2", 100, 100), +// ("user3", 300, 100), +// ("user4", 200, 50), +// ("user5", 0, 90), +// ("user6", 100, 200), +// ("user7", 30, 0), +// ("user8", 80, 100), +// ("user9", 50, 0), +// ("user10", 0, 90), +// ("user11", 500, 0), +// ("user12", 10000_000000, 0), +// ]; +// +// let default_allocation_params = AllocationParams { +// amount: Uint128::zero(), +// unlock_schedule: Schedule { +// start_time: 12_345, +// cliff: 5, +// duration: 500, +// percent_at_cliff: None, +// }, +// proposed_receiver: None, +// }; +// +// let locked_balances = vec![ +// ( +// "user1".to_string(), +// AllocationParams { +// amount: Uint128::from(80u32), +// ..default_allocation_params.clone() +// }, +// ), +// ( +// "user4".to_string(), +// AllocationParams { +// amount: Uint128::from(50u32), +// ..default_allocation_params.clone() +// }, +// ), +// ( +// "user7".to_string(), +// AllocationParams { +// amount: Uint128::from(100u32), +// ..default_allocation_params.clone() +// }, +// ), +// ( +// "user10".to_string(), +// AllocationParams { +// amount: Uint128::from(30u32), +// ..default_allocation_params +// }, +// ), +// ]; +// +// for (addr, xastro, vxastro) in balances { +// if xastro > 0 { +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked(addr), +// xastro, +// ); +// } +// +// if vxastro > 0 { +// mint_vxastro( +// &mut app, +// &staking_instance, +// xastro_addr.clone(), +// &vxastro_addr, +// Addr::unchecked(addr), +// vxastro, +// ); +// } +// } +// +// create_allocations(&mut app, token_addr, builder_unlock_addr, locked_balances); +// +// // Skip period +// app.update_block(|mut block| { +// block.time = block.time.plus_seconds(WEEK); +// block.height += WEEK / 5; +// }); +// +// // Create default proposal +// create_proposal( +// &mut app, +// &xastro_addr, +// &assembly_addr, +// Addr::unchecked("user0"), +// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: assembly_addr.to_string(), +// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { +// xastro_token_addr: None, +// vxastro_token_addr: None, +// voting_escrow_delegator_addr: None, +// ibc_controller: None, +// generator_controller: None, +// hub: None, +// builder_unlock_addr: None, +// proposal_voting_period: Some(PROPOSAL_VOTING_PERIOD + 1000), +// proposal_effective_delay: None, +// proposal_expiration_period: None, +// proposal_required_deposit: None, +// proposal_required_quorum: None, +// proposal_required_threshold: None, +// whitelist_add: Some(vec![ +// "https://some1.link/".to_string(), +// "https://some2.link/".to_string(), +// ]), +// whitelist_remove: Some(vec!["https://some.link/".to_string()]), +// guardian_addr: None, +// }))) +// .unwrap(), +// funds: vec![], +// })]), +// ); +// +// let votes: Vec<(&str, ProposalVoteOption, u128)> = vec![ +// ("user1", ProposalVoteOption::For, 180u128), +// ("user2", ProposalVoteOption::For, 200u128), +// ("user3", ProposalVoteOption::For, 400u128), +// ("user4", ProposalVoteOption::For, 300u128), +// ("user5", ProposalVoteOption::For, 90u128), +// ("user6", ProposalVoteOption::For, 300u128), +// ("user7", ProposalVoteOption::For, 130u128), +// ("user8", ProposalVoteOption::Against, 180u128), +// ("user9", ProposalVoteOption::Against, 50u128), +// ("user10", ProposalVoteOption::Against, 120u128), +// ("user11", ProposalVoteOption::Against, 500u128), +// ("user12", ProposalVoteOption::For, 10000_000000u128), +// ]; +// +// check_total_vp(&mut app, &assembly_addr, 1, 20000002450); +// +// for (addr, option, expected_vp) in votes { +// let sender = Addr::unchecked(addr); +// +// check_user_vp(&mut app, &assembly_addr, &sender, 1, expected_vp); +// +// cast_vote(&mut app, assembly_addr.clone(), 1, sender, option).unwrap(); +// } +// +// let proposal: Proposal = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::Proposal { proposal_id: 1 }, +// ) +// .unwrap(); +// +// let proposal_votes: ProposalVotesResponse = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::ProposalVotes { proposal_id: 1 }, +// ) +// .unwrap(); +// +// let proposal_for_voters: Vec = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::ProposalVoters { +// proposal_id: 1, +// vote_option: ProposalVoteOption::For, +// start: None, +// limit: None, +// }, +// ) +// .unwrap(); +// +// let proposal_against_voters: Vec = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::ProposalVoters { +// proposal_id: 1, +// vote_option: ProposalVoteOption::Against, +// start: None, +// limit: None, +// }, +// ) +// .unwrap(); +// +// // Check proposal votes +// assert_eq!(proposal.for_power, Uint128::from(10000001600u128)); +// assert_eq!(proposal.against_power, Uint128::from(850u32)); +// +// assert_eq!(proposal_votes.for_power, Uint128::from(10000001600u128)); +// assert_eq!(proposal_votes.against_power, Uint128::from(850u32)); +// +// assert_eq!( +// proposal_for_voters, +// vec![ +// Addr::unchecked("user1"), +// Addr::unchecked("user2"), +// Addr::unchecked("user3"), +// Addr::unchecked("user4"), +// Addr::unchecked("user5"), +// Addr::unchecked("user6"), +// Addr::unchecked("user7"), +// Addr::unchecked("user12"), +// ] +// ); +// assert_eq!( +// proposal_against_voters, +// vec![ +// Addr::unchecked("user8"), +// Addr::unchecked("user9"), +// Addr::unchecked("user10"), +// Addr::unchecked("user11") +// ] +// ); +// +// // Skip voting period +// app.update_block(|bi| { +// bi.height += PROPOSAL_VOTING_PERIOD + 1; +// bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); +// }); +// +// // Try to vote after voting period +// let err = cast_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// Addr::unchecked("user11"), +// ProposalVoteOption::Against, +// ) +// .unwrap_err(); +// +// assert_eq!(err.root_cause().to_string(), "Voting period ended!"); +// +// // Try to execute the proposal before end_proposal +// let err = app +// .execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!(err.root_cause().to_string(), "Proposal not passed!"); +// +// // Check the successful completion of the proposal +// check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); +// +// app.execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::EndProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap(); +// +// check_token_balance( +// &mut app, +// &xastro_addr, +// &Addr::unchecked("user0"), +// PROPOSAL_REQUIRED_DEPOSIT, +// ); +// +// let proposal: Proposal = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::Proposal { proposal_id: 1 }, +// ) +// .unwrap(); +// +// assert_eq!(proposal.status, ProposalStatus::Passed); +// +// // Try to end proposal again +// let err = app +// .execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::EndProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!(err.root_cause().to_string(), "Proposal not active!"); +// +// // Try to execute the proposal before the delay +// let err = app +// .execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!(err.root_cause().to_string(), "Proposal delay not ended!"); +// +// // Skip blocks +// app.update_block(|bi| { +// bi.height += PROPOSAL_EFFECTIVE_DELAY + 1; +// bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EFFECTIVE_DELAY + 1)); +// }); +// +// // Try to execute the proposal after the delay +// app.execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap(); +// +// let config: Config = app +// .wrap() +// .query_wasm_smart(assembly_addr.to_string(), &QueryMsg::Config {}) +// .unwrap(); +// +// let proposal: Proposal = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.to_string(), +// &QueryMsg::Proposal { proposal_id: 1 }, +// ) +// .unwrap(); +// +// // Check execution result +// assert_eq!(config.proposal_voting_period, PROPOSAL_VOTING_PERIOD + 1000); +// assert_eq!( +// config.whitelisted_links, +// vec![ +// "https://some1.link/".to_string(), +// "https://some2.link/".to_string(), +// ] +// ); +// assert_eq!(proposal.status, ProposalStatus::Executed); +// +// // Try to remove proposal before expiration period +// let err = app +// .execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!(err.root_cause().to_string(), "Proposal not completed!"); +// +// // Remove expired proposal +// app.update_block(|bi| { +// bi.height += PROPOSAL_EXPIRATION_PERIOD + 1; +// bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + 1)); +// }); +// +// app.execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap(); +// +// let res: ProposalListResponse = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.to_string(), +// &QueryMsg::Proposals { +// start: None, +// limit: None, +// }, +// ) +// .unwrap(); +// +// assert_eq!(res.proposal_list, vec![]); +// // proposal_count should not be changed after removing a proposal +// assert_eq!(res.proposal_count, Uint64::from(1u32)); +// } +// +// #[test] +// fn test_successful_emissions_proposal() { +// use cosmwasm_std::{coins, BankMsg}; +// +// let mut app = mock_app(); +// let owner = Addr::unchecked("generator_controller"); +// +// let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); +// +// // Provide some funds to the Assembly contract to use in the proposal messages +// app.init_modules(|router, _, storage| { +// router.bank.init_balance( +// storage, +// &Addr::unchecked(assembly_addr.clone()), +// coins(1000, "uluna"), +// ) // }) // .unwrap(); - +// +// let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { +// title: "Emissions Test title".to_string(), +// description: "Emissions Test description".to_string(), +// // Sample message to use as we don't have IBC or the Generator to set emissions on +// messages: vec![CosmosMsg::Bank(BankMsg::Send { +// to_address: "generator_controller".into(), +// amount: coins(1, "uluna"), +// })], +// ibc_channel: None, +// }; +// +// app.execute_contract( +// Addr::unchecked("generator_controller"), +// assembly_addr.clone(), +// &emissions_proposal_msg, +// &[], +// ) +// .unwrap(); +// +// let proposal: Proposal = app +// .wrap() +// .query_wasm_smart(assembly_addr, &QueryMsg::Proposal { proposal_id: 1 }) +// .unwrap(); +// +// assert_eq!(proposal.status, ProposalStatus::Executed); +// } +// +// #[test] +// fn test_no_generator_controller_emissions_proposal() { +// let mut app = mock_app(); +// let owner = Addr::unchecked("generator_controller"); +// +// let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); +// let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { +// title: "Emissions Test title!".to_string(), +// description: "Emissions Test description!".to_string(), +// messages: vec![], +// ibc_channel: None, +// }; +// +// let err = app +// .execute_contract( +// Addr::unchecked("generator_controller"), +// assembly_addr, +// &emissions_proposal_msg, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Sender is not the Generator controller installed in the assembly" +// ); +// } +// +// #[test] +// fn test_empty_messages_emissions_proposal() { +// let mut app = mock_app(); +// let owner = Addr::unchecked("generator_controller"); +// +// let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); +// let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { +// title: "Emissions Test title!".to_string(), +// description: "Emissions Test description!".to_string(), +// messages: vec![], +// ibc_channel: None, +// }; +// +// let err = app +// .execute_contract( +// Addr::unchecked("generator_controller"), +// assembly_addr, +// &emissions_proposal_msg, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "The proposal has no messages to execute" +// ); +// } +// +// #[test] +// fn test_unauthorised_emissions_proposal() { +// use cosmwasm_std::BankMsg; +// +// let mut app = mock_app(); +// let owner = Addr::unchecked("generator_controller"); +// +// let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); +// let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { +// title: "Emissions Test title!".to_string(), +// description: "Emissions Test description!".to_string(), +// // Sample message to use as we don't have IBC or the Generator to set emissions on +// messages: vec![CosmosMsg::Bank(BankMsg::Send { +// to_address: "generator_controller".into(), +// amount: coins(1, "uluna"), +// })], +// ibc_channel: None, +// }; +// +// let err = app +// .execute_contract( +// Addr::unchecked("not_generator_controller"), +// assembly_addr, +// &emissions_proposal_msg, +// &[], +// ) +// .unwrap_err(); +// +// assert_eq!(err.root_cause().to_string(), "Unauthorized"); +// } +// +// #[test] +// fn test_voting_power_changes() { +// let mut app = mock_app(); +// +// let owner = Addr::unchecked("owner"); +// +// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = +// instantiate_contracts(&mut app, owner, false, false); +// +// // Mint tokens for submitting proposal +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user0"), +// PROPOSAL_REQUIRED_DEPOSIT, +// ); +// +// // Mint tokens for casting votes at start block +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user1"), +// 40000_000000, +// ); +// +// app.update_block(|mut block| { +// block.time = block.time.plus_seconds(WEEK); +// block.height += WEEK / 5; +// }); +// +// // Create proposal +// create_proposal( +// &mut app, +// &xastro_addr, +// &assembly_addr, +// Addr::unchecked("user0"), +// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: assembly_addr.to_string(), +// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { +// xastro_token_addr: None, +// vxastro_token_addr: None, +// voting_escrow_delegator_addr: None, +// ibc_controller: None, +// generator_controller: None, +// hub: None, +// builder_unlock_addr: None, +// proposal_voting_period: Some(750), +// proposal_effective_delay: None, +// proposal_expiration_period: None, +// proposal_required_deposit: None, +// proposal_required_quorum: None, +// proposal_required_threshold: None, +// whitelist_add: None, +// whitelist_remove: None, +// guardian_addr: None, +// }))) +// .unwrap(), +// funds: vec![], +// })]), +// ); +// // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user2"), +// 5000_000000, +// ); +// +// app.update_block(next_block); +// +// // user1 can vote as he had voting power before the proposal submitting. +// cast_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// Addr::unchecked("user1"), +// ProposalVoteOption::For, +// ) +// .unwrap(); +// // Should panic, because user2 doesn't have any voting power. +// let err = cast_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// Addr::unchecked("user2"), +// ProposalVoteOption::Against, +// ) +// .unwrap_err(); +// +// // user2 doesn't have voting power and doesn't affect on total voting power(total supply at) +// // total supply = 5000 +// assert_eq!( +// err.root_cause().to_string(), +// "You don't have any voting power!" +// ); +// +// app.update_block(next_block); +// +// // Skip voting period and delay +// app.update_block(|bi| { +// bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; +// bi.time = bi +// .time +// .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); +// }); +// +// // End proposal +// app.execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::EndProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap(); +// +// let proposal: Proposal = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::Proposal { proposal_id: 1 }, +// ) +// .unwrap(); +// +// // Check proposal votes +// assert_eq!(proposal.for_power, Uint128::from(40000_000000u128)); +// assert_eq!(proposal.against_power, Uint128::zero()); +// // Should be passed, as total_voting_power=5000, for_votes=40000. +// // So user2 didn't affect the result. Because he had to have xASTRO before the vote was submitted. +// assert_eq!(proposal.status, ProposalStatus::Passed); +// } +// +// #[test] +// fn test_fail_outpost_vote_without_hub() { +// let mut app = mock_app(); +// +// let owner = Addr::unchecked("owner"); +// +// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = +// instantiate_contracts(&mut app, owner, false, false); +// +// // Mint tokens for submitting proposal +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user0"), +// PROPOSAL_REQUIRED_DEPOSIT, +// ); +// +// // Mint tokens for casting votes at start block +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user1"), +// 40000_000000, +// ); +// +// app.update_block(|mut block| { +// block.time = block.time.plus_seconds(WEEK); +// block.height += WEEK / 5; +// }); +// +// // Create proposal +// create_proposal( +// &mut app, +// &xastro_addr, +// &assembly_addr, +// Addr::unchecked("user0"), +// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: assembly_addr.to_string(), +// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { +// xastro_token_addr: None, +// vxastro_token_addr: None, +// voting_escrow_delegator_addr: None, +// ibc_controller: None, +// generator_controller: None, +// hub: None, +// builder_unlock_addr: None, +// proposal_voting_period: Some(750), +// proposal_effective_delay: None, +// proposal_expiration_period: None, +// proposal_required_deposit: None, +// proposal_required_quorum: None, +// proposal_required_threshold: None, +// whitelist_add: None, +// whitelist_remove: None, +// guardian_addr: None, +// }))) +// .unwrap(), +// funds: vec![], +// })]), +// ); +// // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user2"), +// 5000_000000, +// ); +// +// app.update_block(next_block); +// +// // user1 can not vote from an Outpost due to no Hub contract set +// let err = cast_outpost_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// Addr::unchecked("invalid_contract"), +// Addr::unchecked("user1"), +// ProposalVoteOption::For, +// Uint128::from(100u64), +// ) +// .unwrap_err(); +// +// assert_eq!( +// err.root_cause().to_string(), +// "Sender is not the Hub installed in the assembly" +// ); +// } +// +// #[test] +// fn test_outpost_vote() { +// let mut app = mock_app(); +// +// let owner = Addr::unchecked("owner"); +// +// let (astro_token, staking_instance, xastro_addr, _, _, assembly_addr, _, hub_addr) = +// instantiate_contracts(&mut app, owner.clone(), false, true); +// +// let user1_voting_power = 10_000_000_000; +// let user2_voting_power = 5_000_000_000; +// let remote_user1_voting_power = 80_000_000_000u128; +// // let remote_user2_voting_power = 3_000_000_000u128; +// +// let hub_addr = hub_addr.unwrap(); +// +// // Mint tokens for submitting proposal +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user0"), +// PROPOSAL_REQUIRED_DEPOSIT, +// ); +// +// // Mint tokens for casting votes at start block +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user1"), +// user1_voting_power, +// ); +// +// // Mint tokens for casting votes against vote at start block +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user2"), +// user2_voting_power, +// ); +// +// // Mint ASTRO to stake +// mint_tokens( +// &mut app, +// &owner, +// &astro_token, +// &Addr::unchecked("cw20ics20"), +// 1_000_000_000_000u128, +// ); +// +// app.update_block(|mut block| { +// block.time = block.time.plus_seconds(WEEK); +// block.height += WEEK / 5; +// }); +// +// // Create proposal +// create_proposal( +// &mut app, +// &xastro_addr, +// &assembly_addr, +// Addr::unchecked("user0"), +// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr: assembly_addr.to_string(), +// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { +// xastro_token_addr: None, +// vxastro_token_addr: None, +// voting_escrow_delegator_addr: None, +// ibc_controller: None, +// generator_controller: None, +// hub: None, +// builder_unlock_addr: None, +// proposal_voting_period: Some(750), +// proposal_effective_delay: None, +// proposal_expiration_period: None, +// proposal_required_deposit: None, +// proposal_required_quorum: None, +// proposal_required_threshold: None, +// whitelist_add: None, +// whitelist_remove: None, +// guardian_addr: None, +// }))) +// .unwrap(), +// funds: vec![], +// })]), +// ); +// +// app.update_block(next_block); +// +// // Outpost votes won't be accepted from other addresses +// let err = cast_outpost_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// Addr::unchecked("other_contract"), +// Addr::unchecked("remote1"), +// ProposalVoteOption::For, +// Uint128::from(remote_user1_voting_power), +// ) +// .unwrap_err(); +// assert_eq!(err.root_cause().to_string(), "Unauthorized"); +// +// // Attempts to vote with no xASTRO minted on Outposts +// let err = cast_outpost_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// hub_addr, +// Addr::unchecked("remote1"), +// ProposalVoteOption::For, +// Uint128::from(remote_user1_voting_power), +// ) +// .unwrap_err(); +// assert_eq!( +// err.root_cause().to_string(), +// "Voting power exceeds maximum Outpost power" +// ); +// +// // Note: Due to cw-multitest not supporting IBC messages we can no longer +// // test voting with Outpost voting power +// +// // app.execute_contract( +// // owner, +// // hub_addr.clone(), +// // &astroport_governance::hub::ExecuteMsg::AddOutpost { +// // outpost_addr: "outpost1".to_string(), +// // outpost_channel: "channel-3".to_string(), +// // cw20_ics20_channel: "channel-1".to_string(), +// // }, +// // &[], +// // ) +// // .unwrap_err(); +// +// // Stake some ASTRO from an Outpost +// // stake_remote_astro( +// // &mut app, +// // Addr::unchecked("cw20ics20".to_string()), +// // hub_addr.clone(), +// // astro_token, +// // Uint128::from(remote_user1_voting_power), +// // ) +// // .unwrap_err(); +// +// // Continue normally +// // cast_outpost_vote( +// // &mut app, +// // assembly_addr.clone(), +// // 1, +// // hub_addr.clone(), +// // Addr::unchecked("remote1"), +// // ProposalVoteOption::For, +// // Uint128::from(remote_user1_voting_power), +// // ) +// // .unwrap(); +// } +// +// #[test] +// fn test_block_height_selection() { +// // Block height is 12345 after app initialization +// let mut app = mock_app(); +// +// let owner = Addr::unchecked("owner"); +// let user1 = Addr::unchecked("user1"); +// let user2 = Addr::unchecked("user2"); +// let user3 = Addr::unchecked("user3"); +// +// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = +// instantiate_contracts(&mut app, owner, false, false); +// +// // Mint tokens for submitting proposal +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked("user0"), +// PROPOSAL_REQUIRED_DEPOSIT, +// ); +// +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &user1, +// 6000_000001, +// ); +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &user2, +// 4000_000000, +// ); +// +// // Skip to the next period +// app.update_block(|mut block| { +// block.time = block.time.plus_seconds(WEEK); +// block.height += WEEK / 5; +// }); +// +// // Create proposal +// create_proposal( +// &mut app, +// &xastro_addr, +// &assembly_addr, +// Addr::unchecked("user0"), +// None, +// ); +// +// cast_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// user1, +// ProposalVoteOption::For, +// ) +// .unwrap(); +// +// // Mint huge amount of xASTRO. These tokens cannot affect on total supply in proposal 1 because +// // they were minted after proposal.start_block - 1 +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &user3, +// 100000_000000, +// ); +// // Mint more xASTRO to user2, who will vote against the proposal, what is enough to make proposal unsuccessful. +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &user2, +// 3000_000000, +// ); +// // Total voting power should be 20k xASTRO (proposal minimum deposit 10k + 4k + 6k users VP) +// check_total_vp(&mut app, &assembly_addr, 1, 20000_000001); +// +// cast_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// user2, +// ProposalVoteOption::Against, +// ) +// .unwrap(); +// +// // Skip voting period +// app.update_block(|bi| { +// bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; +// bi.time = bi +// .time +// .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); +// }); +// +// // End proposal +// app.execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::EndProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap(); +// +// let proposal: Proposal = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::Proposal { proposal_id: 1 }, +// ) +// .unwrap(); +// +// assert_eq!(proposal.for_power, Uint128::new(6000_000001)); +// // Against power is 4000, as user2's balance was increased after proposal.start_block - 1 +// // at which everyone's voting power are considered. +// assert_eq!(proposal.against_power, Uint128::new(4000_000000)); +// // Proposal is passed, as the total supply was increased after proposal.start_block - 1. +// assert_eq!(proposal.status, ProposalStatus::Passed); +// } +// +// #[test] +// fn test_unsuccessful_proposal() { +// let mut app = mock_app(); +// +// let owner = Addr::unchecked("owner"); +// +// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = +// instantiate_contracts(&mut app, owner, false, false); +// +// // Init voting power for users +// let xastro_balances: Vec<(&str, u128)> = vec![ +// ("user0", PROPOSAL_REQUIRED_DEPOSIT), // proposal submitter +// ("user1", 100), +// ("user2", 200), +// ("user3", 400), +// ("user4", 250), +// ("user5", 90), +// ("user6", 300), +// ("user7", 30), +// ("user8", 180), +// ("user9", 50), +// ("user10", 90), +// ("user11", 500), +// ]; +// +// for (addr, xastro) in xastro_balances { +// mint_tokens( +// &mut app, +// &staking_instance, +// &xastro_addr, +// &Addr::unchecked(addr), +// xastro, +// ); +// } +// +// // Skip period +// app.update_block(|mut block| { +// block.time = block.time.plus_seconds(WEEK); +// block.height += WEEK / 5; +// }); +// +// // Create proposal +// create_proposal( +// &mut app, +// &xastro_addr, +// &assembly_addr, +// Addr::unchecked("user0"), +// None, +// ); +// +// let expected_voting_power: Vec<(&str, ProposalVoteOption)> = vec![ +// ("user1", ProposalVoteOption::For), +// ("user2", ProposalVoteOption::For), +// ("user3", ProposalVoteOption::For), +// ("user4", ProposalVoteOption::Against), +// ("user5", ProposalVoteOption::Against), +// ("user6", ProposalVoteOption::Against), +// ("user7", ProposalVoteOption::Against), +// ("user8", ProposalVoteOption::Against), +// ("user9", ProposalVoteOption::Against), +// ("user10", ProposalVoteOption::Against), +// ]; +// +// for (addr, option) in expected_voting_power { +// cast_vote( +// &mut app, +// assembly_addr.clone(), +// 1, +// Addr::unchecked(addr), +// option, +// ) +// .unwrap(); +// } +// +// // Skip voting period +// app.update_block(|bi| { +// bi.height += PROPOSAL_VOTING_PERIOD + 1; +// bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); +// }); +// +// // Check balance of submitter before and after proposal completion +// check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); +// +// app.execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::EndProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap(); +// +// check_token_balance( +// &mut app, +// &xastro_addr, +// &Addr::unchecked("user0"), +// 10000_000000, +// ); +// +// // Check proposal status +// let proposal: Proposal = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.clone(), +// &QueryMsg::Proposal { proposal_id: 1 }, +// ) +// .unwrap(); +// +// assert_eq!(proposal.status, ProposalStatus::Rejected); +// +// // Remove expired proposal +// app.update_block(|bi| { +// bi.height += PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; +// bi.time = bi +// .time +// .plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); +// }); +// +// app.execute_contract( +// Addr::unchecked("user0"), +// assembly_addr.clone(), +// &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, +// &[], +// ) +// .unwrap(); +// +// let res: ProposalListResponse = app +// .wrap() +// .query_wasm_smart( +// assembly_addr.to_string(), +// &QueryMsg::Proposals { +// start: None, +// limit: None, +// }, +// ) +// .unwrap(); +// +// assert_eq!(res.proposal_list, vec![]); +// // proposal_count should not be changed after removing +// assert_eq!(res.proposal_count, Uint64::from(1u32)); +// } +// +// #[test] +// fn test_check_messages() { +// let mut app = mock_app(); +// let owner = Addr::unchecked("owner"); +// let (_, _, _, vxastro_addr, _, assembly_addr, _, _) = +// instantiate_contracts(&mut app, owner, false, false); +// +// change_owner(&mut app, &vxastro_addr, &assembly_addr); +// let user = Addr::unchecked("user"); +// let into_check_msg = |msgs: Vec<(String, Binary)>| { +// let messages = msgs +// .into_iter() +// .map(|(contract_addr, msg)| { +// CosmosMsg::Wasm(WasmMsg::Execute { +// contract_addr, +// msg, +// funds: vec![], +// }) +// }) +// .collect(); +// ExecuteMsg::CheckMessages { messages } +// }; +// +// let config_before: astroport_governance::voting_escrow_lite::Config = app +// .wrap() +// .query_wasm_smart( +// &vxastro_addr, +// &astroport_governance::voting_escrow_lite::QueryMsg::Config {}, +// ) +// .unwrap(); +// +// let vxastro_blacklist_msg = vec![( +// vxastro_addr.to_string(), +// to_json_binary( +// &astroport_governance::voting_escrow_lite::ExecuteMsg::UpdateConfig { +// new_guardian: None, +// generator_controller: None, +// outpost: None, +// }, +// ) +// .unwrap(), +// )]; +// let err = app +// .execute_contract( +// user, +// assembly_addr.clone(), +// &into_check_msg(vxastro_blacklist_msg), +// &[], +// ) +// .unwrap_err(); +// assert_eq!( +// &err.root_cause().to_string(), +// "Messages check passed. Nothing was committed to the blockchain" +// ); +// +// let config_after: astroport_governance::voting_escrow_lite::Config = app +// .wrap() +// .query_wasm_smart( +// &vxastro_addr, +// &astroport_governance::voting_escrow_lite::QueryMsg::Config {}, +// ) +// .unwrap(); +// assert_eq!(config_before, config_after); +// } +// +// fn mock_app() -> App { +// let mut env = mock_env(); +// env.block.time = Timestamp::from_seconds(EPOCH_START); +// let api = MockApi::default(); +// let bank = BankKeeper::new(); +// let storage = MockStorage::new(); +// +// AppBuilder::new() +// .with_api(api) +// .with_block(env.block) +// .with_bank(bank) +// .with_storage(storage) +// .build(|_, _, _| {}) +// } +// +// fn instantiate_contracts( +// router: &mut App, +// owner: Addr, +// with_generator_controller: bool, +// with_hub: bool, +// ) -> ( +// Addr, +// Addr, +// Addr, +// Addr, +// Addr, +// Addr, +// Option, +// Option, +// ) { +// let token_addr = instantiate_astro_token(router, &owner); +// let (staking_addr, xastro_token_addr) = instantiate_xastro_token(router, &owner, &token_addr); +// let vxastro_token_addr = instantiate_vxastro_token(router, &owner, &xastro_token_addr); +// let builder_unlock_addr = instantiate_builder_unlock_contract(router, &owner, &token_addr); +// +// // If we want to test immediate proposals we need to set the address +// // for the generator controller. Deploying the generator controller in this +// // test would require deploying factory, tokens and pools. That test is +// // better suited in the generator controller itself. Thus, we use the owner +// // address as the generator controller address to test immediate proposals. +// let mut generator_controller_addr = None; +// +// if with_generator_controller { +// generator_controller_addr = Some(owner.to_string()); +// } +// +// let mut hub_addr = None; +// +// if with_hub { +// hub_addr = Some(instantiate_hub( +// router, +// &owner, +// &Addr::unchecked("contract6".to_string()), +// &staking_addr, +// )); +// } +// +// let assembly_addr = instantiate_assembly_contract( +// router, +// &owner, +// &xastro_token_addr, +// &vxastro_token_addr, +// &builder_unlock_addr, +// None, +// generator_controller_addr, +// hub_addr.clone(), +// ); +// +// ( +// token_addr, +// staking_addr, +// xastro_token_addr, +// vxastro_token_addr, +// builder_unlock_addr, +// assembly_addr, +// None, +// hub_addr, +// ) +// } +// +// fn instantiate_astro_token(router: &mut App, owner: &Addr) -> Addr { +// let astro_token_contract = Box::new(ContractWrapper::new_with_empty( +// astroport_token::contract::execute, +// astroport_token::contract::instantiate, +// astroport_token::contract::query, +// )); +// +// let astro_token_code_id = router.store_code(astro_token_contract); +// +// let msg = TokenInstantiateMsg { +// name: String::from("Astro token"), +// symbol: String::from("ASTRO"), +// decimals: 6, +// initial_balances: vec![], +// mint: Some(MinterResponse { +// minter: owner.to_string(), +// cap: None, +// }), +// marketing: None, +// }; +// +// router +// .instantiate_contract( +// astro_token_code_id, +// owner.clone(), +// &msg, +// &[], +// String::from("ASTRO"), +// None, +// ) +// .unwrap() +// } +// +// fn instantiate_xastro_token(router: &mut App, owner: &Addr, astro_token: &Addr) -> (Addr, Addr) { +// let xastro_contract = Box::new(ContractWrapper::new_with_empty( +// astroport_xastro_token::contract::execute, +// astroport_xastro_token::contract::instantiate, +// astroport_xastro_token::contract::query, +// )); +// +// let xastro_code_id = router.store_code(xastro_contract); +// +// let staking_contract = Box::new( +// ContractWrapper::new_with_empty( +// astroport_staking::contract::execute, +// astroport_staking::contract::instantiate, +// astroport_staking::contract::query, +// ) +// .with_reply_empty(astroport_staking::contract::reply), +// ); +// +// let staking_code_id = router.store_code(staking_contract); +// +// let msg = astroport::staking::InstantiateMsg { +// owner: owner.to_string(), +// token_code_id: xastro_code_id, +// deposit_token_addr: astro_token.to_string(), +// marketing: None, +// }; +// let staking_instance = router +// .instantiate_contract( +// staking_code_id, +// owner.clone(), +// &msg, +// &[], +// String::from("xASTRO"), +// None, +// ) +// .unwrap(); +// +// let res = router +// .wrap() +// .query::(&QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr: staking_instance.to_string(), +// msg: to_json_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), +// })) +// .unwrap(); +// +// (staking_instance, res.share_token_addr) +// } +// +// fn instantiate_vxastro_token(router: &mut App, owner: &Addr, xastro: &Addr) -> Addr { +// let vxastro_token_contract = Box::new(ContractWrapper::new_with_empty( +// voting_escrow_lite::execute::execute, +// voting_escrow_lite::contract::instantiate, +// voting_escrow_lite::query::query, +// )); +// +// let vxastro_token_code_id = router.store_code(vxastro_token_contract); +// +// let msg = VXAstroInstantiateMsg { +// owner: owner.to_string(), +// guardian_addr: Some(owner.to_string()), +// deposit_token_addr: xastro.to_string(), +// generator_controller_addr: None, +// outpost_addr: None, +// marketing: None, +// logo_urls_whitelist: vec![], +// }; +// +// router +// .instantiate_contract( +// vxastro_token_code_id, +// owner.clone(), +// &msg, +// &[], +// String::from("vxASTRO"), +// None, +// ) +// .unwrap() +// } +// +// fn instantiate_hub( +// router: &mut App, +// owner: &Addr, +// assembly_addr: &Addr, +// staking_addr: &Addr, +// ) -> Addr { +// let hub_contract = Box::new( +// ContractWrapper::new_with_empty( +// astroport_hub::execute::execute, +// astroport_hub::contract::instantiate, +// astroport_hub::query::query, +// ) +// .with_reply(astroport_hub::reply::reply), +// ); +// +// let hub_code_id = router.store_code(hub_contract); +// +// let msg = HubInstantiateMsg { +// owner: owner.to_string(), +// assembly_addr: assembly_addr.to_string(), +// cw20_ics20_addr: "cw20ics20".to_string(), +// generator_controller_addr: "unknown".to_string(), +// ibc_timeout_seconds: 60, +// staking_addr: staking_addr.to_string(), +// }; +// +// router +// .instantiate_contract( +// hub_code_id, +// owner.clone(), +// &msg, +// &[], +// String::from("Hub"), +// None, +// ) +// .unwrap() +// } +// +// fn instantiate_builder_unlock_contract(router: &mut App, owner: &Addr, astro_token: &Addr) -> Addr { +// let builder_unlock_contract = Box::new(ContractWrapper::new_with_empty( +// builder_unlock::contract::execute, +// builder_unlock::contract::instantiate, +// builder_unlock::contract::query, +// )); +// +// let builder_unlock_code_id = router.store_code(builder_unlock_contract); +// +// let msg = BuilderUnlockInstantiateMsg { +// owner: owner.to_string(), +// astro_token: astro_token.to_string(), +// max_allocations_amount: Uint128::new(300_000_000_000_000u128), +// }; +// +// router +// .instantiate_contract( +// builder_unlock_code_id, +// owner.clone(), +// &msg, +// &[], +// "Builder Unlock contract".to_string(), +// Some(owner.to_string()), +// ) +// .unwrap() +// } +// +// #[allow(clippy::too_many_arguments)] +// fn instantiate_assembly_contract( +// router: &mut App, +// owner: &Addr, +// xastro: &Addr, +// vxastro: &Addr, +// builder: &Addr, +// delegator: Option, +// generator_controller_addr: Option, +// hub_addr: Option, +// ) -> Addr { +// let assembly_contract = Box::new(ContractWrapper::new_with_empty( +// astro_assembly::contract::execute, +// astro_assembly::contract::instantiate, +// astro_assembly::contract::query, +// )); +// +// let assembly_code = router.store_code(assembly_contract); +// +// let hub: Option = hub_addr.as_ref().map(|s| s.to_string()); +// +// let msg = InstantiateMsg { +// xastro_token_addr: xastro.to_string(), +// vxastro_token_addr: Some(vxastro.to_string()), +// voting_escrow_delegator_addr: delegator, +// ibc_controller: None, +// generator_controller_addr, +// hub_addr: hub, +// builder_unlock_addr: builder.to_string(), +// proposal_voting_period: PROPOSAL_VOTING_PERIOD, +// proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, +// proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, +// proposal_required_deposit: Uint128::new(PROPOSAL_REQUIRED_DEPOSIT), +// proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), +// proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), +// whitelisted_links: vec!["https://some.link/".to_string()], +// }; +// +// router +// .instantiate_contract( +// assembly_code, +// owner.clone(), +// &msg, +// &[], +// "Assembly".to_string(), +// Some(owner.to_string()), +// ) +// .unwrap() +// } +// +// fn mint_tokens(app: &mut App, minter: &Addr, token: &Addr, recipient: &Addr, amount: u128) { +// let msg = Cw20ExecuteMsg::Mint { +// recipient: recipient.to_string(), +// amount: Uint128::from(amount), +// }; +// +// app.execute_contract(minter.clone(), token.to_owned(), &msg, &[]) +// .unwrap(); +// } +// +// fn mint_vxastro( +// app: &mut App, +// staking_instance: &Addr, +// xastro: Addr, +// vxastro: &Addr, +// recipient: Addr, +// amount: u128, +// ) { +// mint_tokens(app, staking_instance, &xastro, &recipient, amount); +// // let msg = Cw20ExecuteMsg::Send { -// contract: hub.to_string(), +// contract: vxastro.to_string(), +// amount: Uint128::from(amount), +// msg: to_json_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), +// }; +// +// app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); +// } +// +// fn create_allocations( +// app: &mut App, +// token: Addr, +// builder_unlock_contract_addr: Addr, +// allocations: Vec<(String, AllocationParams)>, +// ) { +// let amount = allocations +// .iter() +// .map(|params| params.1.amount.u128()) +// .sum(); +// +// mint_tokens( +// app, +// &Addr::unchecked("owner"), +// &token, +// &Addr::unchecked("owner"), // amount, -// msg: cw20_msg, +// ); +// +// app.execute_contract( +// Addr::unchecked("owner"), +// Addr::unchecked(token.to_string()), +// &Cw20ExecuteMsg::Send { +// contract: builder_unlock_contract_addr.to_string(), +// amount: Uint128::from(amount), +// msg: to_json_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }) +// .unwrap(), +// }, +// &[], +// ) +// .unwrap(); +// } +// +// fn create_proposal( +// app: &mut App, +// token: &Addr, +// assembly: &Addr, +// submitter: Addr, +// msgs: Option>, +// ) { +// let submit_proposal_msg = Cw20HookMsg::SubmitProposal { +// title: "Test title!".to_string(), +// description: "Test description!".to_string(), +// link: None, +// messages: msgs, +// ibc_channel: None, // }; - -// app.execute_contract(sender, astro_token, &msg, &[]) +// +// app.execute_contract( +// submitter, +// token.clone(), +// &Cw20ExecuteMsg::Send { +// contract: assembly.to_string(), +// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), +// msg: to_json_binary(&submit_proposal_msg).unwrap(), +// }, +// &[], +// ) +// .unwrap(); +// } +// +// fn check_token_balance(app: &mut App, token: &Addr, address: &Addr, expected: u128) { +// let msg = XAstroQueryMsg::Balance { +// address: address.to_string(), +// }; +// let res: StdResult = app.wrap().query_wasm_smart(token, &msg); +// assert_eq!(res.unwrap().balance, Uint128::from(expected)); +// } +// +// fn check_user_vp(app: &mut App, assembly: &Addr, address: &Addr, proposal_id: u64, expected: u128) { +// let res: Uint128 = app +// .wrap() +// .query_wasm_smart( +// assembly.to_string(), +// &QueryMsg::UserVotingPower { +// user: address.to_string(), +// proposal_id, +// }, +// ) +// .unwrap(); +// +// assert_eq!(res.u128(), expected); +// } +// +// fn check_total_vp(app: &mut App, assembly: &Addr, proposal_id: u64, expected: u128) { +// let res: Uint128 = app +// .wrap() +// .query_wasm_smart( +// assembly.to_string(), +// &QueryMsg::TotalVotingPower { proposal_id }, +// ) +// .unwrap(); +// +// assert_eq!(res.u128(), expected); +// } +// +// fn cast_vote( +// app: &mut App, +// assembly: Addr, +// proposal_id: u64, +// sender: Addr, +// option: ProposalVoteOption, +// ) -> anyhow::Result { +// app.execute_contract( +// sender, +// assembly, +// &ExecuteMsg::CastVote { +// proposal_id, +// vote: option, +// }, +// &[], +// ) +// } +// +// fn cast_outpost_vote( +// app: &mut App, +// assembly: Addr, +// proposal_id: u64, +// sender: Addr, +// voter: Addr, +// option: ProposalVoteOption, +// voting_power: Uint128, +// ) -> anyhow::Result { +// app.execute_contract( +// sender, +// assembly, +// &ExecuteMsg::CastOutpostVote { +// proposal_id, +// voter: voter.to_string(), +// vote: option, +// voting_power, +// }, +// &[], +// ) +// } +// +// // Add back once cw-multitest supports IBC +// // fn stake_remote_astro( +// // app: &mut App, +// // sender: Addr, +// // hub: Addr, +// // astro_token: Addr, +// // amount: Uint128, +// // ) -> anyhow::Result { +// // let cw20_msg = to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { +// // channel: "channel-1".to_string(), +// // sender: "remoteuser1".to_string(), +// // receiver: hub.to_string(), +// // memo: "{\"stake\":{}}".to_string(), +// // }) +// // .unwrap(); +// +// // let msg = Cw20ExecuteMsg::Send { +// // contract: hub.to_string(), +// // amount, +// // msg: cw20_msg, +// // }; +// +// // app.execute_contract(sender, astro_token, &msg, &[]) +// // } +// +// fn change_owner(app: &mut App, contract: &Addr, assembly: &Addr) { +// let msg = astroport_governance::voting_escrow_lite::ExecuteMsg::ProposeNewOwner { +// new_owner: assembly.to_string(), +// expires_in: 100, +// }; +// app.execute_contract(Addr::unchecked("owner"), contract.clone(), &msg, &[]) +// .unwrap(); +// +// app.execute_contract( +// assembly.clone(), +// contract.clone(), +// &astroport_governance::voting_escrow_lite::ExecuteMsg::ClaimOwnership {}, +// &[], +// ) +// .unwrap(); // } - -fn change_owner(app: &mut App, contract: &Addr, assembly: &Addr) { - let msg = astroport_governance::voting_escrow_lite::ExecuteMsg::ProposeNewOwner { - new_owner: assembly.to_string(), - expires_in: 100, - }; - app.execute_contract(Addr::unchecked("owner"), contract.clone(), &msg, &[]) - .unwrap(); - - app.execute_contract( - assembly.clone(), - contract.clone(), - &astroport_governance::voting_escrow_lite::ExecuteMsg::ClaimOwnership {}, - &[], - ) - .unwrap(); -} diff --git a/packages/astroport-governance/src/hub.rs b/packages/astroport-governance/src/hub.rs index 632e0c04..95fd59eb 100644 --- a/packages/astroport-governance/src/hub.rs +++ b/packages/astroport-governance/src/hub.rs @@ -96,7 +96,6 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, - // TODO: change to track by nano seconds /// Returns the current balance of xASTRO minted via a specific Outpost channel #[returns(HubBalance)] ChannelBalanceAt { channel: String, timestamp: Uint64 }, From a49b840b96b0a3e902afc6646ef47adc3d515ecb Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:20:39 +0400 Subject: [PATCH 14/47] feat(builder unlock): native ASTRO support --- Cargo.lock | 32 +- contracts/assembly/tests/common/helper.rs | 2 +- contracts/builder_unlock/Cargo.toml | 14 +- contracts/builder_unlock/src/contract.rs | 147 ++--- contracts/builder_unlock/src/lib.rs | 5 - contracts/builder_unlock/src/migration.rs | 5 - contracts/builder_unlock/src/state.rs | 2 +- ...ation.rs => builder_unlock_integration.rs} | 565 +++++------------- packages/astroport-governance/Cargo.toml | 1 + .../src/builder_unlock.rs | 26 +- 10 files changed, 259 insertions(+), 540 deletions(-) delete mode 100644 contracts/builder_unlock/src/migration.rs rename contracts/builder_unlock/tests/{integration.rs => builder_unlock_integration.rs} (78%) diff --git a/Cargo.lock b/Cargo.lock index 2fac102f..6b1adc40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,7 +33,7 @@ dependencies = [ "builder-unlock", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.20.0", + "cw-multi-test 0.20.0 (git+https://github.com/astroport-fi/cw-multi-test?branch=feat/bank_with_send_hooks)", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -479,18 +479,16 @@ checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" [[package]] name = "builder-unlock" -version = "2.0.0" +version = "3.0.0" dependencies = [ "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", - "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.15.1", + "cw-multi-test 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "thiserror", + "cw-utils 1.0.3", + "cw2 1.1.2", ] [[package]] @@ -705,6 +703,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-multi-test" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fff029689ae89127cf6d7655809a68d712f3edbdb9686c70b018ba438b26ca" +dependencies = [ + "anyhow", + "bech32", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "derivative", + "itertools 0.12.0", + "prost 0.12.3", + "schemars", + "serde", + "sha2 0.10.8", + "thiserror", +] + [[package]] name = "cw-multi-test" version = "0.20.0" diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index f726f28b..8778235e 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -125,7 +125,7 @@ impl Helper { let msg = astroport_governance::builder_unlock::msg::InstantiateMsg { owner: owner.to_string(), - astro_token: ASTRO_DENOM.to_string(), + astro_denom: ASTRO_DENOM.to_string(), max_allocations_amount: Uint128::new(300_000_000_000000), }; diff --git a/contracts/builder_unlock/Cargo.toml b/contracts/builder_unlock/Cargo.toml index 96337916..75917b7c 100644 --- a/contracts/builder_unlock/Cargo.toml +++ b/contracts/builder_unlock/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "builder-unlock" -version = "2.0.0" +version = "3.0.0" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" @@ -15,15 +15,13 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cw2 = "0.15" -cw20 = "0.15" -cosmwasm-std = "1.1" +cw2 = "1.1" +cw-utils = "1" +cosmwasm-std = "1.5" cw-storage-plus = "0.15" astroport-governance = { path = "../../packages/astroport-governance" } astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -thiserror = { version = "1.0" } -cosmwasm-schema = "1.1" +cosmwasm-schema = "1.5" [dev-dependencies] -cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +cw-multi-test = "0.20" \ No newline at end of file diff --git a/contracts/builder_unlock/src/contract.rs b/contracts/builder_unlock/src/contract.rs index 9d2a2f20..e5bb692a 100644 --- a/contracts/builder_unlock/src/contract.rs +++ b/contracts/builder_unlock/src/contract.rs @@ -1,27 +1,28 @@ +use astroport::asset::addr_opt_validate; +use astroport::asset::validate_native_denom; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, from_json, to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, - Response, StdError, StdResult, Uint128, WasmMsg, + attr, coins, ensure, to_json_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, + Order, Response, StdError, StdResult, Uint128, }; use cw2::set_contract_version; -use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; use cw_storage_plus::Bound; +use cw_utils::{may_pay, must_pay}; use astroport_governance::builder_unlock::msg::{ - AllocationResponse, ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg, SimulateWithdrawResponse, + AllocationResponse, ExecuteMsg, InstantiateMsg, QueryMsg, SimulateWithdrawResponse, StateResponse, }; use astroport_governance::builder_unlock::{AllocationParams, AllocationStatus, Config, Schedule}; use astroport_governance::{DEFAULT_LIMIT, MAX_LIMIT}; -use crate::astroport::asset::addr_opt_validate; -use crate::astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; use crate::contract::helpers::{compute_unlocked_amount, compute_withdraw_amount}; use crate::state::{CONFIG, OWNERSHIP_PROPOSAL, PARAMS, STATE, STATUS}; // Version and name used for contract migration. -const CONTRACT_NAME: &str = "builder-unlock"; +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Creates a new contract with the specified parameters in the `msg` variable. @@ -32,25 +33,28 @@ pub fn instantiate( _info: MessageInfo, msg: InstantiateMsg, ) -> StdResult { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - STATE.save(deps.storage, &Default::default())?; + validate_native_denom(&msg.astro_denom)?; CONFIG.save( deps.storage, &Config { owner: deps.api.addr_validate(&msg.owner)?, - astro_token: deps.api.addr_validate(&msg.astro_token)?, + astro_denom: msg.astro_denom, max_allocations_amount: msg.max_allocations_amount, }, )?; + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + STATE.save(deps.storage, &Default::default())?; + Ok(Response::default()) } /// Exposes all the execute functions available in the contract. /// /// ## Execute messages -/// * **ExecuteMsg::Receive(cw20_msg)** Parse incoming messages coming from the ASTRO token contract. +/// * **ExecuteMsg::CreateAllocations** Create allocations. /// /// * **ExecuteMsg::Withdraw** Withdraw unlocked ASTRO. /// @@ -78,7 +82,9 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { match msg { - ExecuteMsg::Receive(cw20_msg) => execute_receive_cw20(deps, info, cw20_msg), + ExecuteMsg::CreateAllocations { allocations } => { + execute_create_allocations(deps, info, allocations) + } ExecuteMsg::Withdraw {} => execute_withdraw(deps, env, info), ExecuteMsg::ProposeNewReceiver { new_receiver } => { execute_propose_new_receiver(deps, info, new_receiver) @@ -92,9 +98,13 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S if info.sender != config.owner { return Err(StdError::generic_err( "Only the contract owner can increase allocations", - )); + ) + .into()); } - execute_increase_allocation(deps, &config, receiver, amount, None) + let deposit_amount = may_pay(&info, &config.astro_denom) + .map_err(|err| StdError::generic_err(err.to_string()))?; + + execute_increase_allocation(deps, &config, receiver, amount, deposit_amount) } ExecuteMsg::DecreaseAllocation { receiver, amount } => { execute_decrease_allocation(deps, env, info, receiver, amount) @@ -142,39 +152,6 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S } } -/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. -/// -/// * **cw20_msg** CW20 message to process. -fn execute_receive_cw20( - deps: DepsMut, - info: MessageInfo, - cw20_msg: Cw20ReceiveMsg, -) -> StdResult { - match from_json(&cw20_msg.msg)? { - ReceiveMsg::CreateAllocations { allocations } => execute_create_allocations( - deps, - cw20_msg.sender, - info.sender, - cw20_msg.amount, - allocations, - ), - ReceiveMsg::IncreaseAllocation { user, amount } => { - let config = CONFIG.load(deps.storage)?; - - if config.astro_token != info.sender { - return Err(StdError::generic_err("Only ASTRO can be deposited")); - } - if deps.api.addr_validate(&cw20_msg.sender)? != config.owner { - return Err(StdError::generic_err( - "Only the contract owner can increase allocations", - )); - } - - execute_increase_allocation(deps, &config, user, amount, Some(cw20_msg.amount)) - } - } -} - /// Expose available contract queries. /// /// ## Queries @@ -216,23 +193,18 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { /// * **deposit_amount** new allocations being created. fn execute_create_allocations( deps: DepsMut, - creator: String, - deposit_token: Addr, - deposit_amount: Uint128, + info: MessageInfo, allocations: Vec<(String, AllocationParams)>, ) -> StdResult { let config = CONFIG.load(deps.storage)?; - let mut state = STATE.load(deps.storage)?; - if deps.api.addr_validate(&creator)? != config.owner { - return Err(StdError::generic_err( - "Only the contract owner can create allocations", - )); - } + ensure!( + info.sender == config.owner, + StdError::generic_err("Only the contract owner can create allocations",) + ); - if deposit_token != config.astro_token { - return Err(StdError::generic_err("Only ASTRO can be deposited")); - } + let deposit_amount = must_pay(&info, &config.astro_denom) + .map_err(|err| StdError::generic_err(err.to_string()))?; if deposit_amount != allocations @@ -243,6 +215,8 @@ fn execute_create_allocations( return Err(StdError::generic_err("ASTRO deposit amount mismatch")); } + let mut state = STATE.load(deps.storage)?; + state.total_astro_deposited += deposit_amount; state.remaining_astro_tokens += deposit_amount; @@ -272,9 +246,6 @@ fn execute_create_allocations( /// Allow allocation recipients to withdraw unlocked ASTRO. fn execute_withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult { - let config = CONFIG.load(deps.storage)?; - let mut state = STATE.load(deps.storage)?; - let params = PARAMS.load(deps.storage, &info.sender)?; if params.proposed_receiver.is_some() { @@ -292,6 +263,8 @@ fn execute_withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult StdResult, + deposit_amount: Uint128, ) -> StdResult { let receiver = deps.api.addr_validate(&receiver)?; @@ -439,17 +412,15 @@ fn execute_increase_allocation( Some(mut params) => { let mut state = STATE.load(deps.storage)?; - if let Some(deposit_amount) = deposit_amount { - state.total_astro_deposited = - state.total_astro_deposited.checked_add(deposit_amount)?; - state.unallocated_tokens = state.unallocated_tokens.checked_add(deposit_amount)?; + state.total_astro_deposited = + state.total_astro_deposited.checked_add(deposit_amount)?; + state.unallocated_tokens = state.unallocated_tokens.checked_add(deposit_amount)?; - if state.total_astro_deposited > config.max_allocations_amount { - return Err(StdError::generic_err(format!( - "The total allocation for all recipients cannot exceed total ASTRO amount allocated to unlock (currently {} ASTRO)", - config.max_allocations_amount, - ))); - } + if state.total_astro_deposited > config.max_allocations_amount { + return Err(StdError::generic_err(format!( + "The total allocation for all recipients cannot exceed total ASTRO amount allocated to unlock (currently {} ASTRO)", + config.max_allocations_amount, + ))); } if state.unallocated_tokens < amount { @@ -509,13 +480,9 @@ fn execute_transfer_unallocated( state.total_astro_deposited = state.total_astro_deposited.checked_sub(amount)?; let recipient = addr_opt_validate(deps.api, &recipient)?.unwrap_or_else(|| info.sender.clone()); - let msg = WasmMsg::Execute { - contract_addr: config.astro_token.to_string(), - msg: to_json_binary(&Cw20ExecuteMsg::Transfer { - recipient: recipient.to_string(), - amount, - })?, - funds: vec![], + let bank_msg = BankMsg::Send { + to_address: recipient.to_string(), + amount: coins(amount.u128(), config.astro_denom), }; STATE.save(deps.storage, &state)?; @@ -523,7 +490,7 @@ fn execute_transfer_unallocated( Ok(Response::new() .add_attribute("action", "execute_transfer_unallocated") .add_attribute("amount", amount) - .add_message(msg)) + .add_message(bank_msg)) } /// Allows a newly proposed allocation receiver to claim the ownership of that allocation. diff --git a/contracts/builder_unlock/src/lib.rs b/contracts/builder_unlock/src/lib.rs index f15a6c42..3407c199 100644 --- a/contracts/builder_unlock/src/lib.rs +++ b/contracts/builder_unlock/src/lib.rs @@ -1,7 +1,2 @@ pub mod contract; -mod migration; pub mod state; - -// During development this import could be replaced with another astroport version. -// However, in production, the astroport version should be the same for all contracts. -pub use astroport_governance::astroport; diff --git a/contracts/builder_unlock/src/migration.rs b/contracts/builder_unlock/src/migration.rs deleted file mode 100644 index c57a7f94..00000000 --- a/contracts/builder_unlock/src/migration.rs +++ /dev/null @@ -1,5 +0,0 @@ -use cosmwasm_schema::cw_serde; - -/// This structure describes a migration message. -#[cw_serde] -pub struct MigrateMsg {} diff --git a/contracts/builder_unlock/src/state.rs b/contracts/builder_unlock/src/state.rs index 5e85e603..14507d0d 100644 --- a/contracts/builder_unlock/src/state.rs +++ b/contracts/builder_unlock/src/state.rs @@ -1,4 +1,4 @@ -use crate::astroport::common::OwnershipProposal; +use astroport::common::OwnershipProposal; use cosmwasm_std::Addr; use cw_storage_plus::{Item, Map}; diff --git a/contracts/builder_unlock/tests/integration.rs b/contracts/builder_unlock/tests/builder_unlock_integration.rs similarity index 78% rename from contracts/builder_unlock/tests/integration.rs rename to contracts/builder_unlock/tests/builder_unlock_integration.rs index cdd0b9bc..017700b5 100644 --- a/contracts/builder_unlock/tests/integration.rs +++ b/contracts/builder_unlock/tests/builder_unlock_integration.rs @@ -1,54 +1,35 @@ -use astroport::token::InstantiateMsg as TokenInstantiateMsg; -use cosmwasm_std::{attr, to_json_binary, Addr, Decimal, StdResult, Timestamp, Uint128}; -use cw20::BalanceResponse; -use cw_multi_test::{App, BasicApp, ContractWrapper, Executor}; use std::time::SystemTime; +use cosmwasm_std::{coin, coins, Addr, Decimal, StdResult, Timestamp, Uint128}; +use cw_multi_test::{App, BasicApp, ContractWrapper, Executor}; + use astroport_governance::builder_unlock::msg::{ - AllocationResponse, ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg, + AllocationResponse, ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, SimulateWithdrawResponse, StateResponse, }; use astroport_governance::builder_unlock::{AllocationParams, Schedule}; +pub const ASTRO_DENOM: &str = "factory/assembly/ASTRO"; + const OWNER: &str = "owner"; fn mock_app() -> App { - BasicApp::default() -} - -fn init_contracts(app: &mut App) -> (Addr, Addr, InstantiateMsg) { - // Instantiate ASTRO token contract - let astro_token_contract = Box::new(ContractWrapper::new( - astroport_token::contract::execute, - astroport_token::contract::instantiate, - astroport_token::contract::query, - )); - - let astro_token_code_id = app.store_code(astro_token_contract); - - let msg = TokenInstantiateMsg { - name: String::from("Astro token"), - symbol: String::from("ASTRO"), - decimals: 6, - initial_balances: vec![], - mint: Some(cw20::MinterResponse { - minter: OWNER.clone().to_string(), - cap: None, - }), - marketing: None, - }; + let mut app = BasicApp::default(); + app.init_modules(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(OWNER), + vec![coin(u128::MAX, ASTRO_DENOM), coin(u128::MAX, "random")], + ) + .unwrap() + }); - let astro_token_instance = app - .instantiate_contract( - astro_token_code_id, - Addr::unchecked(OWNER.clone().to_string()), - &msg, - &[], - String::from("ASTRO"), - None, - ) - .unwrap(); + app +} +fn init_contracts(app: &mut App) -> (Addr, InstantiateMsg) { // Instantiate the contract let unlock_contract = Box::new(ContractWrapper::new( builder_unlock::contract::execute, @@ -59,8 +40,8 @@ fn init_contracts(app: &mut App) -> (Addr, Addr, InstantiateMsg) { let unlock_code_id = app.store_code(unlock_contract); let unlock_instantiate_msg = InstantiateMsg { - owner: OWNER.clone().to_string(), - astro_token: astro_token_instance.to_string(), + owner: OWNER.to_string(), + astro_denom: ASTRO_DENOM.to_string(), max_allocations_amount: Uint128::new(300_000_000_000_000u128), }; @@ -68,7 +49,7 @@ fn init_contracts(app: &mut App) -> (Addr, Addr, InstantiateMsg) { let unlock_instance = app .instantiate_contract( unlock_code_id, - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), &unlock_instantiate_msg, &[], "unlock", @@ -76,30 +57,16 @@ fn init_contracts(app: &mut App) -> (Addr, Addr, InstantiateMsg) { ) .unwrap(); - ( - unlock_instance, - astro_token_instance, - unlock_instantiate_msg, - ) + (unlock_instance, unlock_instantiate_msg) } -fn mint_some_astro( - app: &mut App, - owner: Addr, - astro_token_instance: Addr, - amount: Uint128, - to: String, -) { - let msg = cw20::Cw20ExecuteMsg::Mint { - recipient: to.clone(), - amount: amount, - }; - let res = app - .execute_contract(owner.clone(), astro_token_instance.clone(), &msg, &[]) - .unwrap(); - assert_eq!(res.events[1].attributes[1], attr("action", "mint")); - assert_eq!(res.events[1].attributes[2], attr("to", to)); - assert_eq!(res.events[1].attributes[3], attr("amount", amount)); +fn mint_some_astro(app: &mut App, amount: Uint128, to: String) { + app.send_tokens( + Addr::unchecked(OWNER), + Addr::unchecked(to), + &coins(amount.u128(), ASTRO_DENOM), + ) + .unwrap(); } fn check_alloc_amount(app: &mut App, contract_addr: &Addr, account: &Addr, amount: Uint128) { @@ -131,7 +98,7 @@ fn check_unlock_amount(app: &mut App, contract_addr: &Addr, account: &Addr, amou #[test] fn proper_initialization() { let mut app = mock_app(); - let (unlock_instance, _astro_instance, init_msg) = init_contracts(&mut app); + let (unlock_instance, init_msg) = init_contracts(&mut app); let resp: ConfigResponse = app .wrap() @@ -140,7 +107,7 @@ fn proper_initialization() { // Check config assert_eq!(init_msg.owner, resp.owner); - assert_eq!(init_msg.astro_token, resp.astro_token); + assert_eq!(init_msg.astro_denom, resp.astro_denom); // Check state let resp: StateResponse = app @@ -155,7 +122,7 @@ fn proper_initialization() { #[test] fn test_transfer_ownership() { let mut app = mock_app(); - let (unlock_instance, _, init_msg) = init_contracts(&mut app); + let (unlock_instance, init_msg) = init_contracts(&mut app); // ###### ERROR :: Unauthorized ###### let err = app @@ -197,21 +164,13 @@ fn test_transfer_ownership() { // Check config assert_eq!("new_owner".to_string(), resp.owner); - assert_eq!(init_msg.astro_token, resp.astro_token); + assert_eq!(init_msg.astro_denom, resp.astro_denom); } #[test] fn test_create_allocations() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); - - mint_some_astro( - &mut app, - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - Uint128::new(1_000_000_000_000000), - OWNER.to_string(), - ); + let (unlock_instance, _) = init_contracts(&mut app); let mut allocations: Vec<(String, AllocationParams)> = vec![]; allocations.push(( @@ -255,27 +214,16 @@ fn test_create_allocations() { )); // ###### ERROR :: Only owner can create allocations ###### - mint_some_astro( - &mut app, - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - Uint128::new(1_000), - "not_owner".to_string(), - ); + mint_some_astro(&mut app, Uint128::new(1_000), "not_owner".to_string()); - let mut err = app + let err = app .execute_contract( Addr::unchecked("not_owner".to_string()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(1_000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(1_000, ASTRO_DENOM), ) .unwrap_err(); assert_eq!( @@ -284,83 +232,32 @@ fn test_create_allocations() { ); // ###### ERROR :: Only ASTRO can be can be deposited ###### - // Instantiate the ASTRO token contract - let not_astro_token_contract = Box::new(ContractWrapper::new( - astroport_token::contract::execute, - astroport_token::contract::instantiate, - astroport_token::contract::query, - )); - - let not_astro_token_code_id = app.store_code(not_astro_token_contract); - - let msg = TokenInstantiateMsg { - name: String::from("Astro Token"), - symbol: String::from("ASTRO"), - decimals: 6, - initial_balances: vec![], - mint: Some(cw20::MinterResponse { - minter: OWNER.clone().to_string(), - cap: None, - }), - marketing: None, - }; - - let not_astro_token_instance = app - .instantiate_contract( - not_astro_token_code_id, - Addr::unchecked(OWNER.clone().to_string()), - &msg, - &[], - String::from("FAKE_ASTRO"), - None, - ) - .unwrap(); - app.execute_contract( - Addr::unchecked(OWNER.clone()), - not_astro_token_instance.clone(), - &cw20::Cw20ExecuteMsg::Mint { - recipient: OWNER.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - }, - &[], - ) - .unwrap(); - - err = app + let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), - not_astro_token_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(15_000_000_000000, "random"), ) .unwrap_err(); + assert_eq!( err.root_cause().to_string(), - "Generic error: Only ASTRO can be deposited" + format!("Generic error: Must send reserve token '{ASTRO_DENOM}'") ); // ###### ERROR :: ASTRO deposit amount mismatch ###### - err = app + let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000001u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(15_000_000_000001, ASTRO_DENOM), ) .unwrap_err(); assert_eq!( @@ -370,17 +267,12 @@ fn test_create_allocations() { // ###### SUCCESSFULLY CREATES ALLOCATIONS ###### app.execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(15_000_000_000000, ASTRO_DENOM), ) .unwrap(); @@ -465,19 +357,14 @@ fn test_create_allocations() { ); // ###### ERROR :: Allocation already exists for user {} ###### - err = app + let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(5_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: vec![allocations[0].clone()], - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: vec![allocations[0].clone()], }, - &[], + &coins(5_000_000_000000, ASTRO_DENOM), ) .unwrap_err(); assert_eq!( @@ -489,15 +376,7 @@ fn test_create_allocations() { #[test] fn test_withdraw() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); - - mint_some_astro( - &mut app, - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - Uint128::new(1_000_000_000_000000), - OWNER.to_string(), - ); + let (unlock_instance, _) = init_contracts(&mut app); let mut allocations: Vec<(String, AllocationParams)> = vec![]; allocations.push(( @@ -542,24 +421,19 @@ fn test_withdraw() { // SUCCESSFULLY CREATES ALLOCATIONS app.execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(15_000_000_000000, ASTRO_DENOM), ) .unwrap(); // ###### ERROR :: Allocation doesn't exist ###### let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::Withdraw {}, &[], @@ -576,18 +450,10 @@ fn test_withdraw() { b.time = Timestamp::from_seconds(1642402275) }); - let astro_bal_before: BalanceResponse = app - .wrap() - .query_wasm_smart( - &astro_instance, - &cw20::Cw20QueryMsg::Balance { - address: "investor_1".to_string(), - }, - ) - .unwrap(); + let astro_bal_before = app.wrap().query_balance("investor_1", ASTRO_DENOM).unwrap(); app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::Withdraw {}, &[], @@ -621,18 +487,10 @@ fn test_withdraw() { assert_eq!(alloc_resp.params.amount, Uint128::from(5_000_000_000000u64)); assert_eq!(alloc_resp.status.astro_withdrawn, Uint128::from(158548u64)); - let astro_bal_after: BalanceResponse = app - .wrap() - .query_wasm_smart( - &astro_instance, - &cw20::Cw20QueryMsg::Balance { - address: "investor_1".to_string(), - }, - ) - .unwrap(); + let astro_bal_after = app.wrap().query_balance("investor_1", ASTRO_DENOM).unwrap(); assert_eq!( - astro_bal_after.balance - astro_bal_before.balance, + astro_bal_after.amount - astro_bal_before.amount, alloc_resp.status.astro_withdrawn ); @@ -651,7 +509,7 @@ fn test_withdraw() { // ###### ERROR :: No unlocked ASTRO to be withdrawn ###### let err = app .execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::Withdraw {}, &[], @@ -698,7 +556,7 @@ fn test_withdraw() { ); app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::Withdraw {}, &[], @@ -720,7 +578,7 @@ fn test_withdraw() { // ###### ERROR :: No unlocked ASTRO to be withdrawn ###### let err = app .execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::Withdraw {}, &[], @@ -802,7 +660,7 @@ fn test_withdraw() { ); app.execute_contract( - Addr::unchecked("team_1".clone()), + Addr::unchecked("team_1"), unlock_instance.clone(), &ExecuteMsg::Withdraw {}, &[], @@ -841,15 +699,7 @@ fn test_withdraw() { #[test] fn test_propose_new_receiver() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); - - mint_some_astro( - &mut app, - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - Uint128::new(1_000_000_000_000000), - OWNER.to_string(), - ); + let (unlock_instance, _) = init_contracts(&mut app); let mut allocations: Vec<(String, AllocationParams)> = vec![]; allocations.push(( @@ -894,24 +744,19 @@ fn test_propose_new_receiver() { // SUCCESSFULLY CREATES ALLOCATIONS app.execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(15_000_000_000000, ASTRO_DENOM), ) .unwrap(); // ###### ERROR :: Allocation doesn't exist ###### let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::ProposeNewReceiver { new_receiver: "investor_1_new".to_string(), @@ -927,7 +772,7 @@ fn test_propose_new_receiver() { // ###### ERROR :: Invalid new_receiver ###### let err = app .execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::ProposeNewReceiver { new_receiver: "team_1".to_string(), @@ -942,7 +787,7 @@ fn test_propose_new_receiver() { // ###### SUCCESSFULLY PROPOSES NEW RECEIVER ###### app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::ProposeNewReceiver { new_receiver: "investor_1_new".to_string(), @@ -968,7 +813,7 @@ fn test_propose_new_receiver() { // ###### ERROR ::"Proposed receiver already set" ###### let err = app .execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::ProposeNewReceiver { new_receiver: "investor_1_new_".to_string(), @@ -985,15 +830,7 @@ fn test_propose_new_receiver() { #[test] fn test_drop_new_receiver() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); - - mint_some_astro( - &mut app, - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - Uint128::new(1_000_000_000_000000), - OWNER.to_string(), - ); + let (unlock_instance, _) = init_contracts(&mut app); let mut allocations: Vec<(String, AllocationParams)> = vec![]; allocations.push(( @@ -1038,24 +875,19 @@ fn test_drop_new_receiver() { // SUCCESSFULLY CREATES ALLOCATIONS app.execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(15_000_000_000000, ASTRO_DENOM), ) .unwrap(); // ###### ERROR :: Allocation doesn't exist ###### let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::DropNewReceiver {}, &[], @@ -1069,7 +901,7 @@ fn test_drop_new_receiver() { // ###### ERROR ::"Proposed receiver not set" ###### let err = app .execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::DropNewReceiver {}, &[], @@ -1083,7 +915,7 @@ fn test_drop_new_receiver() { // ###### SUCCESSFULLY DROP NEW RECEIVER ###### // SUCCESSFULLY PROPOSES NEW RECEIVER app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::ProposeNewReceiver { new_receiver: "investor_1_new".to_string(), @@ -1107,7 +939,7 @@ fn test_drop_new_receiver() { ); app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::DropNewReceiver {}, &[], @@ -1129,15 +961,7 @@ fn test_drop_new_receiver() { #[test] fn test_claim_receiver() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); - - mint_some_astro( - &mut app, - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - Uint128::new(1_000_000_000_000000), - OWNER.to_string(), - ); + let (unlock_instance, _) = init_contracts(&mut app); let mut allocations: Vec<(String, AllocationParams)> = vec![]; allocations.push(( @@ -1182,24 +1006,19 @@ fn test_claim_receiver() { // SUCCESSFULLY CREATES ALLOCATIONS app.execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(15_000_000_000000, ASTRO_DENOM), ) .unwrap(); // ###### ERROR :: Allocation doesn't exist ###### let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::Withdraw {}, &[], @@ -1213,7 +1032,7 @@ fn test_claim_receiver() { // ###### ERROR ::"Proposed receiver not set" ###### let err = app .execute_contract( - Addr::unchecked("investor_1_new".clone()), + Addr::unchecked("investor_1_new"), unlock_instance.clone(), &ExecuteMsg::ClaimReceiver { prev_receiver: "investor_1".to_string(), @@ -1229,7 +1048,7 @@ fn test_claim_receiver() { // ###### SUCCESSFULLY CLAIMED BY NEW RECEIVER ###### // SUCCESSFULLY PROPOSES NEW RECEIVER app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::ProposeNewReceiver { new_receiver: "investor_1_new".to_string(), @@ -1262,7 +1081,7 @@ fn test_claim_receiver() { // Claimed by new receiver app.execute_contract( - Addr::unchecked("investor_1_new".clone()), + Addr::unchecked("investor_1_new"), unlock_instance.clone(), &ExecuteMsg::ClaimReceiver { prev_receiver: "investor_1".to_string(), @@ -1357,15 +1176,7 @@ fn test_claim_receiver() { #[test] fn test_increase_and_decrease_allocation() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); - - mint_some_astro( - &mut app, - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - Uint128::new(1_000_000_000_000_000), - OWNER.to_string(), - ); + let (unlock_instance, _) = init_contracts(&mut app); // Create allocations let allocations: Vec<(String, AllocationParams)> = vec![( @@ -1383,17 +1194,12 @@ fn test_increase_and_decrease_allocation() { )]; app.execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(5_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(5_000_000_000000, ASTRO_DENOM), ) .unwrap(); @@ -1436,7 +1242,7 @@ fn test_increase_and_decrease_allocation() { // Try to decrease 4918550856846 ASTRO let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::DecreaseAllocation { receiver: "investor".to_string(), @@ -1451,7 +1257,7 @@ fn test_increase_and_decrease_allocation() { ); app.execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::DecreaseAllocation { receiver: "investor".to_string(), @@ -1485,7 +1291,7 @@ fn test_increase_and_decrease_allocation() { // Try to increase let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::IncreaseAllocation { receiver: "investor".to_string(), @@ -1499,6 +1305,8 @@ fn test_increase_and_decrease_allocation() { "Generic error: Insufficient unallocated ASTRO to increase allocation. Contract has: 1000000000000 unallocated ASTRO." ); + let balance_before = app.wrap().query_balance(OWNER, ASTRO_DENOM).unwrap().amount; + // Transfer unallocated tokens to owner app.execute_contract( Addr::unchecked("owner".to_string()), @@ -1511,31 +1319,18 @@ fn test_increase_and_decrease_allocation() { ) .unwrap(); - let res: BalanceResponse = app - .wrap() - .query_wasm_smart( - &astro_instance, - &cw20::Cw20QueryMsg::Balance { - address: OWNER.to_string(), - }, - ) - .unwrap(); - assert_eq!(res.balance, Uint128::from(995_500_000_000_000u128)); + let balance_after = app.wrap().query_balance(OWNER, ASTRO_DENOM).unwrap().amount; + assert_eq!((balance_after - balance_before).u128(), 500_000_000_000u128); - // Increase allocations with sending cw20 + // Increase allocations app.execute_contract( Addr::unchecked(OWNER), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(1_000u64), - msg: to_json_binary(&ReceiveMsg::IncreaseAllocation { - amount: Uint128::from(500_000_001_000u128), - user: "investor".to_string(), - }) - .unwrap(), + unlock_instance.clone(), + &ExecuteMsg::IncreaseAllocation { + amount: Uint128::from(500_000_001_000u128), + receiver: "investor".to_string(), }, - &[], + &coins(1_000, ASTRO_DENOM), ) .unwrap(); @@ -1548,16 +1343,8 @@ fn test_increase_and_decrease_allocation() { ) .unwrap(); - let res: BalanceResponse = app - .wrap() - .query_wasm_smart( - &astro_instance, - &cw20::Cw20QueryMsg::Balance { - address: "investor".to_string(), - }, - ) - .unwrap(); - assert_eq!(res.balance, Uint128::from(81_449_143_155u128)); + let balance = app.wrap().query_balance("investor", ASTRO_DENOM).unwrap(); + assert_eq!(balance.amount, Uint128::from(81_449_143_155u128)); // Check allocation amount after decreasing and increasing check_alloc_amount( @@ -1596,15 +1383,7 @@ fn test_increase_and_decrease_allocation() { #[test] fn test_updates_schedules() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); - - mint_some_astro( - &mut app, - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - Uint128::new(1_000_000_000_000000), - OWNER.to_string(), - ); + let (unlock_instance, _) = init_contracts(&mut app); let mut allocations: Vec<(String, AllocationParams)> = vec![]; allocations.push(( @@ -1649,17 +1428,12 @@ fn test_updates_schedules() { // ###### SUCCESSFULLY CREATES ALLOCATIONS ###### app.execute_contract( - Addr::unchecked(OWNER.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(15_000_000_000000, ASTRO_DENOM), ) .unwrap(); @@ -1728,7 +1502,7 @@ fn test_updates_schedules() { // not owner try to update configs let err = app .execute_contract( - Addr::unchecked("not_owner".clone()), + Addr::unchecked("not_owner"), unlock_instance.clone(), &ExecuteMsg::UpdateUnlockSchedules { new_unlock_schedules: vec![( @@ -1751,7 +1525,7 @@ fn test_updates_schedules() { let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::UpdateUnlockSchedules { new_unlock_schedules: vec![ @@ -1784,7 +1558,7 @@ fn test_updates_schedules() { ); app.execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::UpdateUnlockSchedules { new_unlock_schedules: vec![ @@ -1945,33 +1719,19 @@ fn check_allocation( Ok(()) } -fn query_cw20_bal(app: &mut App, cw20_addr: &Addr, address: &Addr) -> u128 { +fn query_bal(app: &mut App, address: &Addr) -> u128 { app.wrap() - .query_wasm_smart::( - cw20_addr, - &cw20::Cw20QueryMsg::Balance { - address: address.to_string(), - }, - ) + .query_balance(address, ASTRO_DENOM) .unwrap() - .balance + .amount .u128() } #[test] fn test_create_allocations_with_custom_cliff() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); + let (unlock_instance, _) = init_contracts(&mut app); let total_astro = Uint128::new(1_000_000_000000); - let owner = Addr::unchecked(OWNER); - - mint_some_astro( - &mut app, - owner.clone(), - astro_instance.clone(), - total_astro, - owner.to_string(), - ); let now_ts = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -2026,17 +1786,12 @@ fn test_create_allocations_with_custom_cliff() { // Create allocations app.execute_contract( - owner.clone(), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.to_string(), - amount: total_astro, - msg: to_json_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(total_astro.u128(), ASTRO_DENOM), ) .unwrap(); @@ -2076,7 +1831,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor3); + let balance = query_bal(&mut app, &investor3); let amount_at_cliff = allocations[2].1.amount.u128() / 5; let amount_linearly_vested = 64699_453551; assert_eq!(balance, amount_at_cliff + amount_linearly_vested); @@ -2106,7 +1861,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor2); + let balance = query_bal(&mut app, &investor2); assert_eq!(balance, 16666_666666); // Investor3 continues to receive linearly unlocked astro @@ -2117,7 +1872,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor3); + let balance = query_bal(&mut app, &investor3); assert_eq!(balance, 197158_469945); // shift by 7 months @@ -2131,7 +1886,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor1); + let balance = query_bal(&mut app, &investor1); assert_eq!(balance, 166666_666666); // Investor2 continues to receive linearly unlocked astro @@ -2142,7 +1897,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor2); + let balance = query_bal(&mut app, &investor2); assert_eq!(balance, 36247_723132); // Investor3 continues to receive linearly unlocked astro @@ -2153,7 +1908,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor3); + let balance = query_bal(&mut app, &investor3); assert_eq!(balance, 272349_726775); // shift by 2 years @@ -2167,7 +1922,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor1); + let balance = query_bal(&mut app, &investor1); assert_eq!(balance, 500000_000000); // Investor2 receives whole allocation @@ -2178,7 +1933,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor2); + let balance = query_bal(&mut app, &investor2); assert_eq!(balance, 100000_000000); // Investor3 receives whole allocation @@ -2189,7 +1944,7 @@ fn test_create_allocations_with_custom_cliff() { &[], ) .unwrap(); - let balance = query_cw20_bal(&mut app, &astro_instance, &investor3); + let balance = query_bal(&mut app, &investor3); assert_eq!(balance, 400000_000000); app.update_block(|block| block.time = block.time.plus_seconds(day)); diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index f3282330..7d74bd46 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "astroport-governance" +# TODO: bump version and set version in all dependent contracts version = "1.4.0" authors = ["Astroport"] edition = "2021" diff --git a/packages/astroport-governance/src/builder_unlock.rs b/packages/astroport-governance/src/builder_unlock.rs index 29d42d9f..871dede4 100644 --- a/packages/astroport-governance/src/builder_unlock.rs +++ b/packages/astroport-governance/src/builder_unlock.rs @@ -6,8 +6,8 @@ use cosmwasm_std::{Addr, Decimal, StdError, Uint128}; pub struct Config { /// Account that can create new unlock schedules pub owner: Addr, - /// Address of ASTRO token - pub astro_token: Addr, + /// ASTRO token denom + pub astro_denom: String, /// Max ASTRO tokens to allocate pub max_allocations_amount: Uint128, } @@ -127,7 +127,6 @@ impl AllocationStatus { pub mod msg { use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint128; - use cw20::Cw20ReceiveMsg; use crate::builder_unlock::Schedule; @@ -138,8 +137,8 @@ pub mod msg { pub struct InstantiateMsg { /// Account that can create new allocations pub owner: String, - /// ASTRO token address - pub astro_token: String, + /// ASTRO token denom + pub astro_denom: String, /// Max ASTRO tokens to allocate pub max_allocations_amount: Uint128, } @@ -147,8 +146,10 @@ pub mod msg { /// This enum describes all the execute functions available in the contract. #[cw_serde] pub enum ExecuteMsg { - /// Receive is an implementation for the CW20 receive msg - Receive(Cw20ReceiveMsg), + /// CreateAllocations creates new ASTRO allocations + CreateAllocations { + allocations: Vec<(String, AllocationParams)>, + }, /// Withdraw claims withdrawable ASTRO Withdraw {}, /// ProposeNewReceiver allows a user to change the receiver address for their ASTRO allocation @@ -180,17 +181,6 @@ pub mod msg { }, } - /// This enum describes receive msg templates. - #[cw_serde] - pub enum ReceiveMsg { - /// CreateAllocations creates new ASTRO allocations - CreateAllocations { - allocations: Vec<(String, AllocationParams)>, - }, - /// Increase the ASTRO allocation for a receiver - IncreaseAllocation { user: String, amount: Uint128 }, - } - /// Thie enum describes all the queries available in the contract. #[cw_serde] #[derive(QueryResponses)] From d67d70162a10b9324de9b3ad5b987787787ab0b5 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 25 Jan 2024 18:58:01 +0400 Subject: [PATCH 15/47] major test refactor --- Cargo.lock | 34 ++ contracts/assembly/Cargo.toml | 1 + contracts/assembly/src/error.rs | 2 +- contracts/assembly/src/lib.rs | 3 + contracts/assembly/src/unit_tests.rs | 129 ++++ contracts/assembly/tests/common/helper.rs | 98 ++- contracts/assembly/tests/integration.rs | 701 +++++----------------- 7 files changed, 386 insertions(+), 582 deletions(-) create mode 100644 contracts/assembly/src/unit_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 6b1adc40..21228f6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,7 @@ dependencies = [ "cw2 1.1.2", "ibc-controller-package", "osmosis-std", + "test-case", "thiserror", "voting-escrow-delegation", "voting-escrow-lite", @@ -1939,6 +1940,39 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "test-case-core", +] + [[package]] name = "thiserror" version = "1.0.56" diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 599f1a7c..cd905daa 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -36,3 +36,4 @@ astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_co astroport-tokenfactory-tracker = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } builder-unlock = { path = "../builder_unlock" } anyhow = "1" +test-case = "3.3.1" \ No newline at end of file diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index 7e7f2069..15751ec5 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -4,7 +4,7 @@ use cw_utils::PaymentError; use thiserror::Error; /// This enum describes Assembly contract errors -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), diff --git a/contracts/assembly/src/lib.rs b/contracts/assembly/src/lib.rs index 4974f7a6..0f2efaa2 100644 --- a/contracts/assembly/src/lib.rs +++ b/contracts/assembly/src/lib.rs @@ -4,3 +4,6 @@ pub mod state; pub mod queries; pub mod utils; + +#[cfg(test)] +mod unit_tests; diff --git a/contracts/assembly/src/unit_tests.rs b/contracts/assembly/src/unit_tests.rs new file mode 100644 index 00000000..d1addbeb --- /dev/null +++ b/contracts/assembly/src/unit_tests.rs @@ -0,0 +1,129 @@ +use std::str::FromStr; + +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{from_json, Addr, Coin, Decimal, Uint64}; +use test_case::test_case; + +use astroport_governance::assembly::{ + Config, Proposal, ProposalStatus, QueryMsg, DELAY_INTERVAL, DEPOSIT_INTERVAL, + EXPIRATION_PERIOD_INTERVAL, MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE, + MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, VOTING_PERIOD_INTERVAL, +}; + +use crate::contract::submit_proposal; +use crate::queries::query; +use crate::state::{CONFIG, PROPOSAL_COUNT}; +use cosmwasm_std::{coin, coins}; + +const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); +const XASTRO_DENOM: &str = "xastro"; + +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", "description", None, None ; "valid proposal")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "X", "description", None, Some("Generic error: Title too short!") ; "short title")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", "description", Some("X"), Some("Generic error: Link too short!") ; "short link")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", "description", Some("https://some1.link"), Some("Generic error: Link is not whitelisted!") ; "link is not whitelisted")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", "description", Some("https://some.link/"), Some("Generic error: Link is not properly formatted or contains unsafe characters!") ; "malicious link")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", "description", Some(&String::from_utf8(vec![b'X'; 129]).unwrap()), Some("Generic error: Link too long!") ; "long link")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", "X", None, Some("Generic error: Description too short!") ; "short description")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), &String::from_utf8(vec![b'X'; 65]).unwrap(), "description", None, Some("Generic error: Title too long!") ; "long title")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", &String::from_utf8(vec![b'X'; 1025]).unwrap(), None, Some("Generic error: Description too long!") ; "long description")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT - 1, XASTRO_DENOM), "title", "description", None, Some("Insufficient token deposit!") ; "invalid deposit")] +#[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, "random"), "title", "description", None, Some("Must send reserve token 'xastro'") ; "invalid coin deposit")] +#[test_case(vec![coin(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), coin(PROPOSAL_REQUIRED_DEPOSIT, "random")], "title", "description", None, Some("Sent more than one denomination") ; "additional invalid coin deposit")] +fn check_proposal_validation( + funds: Vec, + title: &str, + description: &str, + link: Option<&str>, + expected_error: Option<&str>, +) { + // Linter is not able to properly parse test_case macro; keep this line + let _ = coins(0, "keep_it"); + let _ = coin(0, "keep_it"); + + let mut deps = mock_dependencies(); + let env = mock_env(); + + // Mocked instantiation + PROPOSAL_COUNT + .save(deps.as_mut().storage, &Uint64::zero()) + .unwrap(); + let config = Config { + xastro_denom: XASTRO_DENOM.to_string(), + xastro_denom_tracking: "".to_string(), + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: Addr::unchecked(""), + proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), + proposal_effective_delay: *DELAY_INTERVAL.start(), + proposal_expiration_period: *EXPIRATION_PERIOD_INTERVAL.start(), + proposal_required_deposit: PROPOSAL_REQUIRED_DEPOSIT.into(), + proposal_required_quorum: Decimal::from_str(MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE) + .unwrap(), + proposal_required_threshold: Decimal::from_atomics( + MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, + 2, + ) + .unwrap(), + whitelisted_links: vec!["https://some.link/".to_string()], + guardian_addr: None, + }; + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + let result = submit_proposal( + deps.as_mut(), + mock_env(), + mock_info("creator", &funds), + title.to_string(), + description.to_string(), + link.map(|s| s.to_string()), + vec![], + None, + ); + + if let Some(err_msg) = expected_error { + assert_eq!(err_msg, result.unwrap_err().to_string()) + } else { + result.unwrap(); + + let bin_resp = query( + deps.as_ref(), + env.clone(), + QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + let proposal: Proposal = from_json(&bin_resp).unwrap(); + + assert_eq!( + proposal, + Proposal { + proposal_id: 1u64.into(), + submitter: Addr::unchecked("creator"), + status: ProposalStatus::Active, + for_power: Default::default(), + outpost_for_power: Default::default(), + against_power: Default::default(), + outpost_against_power: Default::default(), + start_block: env.block.height, + start_time: env.block.time.seconds(), + end_block: env.block.height + config.proposal_voting_period, + delayed_end_block: env.block.height + + config.proposal_voting_period + + config.proposal_effective_delay, + expiration_block: env.block.height + + config.proposal_voting_period + + config.proposal_effective_delay + + config.proposal_expiration_period, + title: title.to_string(), + description: description.to_string(), + link: link.map(|s| s.to_string()), + messages: vec![], + deposit_amount: funds[0].amount, + ibc_channel: None, + } + ); + } +} diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index 8778235e..2d82ef1b 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -12,9 +12,8 @@ use cw_multi_test::{ Executor, FailingModule, StakeKeeper, WasmKeeper, TOKEN_FACTORY_MODULE, }; -use astroport_governance::assembly; use astroport_governance::assembly::{ - DELAY_INTERVAL, DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, + InstantiateMsg, DELAY_INTERVAL, DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE, MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, VOTING_PERIOD_INTERVAL, }; @@ -61,6 +60,40 @@ fn builder_contract() -> Box> { )) } +fn vxastro_contract() -> Box> { + Box::new(ContractWrapper::new_with_empty( + voting_escrow_lite::execute::execute, + voting_escrow_lite::contract::instantiate, + voting_escrow_lite::query::query, + )) +} + +pub const PROPOSAL_REQUIRED_DEPOSIT: Uint128 = Uint128::new(*DEPOSIT_INTERVAL.start()); + +pub fn default_init_msg(staking: &Addr, builder_unlock: &Addr) -> InstantiateMsg { + InstantiateMsg { + staking_addr: staking.to_string(), + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller_addr: None, + hub_addr: None, + builder_unlock_addr: builder_unlock.to_string(), + proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), + proposal_effective_delay: *DELAY_INTERVAL.start(), + proposal_expiration_period: *EXPIRATION_PERIOD_INTERVAL.start(), + proposal_required_deposit: PROPOSAL_REQUIRED_DEPOSIT, + proposal_required_quorum: MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE.to_string(), + proposal_required_threshold: Decimal::from_atomics( + MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, + 2, + ) + .unwrap() + .to_string(), + whitelisted_links: vec!["https://some.link/".to_string()], + } +} + pub type CustomizedApp = App< BankKeeper, MockApi, @@ -80,7 +113,9 @@ pub struct Helper { pub staking: Addr, pub assembly: Addr, pub builder_unlock: Addr, + pub vxastro: Addr, pub xastro_denom: String, + pub assembly_code_id: u64, } pub const ASTRO_DENOM: &str = "factory/assembly/ASTRO"; @@ -100,7 +135,7 @@ impl Helper { let tracker_code_id = app.store_code(tracker_contract()); let assembly_code_id = app.store_code(assembly_contract()); - let msg = astroport::staking::InstantiateMsg { + let msg = staking::InstantiateMsg { deposit_token_denom: ASTRO_DENOM.to_string(), tracking_admin: owner.to_string(), tracking_code_id: tracker_code_id, @@ -140,32 +175,37 @@ impl Helper { ) .unwrap(); - let msg = assembly::InstantiateMsg { - staking_addr: staking.to_string(), - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, + let vxastro_code_id = app.store_code(vxastro_contract()); + + let msg = astroport_governance::voting_escrow_lite::InstantiateMsg { + owner: owner.to_string(), + guardian_addr: Some(owner.to_string()), + deposit_token_addr: xastro_denom.to_string(), generator_controller_addr: None, - hub_addr: None, - builder_unlock_addr: builder_unlock.to_string(), - proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), - proposal_effective_delay: *DELAY_INTERVAL.start(), - proposal_expiration_period: *EXPIRATION_PERIOD_INTERVAL.start(), - proposal_required_deposit: (*DEPOSIT_INTERVAL.start()).into(), - proposal_required_quorum: MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE.to_string(), - proposal_required_threshold: Decimal::from_atomics( - MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, - 2, - ) - .unwrap() - .to_string(), - whitelisted_links: vec!["https://some.link/".to_string()], + outpost_addr: None, + marketing: None, + logo_urls_whitelist: vec![], }; + + let vxastro = app + .instantiate_contract( + vxastro_code_id, + owner.clone(), + &msg, + &[], + "vxASTRO".to_string(), + None, + ) + .unwrap(); + let assembly = app .instantiate_contract( assembly_code_id, owner.clone(), - &msg, + &InstantiateMsg { + vxastro_token_addr: Some(vxastro.to_string()), + ..default_init_msg(&staking, &builder_unlock) + }, &[], String::from("Astroport Assembly"), None, @@ -178,7 +218,9 @@ impl Helper { staking, assembly, builder_unlock, + vxastro, xastro_denom, + assembly_code_id, }) } @@ -210,6 +252,16 @@ impl Helper { ) } + pub fn mint_tokens(&mut self, recipient: &Addr, amount: impl Into + Copy) { + self.give_astro(amount.into(), recipient); + self.stake(recipient, amount.into()).unwrap(); + } + + pub fn mint_vxastro(&mut self, recipient: &Addr, amount: impl Into + Copy) { + self.mint_tokens(recipient, amount); + // TODO: stake in voting escrow + } + pub fn query_balance(&self, sender: &Addr, denom: &str) -> StdResult { self.app .wrap() diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index eeb7b100..7700901f 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -38,561 +38,161 @@ // const PROPOSAL_REQUIRED_THRESHOLD: &str = "0.60"; // -mod common; +use astroport_governance::assembly; +use cosmwasm_std::{coins, Addr, Uint128}; +use cw_multi_test::Executor; + +use astroport_governance::assembly::{Config, InstantiateMsg, ProposalListResponse, QueryMsg}; +use astroport_governance::builder_unlock::{AllocationParams, Schedule}; + +use crate::common::helper::{default_init_msg, Helper, PROPOSAL_REQUIRED_DEPOSIT}; -use crate::common::helper::Helper; -use cosmwasm_std::Addr; +mod common; #[test] -fn test_new_suite() { +fn test_contract_instantiation() { let owner = Addr::unchecked("owner"); - let mut helper = Helper::new(&owner).unwrap(); + + let assembly_code = helper.assembly_code_id; + let staking = helper.staking.clone(); + let builder_unlock = helper.builder_unlock.clone(); + + // Try to instantiate assembly with wrong threshold + let err = helper + .app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_required_threshold: "0.3".to_string(), + ..default_init_msg(&staking, &builder_unlock) + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" + ); + + let err = helper + .app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_required_threshold: "1.1".to_string(), + ..default_init_msg(&staking, &builder_unlock) + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" + ); + + let err = helper + .app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_required_quorum: "1.1".to_string(), + ..default_init_msg(&staking, &builder_unlock) + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The required quorum for a proposal cannot be lower than 1% or higher than 100%" + ); + + let err = helper + .app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_expiration_period: 500, + ..default_init_msg(&staking, &builder_unlock) + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The expiration period for a proposal cannot be lower than 12342 or higher than 100800" + ); + + let err = helper + .app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + proposal_effective_delay: 400, + ..default_init_msg(&staking, &builder_unlock) + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.root_cause().to_string(), + "Generic error: The effective delay for a proposal cannot be lower than 6171 or higher than 14400" + ); + + let assembly_instance = helper + .app + .instantiate_contract( + assembly_code, + owner.clone(), + &default_init_msg(&staking, &builder_unlock), + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap(); + + let res: Config = helper + .app + .wrap() + .query_wasm_smart(assembly_instance, &QueryMsg::Config {}) + .unwrap(); + + assert_eq!(res.xastro_denom, helper.xastro_denom); + assert_eq!(res.builder_unlock_addr, helper.builder_unlock); + assert_eq!( + res.whitelisted_links, + vec!["https://some.link/".to_string(),] + ); } -// #[test] -// fn test_contract_instantiation() { -// let mut app = mock_app(); -// -// let owner = Addr::unchecked("owner"); -// -// // Instantiate needed contracts -// let token_addr = instantiate_astro_token(&mut app, &owner); -// let (staking_addr, xastro_token_addr) = instantiate_xastro_token(&mut app, &owner, &token_addr); -// let vxastro_token_addr = instantiate_vxastro_token(&mut app, &owner, &xastro_token_addr); -// let builder_unlock_addr = instantiate_builder_unlock_contract(&mut app, &owner, &token_addr); -// -// let assembly_contract = Box::new(ContractWrapper::new_with_empty( -// astro_assembly::contract::execute, -// astro_assembly::contract::instantiate, -// astro_assembly::queries::query, -// )); -// -// let assembly_code = app.store_code(assembly_contract); -// -// let assembly_default_instantiate_msg = InstantiateMsg { -// staking_addr: staking_addr.to_string(), -// vxastro_token_addr: Some(vxastro_token_addr.to_string()), -// voting_escrow_delegator_addr: None, -// ibc_controller: None, -// generator_controller_addr: None, -// hub_addr: None, -// builder_unlock_addr: builder_unlock_addr.to_string(), -// proposal_voting_period: PROPOSAL_VOTING_PERIOD, -// proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, -// proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, -// proposal_required_deposit: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), -// proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), -// whitelisted_links: vec!["https://some.link/".to_string()], -// }; -// -// // Try to instantiate assembly with wrong threshold -// let err = app -// .instantiate_contract( -// assembly_code, -// owner.clone(), -// &InstantiateMsg { -// proposal_required_threshold: "0.3".to_string(), -// ..assembly_default_instantiate_msg.clone() -// }, -// &[], -// "Assembly".to_string(), -// Some(owner.to_string()), -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" -// ); -// -// let err = app -// .instantiate_contract( -// assembly_code, -// owner.clone(), -// &InstantiateMsg { -// proposal_required_threshold: "1.1".to_string(), -// ..assembly_default_instantiate_msg.clone() -// }, -// &[], -// "Assembly".to_string(), -// Some(owner.to_string()), -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" -// ); -// -// let err = app -// .instantiate_contract( -// assembly_code, -// owner.clone(), -// &InstantiateMsg { -// proposal_required_quorum: "1.1".to_string(), -// ..assembly_default_instantiate_msg.clone() -// }, -// &[], -// "Assembly".to_string(), -// Some(owner.to_string()), -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: The required quorum for a proposal cannot be lower than 1% or higher than 100%" -// ); -// -// let err = app -// .instantiate_contract( -// assembly_code, -// owner.clone(), -// &InstantiateMsg { -// proposal_expiration_period: 500, -// ..assembly_default_instantiate_msg.clone() -// }, -// &[], -// "Assembly".to_string(), -// Some(owner.to_string()), -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: The expiration period for a proposal cannot be lower than 12342 or higher than 100800" -// ); -// -// let err = app -// .instantiate_contract( -// assembly_code, -// owner.clone(), -// &InstantiateMsg { -// proposal_effective_delay: 400, -// ..assembly_default_instantiate_msg.clone() -// }, -// &[], -// "Assembly".to_string(), -// Some(owner.to_string()), -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: The effective delay for a proposal cannot be lower than 6171 or higher than 14400" -// ); -// -// let assembly_instance = app -// .instantiate_contract( -// assembly_code, -// owner.clone(), -// &assembly_default_instantiate_msg, -// &[], -// "Assembly".to_string(), -// Some(owner.to_string()), -// ) -// .unwrap(); -// -// let res: Config = app -// .wrap() -// .query_wasm_smart(assembly_instance, &QueryMsg::Config {}) -// .unwrap(); -// -// assert_eq!(res.xastro_token_addr, xastro_token_addr); -// assert_eq!(res.builder_unlock_addr, builder_unlock_addr); -// assert_eq!(res.proposal_voting_period, PROPOSAL_VOTING_PERIOD); -// assert_eq!(res.proposal_effective_delay, PROPOSAL_EFFECTIVE_DELAY); -// assert_eq!(res.proposal_expiration_period, PROPOSAL_EXPIRATION_PERIOD); -// assert_eq!( -// res.proposal_required_deposit, -// Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) -// ); -// assert_eq!( -// res.proposal_required_quorum, -// Decimal::from_str(PROPOSAL_REQUIRED_QUORUM).unwrap() -// ); -// assert_eq!( -// res.proposal_required_threshold, -// Decimal::from_str(PROPOSAL_REQUIRED_THRESHOLD).unwrap() -// ); -// assert_eq!( -// res.whitelisted_links, -// vec!["https://some.link/".to_string(),] -// ); -// } -// -// #[test] -// fn test_proposal_submitting() { -// let mut app = mock_app(); -// -// let owner = Addr::unchecked("owner"); -// let user = Addr::unchecked("user1"); -// -// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = -// instantiate_contracts(&mut app, owner, false, false); -// -// let proposals: ProposalListResponse = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::Proposals { -// start: None, -// limit: None, -// }, -// ) -// .unwrap(); -// -// assert_eq!(proposals.proposal_count, Uint64::from(0u32)); -// assert_eq!(proposals.proposal_list, vec![]); -// -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &user, -// PROPOSAL_REQUIRED_DEPOSIT, -// ); -// -// check_token_balance(&mut app, &xastro_addr, &user, PROPOSAL_REQUIRED_DEPOSIT); -// -// // Try to create proposal with insufficient token deposit -// let submit_proposal_msg = Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("Title"), -// description: String::from("Description"), -// link: Some(String::from("https://some.link")), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT - 1), -// }; -// -// let err = app -// .execute_contract(user.clone(), xastro_addr.clone(), &submit_proposal_msg, &[]) -// .unwrap_err(); -// -// assert_eq!(err.root_cause().to_string(), "Insufficient token deposit!"); -// -// // Try to create a proposal with wrong title -// let err = app -// .execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("X"), -// description: String::from("Description"), -// link: Some(String::from("https://some.link/")), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: Title too short!" -// ); -// -// let err = app -// .execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from_utf8(vec![b'X'; 65]).unwrap(), -// description: String::from("Description"), -// link: Some(String::from("https://some.link/")), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: Title too long!" -// ); -// -// // Try to create a proposal with wrong description -// let err = app -// .execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("Title"), -// description: String::from("X"), -// link: Some(String::from("https://some.link/")), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: Description too short!" -// ); -// -// let err = app -// .execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("Title"), -// description: String::from_utf8(vec![b'X'; 1025]).unwrap(), -// link: Some(String::from("https://some.link/")), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: Description too long!" -// ); -// -// // Try to create a proposal with wrong link -// let err = app -// .execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("Title"), -// description: String::from("Description"), -// link: Some(String::from("X")), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: Link too short!" -// ); -// -// let err = app -// .execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("Title"), -// description: String::from("Description"), -// link: Some(String::from_utf8(vec![b'X'; 129]).unwrap()), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: Link too long!" -// ); -// -// let err = app -// .execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("Title"), -// description: String::from("Description"), -// link: Some(String::from("https://some1.link")), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: Link is not whitelisted!" -// ); -// -// let err = app -// .execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("Title"), -// description: String::from("Description"), -// link: Some(String::from( -// "https://some.link/", -// )), -// messages: None, -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Generic error: Link is not properly formatted or contains unsafe characters!" -// ); -// -// // Valid proposal submission -// app.execute_contract( -// user.clone(), -// xastro_addr.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly_addr.to_string(), -// msg: to_json_binary(&Cw20HookMsg::SubmitProposal { -// title: String::from("Title"), -// description: String::from("Description"), -// link: Some(String::from("https://some.link/q/")), -// messages: Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: assembly_addr.to_string(), -// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { -// xastro_token_addr: None, -// vxastro_token_addr: None, -// voting_escrow_delegator_addr: None, -// ibc_controller: None, -// generator_controller: None, -// hub: None, -// builder_unlock_addr: None, -// proposal_voting_period: Some(750), -// proposal_effective_delay: None, -// proposal_expiration_period: None, -// proposal_required_deposit: None, -// proposal_required_quorum: None, -// proposal_required_threshold: None, -// whitelist_add: None, -// whitelist_remove: None, -// guardian_addr: None, -// }))) -// .unwrap(), -// funds: vec![], -// })]), -// ibc_channel: None, -// }) -// .unwrap(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// }, -// &[], -// ) -// .unwrap(); -// -// let proposal: Proposal = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::Proposal { proposal_id: 1 }, -// ) -// .unwrap(); -// -// assert_eq!(proposal.proposal_id, Uint64::from(1u64)); -// assert_eq!(proposal.submitter, user); -// assert_eq!(proposal.status, ProposalStatus::Active); -// assert_eq!(proposal.for_power, Uint128::zero()); -// assert_eq!(proposal.against_power, Uint128::zero()); -// assert_eq!(proposal.start_block, 12_345); -// assert_eq!(proposal.end_block, 12_345 + PROPOSAL_VOTING_PERIOD); -// assert_eq!(proposal.title, String::from("Title")); -// assert_eq!(proposal.description, String::from("Description")); -// assert_eq!(proposal.link, Some(String::from("https://some.link/q/"))); -// assert_eq!( -// proposal.messages, -// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: assembly_addr.to_string(), -// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { -// xastro_token_addr: None, -// vxastro_token_addr: None, -// voting_escrow_delegator_addr: None, -// ibc_controller: None, -// generator_controller: None, -// hub: None, -// builder_unlock_addr: None, -// proposal_voting_period: Some(750), -// proposal_effective_delay: None, -// proposal_expiration_period: None, -// proposal_required_deposit: None, -// proposal_required_quorum: None, -// proposal_required_threshold: None, -// whitelist_add: None, -// whitelist_remove: None, -// guardian_addr: None, -// }))) -// .unwrap(), -// funds: vec![], -// })]) -// ); -// assert_eq!( -// proposal.deposit_amount, -// Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) -// ) -// } -// // #[test] // fn test_successful_proposal() { -// let mut app = mock_app(); -// // let owner = Addr::unchecked("owner"); -// -// let ( -// token_addr, -// staking_instance, -// xastro_addr, -// vxastro_addr, -// builder_unlock_addr, -// assembly_addr, -// _, -// _, -// ) = instantiate_contracts(&mut app, owner, false, false); +// let mut helper = Helper::new(&owner).unwrap(); // // // Init voting power for users // let balances: Vec<(&str, u128, u128)> = vec![ -// ("user0", PROPOSAL_REQUIRED_DEPOSIT, 0), // proposal submitter +// ("user0", PROPOSAL_REQUIRED_DEPOSIT.u128(), 0), // proposal submitter // ("user1", 20, 80), // ("user2", 100, 100), // ("user3", 300, 100), @@ -651,13 +251,7 @@ fn test_new_suite() { // // for (addr, xastro, vxastro) in balances { // if xastro > 0 { -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked(addr), -// xastro, -// ); +// helper.mint_tokens(&Addr::unchecked(addr), xastro); // } // // if vxastro > 0 { @@ -2125,15 +1719,6 @@ fn test_new_suite() { // .unwrap() // } // -// fn mint_tokens(app: &mut App, minter: &Addr, token: &Addr, recipient: &Addr, amount: u128) { -// let msg = Cw20ExecuteMsg::Mint { -// recipient: recipient.to_string(), -// amount: Uint128::from(amount), -// }; -// -// app.execute_contract(minter.clone(), token.to_owned(), &msg, &[]) -// .unwrap(); -// } // // fn mint_vxastro( // app: &mut App, From 565a1c0a436b689a9bdf77e6862e76e6f22cc4ef Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:32:43 +0400 Subject: [PATCH 16/47] WIP: vxastro lite adjustments for native xASTRO --- Cargo.lock | 67 ++-- contracts/assembly/Cargo.toml | 2 +- contracts/assembly/tests/common/helper.rs | 2 +- contracts/voting_escrow_lite/Cargo.toml | 21 +- contracts/voting_escrow_lite/src/contract.rs | 16 +- contracts/voting_escrow_lite/src/error.rs | 21 +- contracts/voting_escrow_lite/src/execute.rs | 94 +++-- contracts/voting_escrow_lite/src/utils.rs | 16 +- .../voting_escrow_lite/tests/integration.rs | 32 +- .../voting_escrow_lite/tests/test_utils.rs | 365 ++++++------------ packages/astroport-governance/Cargo.toml | 8 +- .../src/voting_escrow_lite.rs | 43 +-- packages/astroport-tests-lite/Cargo.toml | 2 +- packages/astroport-tests-lite/src/base.rs | 2 +- .../astroport-tests-lite/src/escrow_helper.rs | 2 +- 15 files changed, 265 insertions(+), 428 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21228f6a..11224dec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,7 @@ dependencies = [ "astroport-nft", "astroport-staking 2.0.0", "astroport-tokenfactory-tracker", + "astroport-voting-escrow-lite", "builder-unlock", "cosmwasm-schema", "cosmwasm-std", @@ -42,7 +43,6 @@ dependencies = [ "test-case", "thiserror", "voting-escrow-delegation", - "voting-escrow-lite", ] [[package]] @@ -185,7 +185,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 0.15.1", - "cw20 0.15.1", + "cw20 1.1.2", "thiserror", ] @@ -335,6 +335,7 @@ dependencies = [ "astroport-pair", "astroport-staking 1.1.0", "astroport-token", + "astroport-voting-escrow-lite", "astroport-whitelist", "cosmwasm-schema", "cosmwasm-std", @@ -342,7 +343,6 @@ dependencies = [ "cw2 0.15.1", "cw20 0.15.1", "generator-controller-lite", - "voting-escrow-lite", ] [[package]] @@ -355,7 +355,7 @@ dependencies = [ "cosmwasm-std", "cw2 0.15.1", "cw20 0.15.1", - "cw20-base", + "cw20-base 0.15.1", "snafu", ] @@ -372,6 +372,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "astroport-voting-escrow-lite" +version = "1.0.0" +dependencies = [ + "anyhow", + "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", + "astroport-governance 1.4.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-storage-plus 0.15.1", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "generator-controller-lite", + "proptest", + "thiserror", +] + [[package]] name = "astroport-whitelist" version = "1.0.1" @@ -896,6 +916,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw20-base" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ad79e86ea3707229bf78df94e08732e8f713207b4a77b2699755596725e7d9" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "cw20 1.1.2", + "schemars", + "semver", + "serde", + "thiserror", +] + [[package]] name = "cw3" version = "1.1.2" @@ -2044,7 +2081,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw2 0.15.1", "cw20 0.15.1", - "cw20-base", + "cw20-base 0.15.1", "proptest", "thiserror", ] @@ -2069,26 +2106,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "voting-escrow-lite" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport-governance 1.4.0", - "astroport-staking 1.1.0", - "astroport-token", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.15.1", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "cw20-base", - "generator-controller-lite", - "proptest", - "thiserror", -] - [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index cd905daa..1fbf74a7 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -29,7 +29,7 @@ cw-utils = "1" cw-multi-test = { git = "https://github.com/astroport-fi/cw-multi-test", branch = "feat/bank_with_send_hooks", features = ["cosmwasm_1_1"] } osmosis-std = "0.21" astroport-hub = { path = "../hub" } -voting-escrow-lite = { path = "../voting_escrow_lite" } +voting-escrow-lite = { package = "astroport-voting-escrow-lite", path = "../../contracts/voting_escrow_lite" } voting-escrow-delegation = { path = "../voting_escrow_delegation" } astroport-nft = { path = "../nft" } astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index 2d82ef1b..cabc23d8 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -180,7 +180,7 @@ impl Helper { let msg = astroport_governance::voting_escrow_lite::InstantiateMsg { owner: owner.to_string(), guardian_addr: Some(owner.to_string()), - deposit_token_addr: xastro_denom.to_string(), + deposit_denom: xastro_denom.to_string(), generator_controller_addr: None, outpost_addr: None, marketing: None, diff --git a/contracts/voting_escrow_lite/Cargo.toml b/contracts/voting_escrow_lite/Cargo.toml index d454d612..b358ce2c 100644 --- a/contracts/voting_escrow_lite/Cargo.toml +++ b/contracts/voting_escrow_lite/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "voting-escrow-lite" +name = "astroport-voting-escrow-lite" version = "1.0.0" authors = ["Astroport"] edition = "2021" @@ -21,21 +21,22 @@ crate-type = ["cdylib", "rlib"] # for quicker tests, cargo test --lib # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] +library = [] [dependencies] -cw2 = "0.15" -cw20 = "0.15" -cw20-base = { version = "0.15", features = ["library"] } -cosmwasm-std = "1.1" +cw2 = "1.1" +cw20 = "1.1" +cw-utils = "1" +cosmwasm-std = "1.5" cw-storage-plus = "0.15" -thiserror = { version = "1.0" } +thiserror = "1" astroport-governance = { path = "../../packages/astroport-governance" } -cosmwasm-schema = "1.1" +cosmwasm-schema = "1.5" [dev-dependencies] -cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +cw-multi-test = "0.20" astroport-generator-controller = { path = "../../contracts/generator_controller_lite", package = "generator-controller-lite" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } +cw20-base = { version = "1.1", features = ["library"] } anyhow = "1" proptest = "1.0" diff --git a/contracts/voting_escrow_lite/src/contract.rs b/contracts/voting_escrow_lite/src/contract.rs index 7df5f783..6c59863b 100644 --- a/contracts/voting_escrow_lite/src/contract.rs +++ b/contracts/voting_escrow_lite/src/contract.rs @@ -5,16 +5,16 @@ use cw2::set_contract_version; use cw20::{Logo, LogoInfo, MarketingInfoResponse}; use cw20_base::state::{TokenInfo, LOGO, MARKETING_INFO, TOKEN_INFO}; -use crate::astroport::asset::addr_opt_validate; use astroport_governance::utils::DEFAULT_UNLOCK_PERIOD; -use astroport_governance::voting_escrow_lite::{Config, InstantiateMsg, MigrateMsg}; +use astroport_governance::voting_escrow_lite::{Config, InstantiateMsg}; +use crate::astroport::asset::{addr_opt_validate, validate_native_denom}; use crate::error::ContractError; use crate::marketing_validation::{validate_marketing_info, validate_whitelist_links}; use crate::state::{BLACKLIST, CONFIG, VOTING_POWER_HISTORY}; /// Contract name that is used for migration. -const CONTRACT_NAME: &str = "astro-voting-escrow-lite"; +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); /// Contract version that is used for migration. const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -27,7 +27,7 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let deposit_token_addr = deps.api.addr_validate(&msg.deposit_token_addr)?; + validate_native_denom(&msg.deposit_denom)?; validate_whitelist_links(&msg.logo_urls_whitelist)?; let guardian_addr = addr_opt_validate(deps.api, &msg.guardian_addr)?; @@ -45,7 +45,7 @@ pub fn instantiate( let config = Config { owner: deps.api.addr_validate(&msg.owner)?, guardian_addr, - deposit_token_addr, + deposit_denom: msg.deposit_denom, logo_urls_whitelist: msg.logo_urls_whitelist.clone(), unlock_period: DEFAULT_UNLOCK_PERIOD, generator_controller_addr: addr_opt_validate(deps.api, &msg.generator_controller_addr)?, @@ -105,9 +105,3 @@ pub fn instantiate( Ok(Response::default()) } - -/// Manages contract migration. -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - Err(ContractError::MigrationError {}) -} diff --git a/contracts/voting_escrow_lite/src/error.rs b/contracts/voting_escrow_lite/src/error.rs index c00fb504..55baa4c4 100644 --- a/contracts/voting_escrow_lite/src/error.rs +++ b/contracts/voting_escrow_lite/src/error.rs @@ -1,5 +1,6 @@ use cosmwasm_std::{OverflowError, StdError}; use cw20_base::ContractError as cw20baseError; +use cw_utils::PaymentError; use thiserror::Error; /// This enum describes vxASTRO contract errors @@ -11,6 +12,12 @@ pub enum ContractError { #[error("{0}")] Cw20Base(#[from] cw20baseError), + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + #[error("Unauthorized")] Unauthorized {}, @@ -20,9 +27,6 @@ pub enum ContractError { #[error("Lock does not exist")] LockDoesNotExist {}, - #[error("Lock time must be within limits (week <= lock time < 2 years)")] - LockTimeLimitsError {}, - #[error("The lock time has not yet expired")] LockHasNotExpired {}, @@ -35,18 +39,9 @@ pub enum ContractError { #[error("Marketing info validation error: {0}")] MarketingInfoValidationError(String), - #[error("Contract can't be migrated!")] - MigrationError {}, - #[error("Already unlocking")] Unlocking {}, #[error("The lock has not been unlocked, call unlock first")] - NotUnlocked, -} - -impl From for ContractError { - fn from(o: OverflowError) -> Self { - StdError::from(o).into() - } + NotUnlocked {}, } diff --git a/contracts/voting_escrow_lite/src/execute.rs b/contracts/voting_escrow_lite/src/execute.rs index d4b5cfe4..f5235615 100644 --- a/contracts/voting_escrow_lite/src/execute.rs +++ b/contracts/voting_escrow_lite/src/execute.rs @@ -1,30 +1,28 @@ -use crate::astroport; use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; - -use astroport_governance::{generator_controller_lite, outpost}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, from_json, to_json_binary, Addr, CosmosMsg, DepsMut, Env, MessageInfo, Response, - StdError, StdResult, Storage, Uint128, WasmMsg, + attr, to_json_binary, Addr, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, + StdResult, Storage, Uint128, WasmMsg, }; -use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg}; +use cw20::Cw20ExecuteMsg; use cw20_base::contract::{execute_update_marketing, execute_upload_logo}; use cw20_base::state::MARKETING_INFO; +use cw_utils::must_pay; -use crate::astroport::common::validate_addresses; -use astroport_governance::voting_escrow_lite::{Config, Cw20HookMsg, ExecuteMsg}; +use astroport_governance::voting_escrow_lite::{Config, ExecuteMsg}; +use astroport_governance::{generator_controller_lite, outpost}; +use crate::astroport; +use crate::astroport::common::validate_addresses; use crate::error::ContractError; use crate::marketing_validation::{validate_marketing_info, validate_whitelist_links}; use crate::state::{Lock, BLACKLIST, CONFIG, LOCKED, OWNERSHIP_PROPOSAL, VOTING_POWER_HISTORY}; -use crate::utils::{blacklist_check, fetch_last_checkpoint, xastro_token_check}; +use crate::utils::{blacklist_check, fetch_last_checkpoint}; /// Exposes all the execute functions available in the contract. /// /// ## Execute messages -/// * **ExecuteMsg::Receive(msg)** Parse incoming messages coming from the xASTRO token contract. -/// /// * **ExecuteMsg::Unlock {}** Unlock all xASTRO from a lock position, subject to a waiting period until withdrawal is possible. /// /// * **ExecuteMsg::Relock {}** Relock all xASTRO from an unlocking position if the Hub could not be notified @@ -54,7 +52,31 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::CreateLock {} => { + blacklist_check(deps.storage, &info.sender)?; + + let config = CONFIG.load(deps.storage)?; + let amount = must_pay(&info, &config.deposit_denom)?; + + create_lock(deps, env, info.sender, amount) + } + ExecuteMsg::DepositFor { user } => { + let addr = deps.api.addr_validate(&user)?; + blacklist_check(deps.storage, &addr)?; + + let config = CONFIG.load(deps.storage)?; + let amount = must_pay(&info, &config.deposit_denom)?; + + deposit_for(deps, env, amount, addr) + } + ExecuteMsg::ExtendLockAmount {} => { + blacklist_check(deps.storage, &info.sender)?; + + let config = CONFIG.load(deps.storage)?; + let amount = must_pay(&info, &config.deposit_denom)?; + + deposit_for(deps, env, amount, info.sender) + } ExecuteMsg::Unlock {} => unlock(deps, env, info), ExecuteMsg::Relock { user } => relock(deps, env, info, user), ExecuteMsg::Withdraw {} => withdraw(deps, env, info), @@ -129,31 +151,6 @@ pub fn execute( } } -/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. -/// Only allows messages coming from the xASTRO token contract. -/// -/// * **cw20_msg** CW20 message to process. -fn receive_cw20( - deps: DepsMut, - env: Env, - info: MessageInfo, - cw20_msg: Cw20ReceiveMsg, -) -> Result { - xastro_token_check(deps.storage, info.sender)?; - let sender = Addr::unchecked(cw20_msg.sender); - blacklist_check(deps.storage, &sender)?; - - match from_json(&cw20_msg.msg)? { - Cw20HookMsg::CreateLock { .. } => create_lock(deps, env, sender, cw20_msg.amount), - Cw20HookMsg::ExtendLockAmount {} => deposit_for(deps, env, cw20_msg.amount, sender), - Cw20HookMsg::DepositFor { user } => { - let addr = deps.api.addr_validate(&user)?; - blacklist_check(deps.storage, &addr)?; - deposit_for(deps, env, cw20_msg.amount, addr) - } - } -} - /// Creates a lock for the user that lasts until Unlock is called /// Creates a lock if it doesn't exist and triggers a [`checkpoint`] for the staker. /// If a lock already exists, then a [`ContractError`] is returned. @@ -274,9 +271,10 @@ fn unlock(deps: DepsMut, env: Env, info: MessageInfo) -> Result { - return Err(ContractError::Std(StdError::GenericErr { - msg: "Either Generator Controller or Outpost must be set".to_string(), - })); + return Err(StdError::generic_err( + "Either Generator Controller or Outpost must be set", + ) + .into()); } }; @@ -364,7 +362,7 @@ fn withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> Result>, - remove_addrs: Option>, + append_addrs: Vec, + remove_addrs: Vec, ) -> Result { + if append_addrs.is_empty() && remove_addrs.is_empty() { + return Err(StdError::generic_err("Append and remove arrays are empty").into()); + } + let config = CONFIG.load(deps.storage)?; // Permission check if info.sender != config.owner && Some(info.sender) != config.guardian_addr { return Err(ContractError::Unauthorized {}); } - let append_addrs = append_addrs.unwrap_or_default(); - let remove_addrs = remove_addrs.unwrap_or_default(); let blacklist = BLACKLIST.load(deps.storage)?; let append: Vec<_> = validate_addresses(deps.api, &append_addrs)? .into_iter() @@ -412,10 +412,6 @@ fn update_blacklist( .filter(|addr| blacklist.contains(addr)) .collect(); - if append.is_empty() && remove.is_empty() { - return Err(StdError::generic_err("Append and remove arrays are empty").into()); - } - let timestamp = env.block.time.seconds(); let mut reduce_total_vp = Uint128::zero(); // accumulator for decreasing total voting power diff --git a/contracts/voting_escrow_lite/src/utils.rs b/contracts/voting_escrow_lite/src/utils.rs index 31c0827a..5d02a1cc 100644 --- a/contracts/voting_escrow_lite/src/utils.rs +++ b/contracts/voting_escrow_lite/src/utils.rs @@ -1,22 +1,12 @@ -use crate::{error::ContractError, state::VOTING_POWER_HISTORY}; - use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; use cw_storage_plus::Bound; -use crate::state::{BLACKLIST, CONFIG}; - -/// Checks that the sender is the xASTRO token. -pub(crate) fn xastro_token_check(storage: &dyn Storage, sender: Addr) -> Result<(), ContractError> { - let config = CONFIG.load(storage)?; - if sender != config.deposit_token_addr { - Err(ContractError::Unauthorized {}) - } else { - Ok(()) - } -} +use crate::state::BLACKLIST; +use crate::{error::ContractError, state::VOTING_POWER_HISTORY}; /// Checks if the blacklist contains a specific address. pub(crate) fn blacklist_check(storage: &dyn Storage, addr: &Addr) -> Result<(), ContractError> { + // TODO: use Map instead of raw array which could be potentially hit gas limit let blacklist = BLACKLIST.load(storage)?; if blacklist.contains(addr) { Err(ContractError::AddressBlacklisted(addr.to_string())) diff --git a/contracts/voting_escrow_lite/tests/integration.rs b/contracts/voting_escrow_lite/tests/integration.rs index 0a82b18b..65696991 100644 --- a/contracts/voting_escrow_lite/tests/integration.rs +++ b/contracts/voting_escrow_lite/tests/integration.rs @@ -2,25 +2,19 @@ use astroport::token as astro; use cosmwasm_std::{attr, to_json_binary, Addr, StdError, Uint128, Uint64}; use cw20::{Cw20ExecuteMsg, Logo, LogoInfo, MarketingInfoResponse, MinterResponse}; use cw_multi_test::{next_block, ContractWrapper, Executor}; -use voting_escrow_lite::astroport; use astroport_governance::utils::{get_lite_period, WEEK}; -use astroport_governance::voting_escrow_lite::{ - Config, Cw20HookMsg, ExecuteMsg, LockInfoResponse, QueryMsg, -}; +use astroport_governance::voting_escrow_lite::{Config, ExecuteMsg, LockInfoResponse, QueryMsg}; -use crate::test_utils::{mock_app, Helper, MULTIPLIER}; +use crate::test_utils::{Helper, MULTIPLIER}; mod test_utils; #[test] fn lock_unlock_logic() { - let mut router = mock_app(); - let router_ref = &mut router; - let owner = Addr::unchecked("owner"); - let helper = Helper::init(router_ref, owner); + let mut helper = Helper::init(); - helper.mint_xastro(router_ref, "owner", 100); + helper.mint_xastro("owner", 100); // Mint ASTRO, stake it and mint xASTRO helper.mint_xastro(router_ref, "user", 100); @@ -127,8 +121,6 @@ fn lock_unlock_logic() { #[test] fn random_token_lock() { - let mut router = mock_app(); - let router_ref = &mut router; let owner = Addr::unchecked("owner"); let helper = Helper::init(router_ref, owner); @@ -185,8 +177,6 @@ fn random_token_lock() { #[test] fn new_lock_after_unlock() { - let mut router = mock_app(); - let router_ref = &mut router; let helper = Helper::init(router_ref, Addr::unchecked("owner")); helper.mint_xastro(router_ref, "owner", 100); @@ -243,8 +233,6 @@ fn new_lock_after_unlock() { /// Plot for this test case is generated at tests/plots/variable_decay.png #[test] fn emissions_voting_no_decay() { - let mut router = mock_app(); - let router_ref = &mut router; let helper = Helper::init(router_ref, Addr::unchecked("owner")); helper.mint_xastro(router_ref, "owner", 100); @@ -336,8 +324,6 @@ fn emissions_voting_no_decay() { #[test] fn check_queries() { - let mut router = mock_app(); - let router_ref = &mut router; let owner = Addr::unchecked("owner"); let helper = Helper::init(router_ref, owner); helper.mint_xastro(router_ref, "owner", 100); @@ -601,8 +587,6 @@ fn check_queries() { #[test] fn check_deposit_for() { - let mut router = mock_app(); - let router_ref = &mut router; let owner = Addr::unchecked("owner"); let helper = Helper::init(router_ref, owner); helper.mint_xastro(router_ref, "owner", 100); @@ -713,8 +697,6 @@ fn check_update_owner() { #[test] fn check_blacklist() { - let mut router = mock_app(); - let router_ref = &mut router; let owner = Addr::unchecked("owner"); let helper = Helper::init(router_ref, owner); @@ -878,8 +860,6 @@ fn check_blacklist() { #[test] fn check_residual() { - let mut router = mock_app(); - let router_ref = &mut router; let owner = Addr::unchecked("owner"); let helper = Helper::init(router_ref, owner); let lock_duration = 104; @@ -969,8 +949,6 @@ fn check_residual() { #[test] fn total_vp_multiple_slope_subtraction() { - let mut router = mock_app(); - let router_ref = &mut router; let owner = Addr::unchecked("owner"); let helper = Helper::init(router_ref, owner); @@ -1019,8 +997,6 @@ fn total_vp_multiple_slope_subtraction() { #[test] fn marketing_info() { - let mut router = mock_app(); - let router_ref = &mut router; let owner = Addr::unchecked("owner"); let helper = Helper::init(router_ref, owner); diff --git a/contracts/voting_escrow_lite/tests/test_utils.rs b/contracts/voting_escrow_lite/tests/test_utils.rs index 5dc10557..3aabb439 100644 --- a/contracts/voting_escrow_lite/tests/test_utils.rs +++ b/contracts/voting_escrow_lite/tests/test_utils.rs @@ -1,116 +1,102 @@ +#![allow(dead_code)] + use anyhow::Result; -use astroport::{staking as xastro, token as astro}; +use cosmwasm_std::{ + attr, coins, to_json_binary, Addr, BlockInfo, StdResult, Timestamp, Uint128, Uint64, +}; +use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, Logo}; +use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor}; + use astroport_governance::utils::EPOCH_START; use astroport_governance::voting_escrow_lite::{ - BlacklistedVotersResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, QueryMsg, - UpdateMarketingInfo, VotingPowerResponse, + BlacklistedVotersResponse, ExecuteMsg, InstantiateMsg, QueryMsg, UpdateMarketingInfo, + VotingPowerResponse, }; -use cosmwasm_std::testing::{mock_env, MockApi, MockStorage}; -use cosmwasm_std::{ - attr, to_json_binary, Addr, QueryRequest, StdResult, Timestamp, Uint128, Uint64, WasmQuery, -}; -use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, Logo, MinterResponse}; -use cw_multi_test::{App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor}; -use voting_escrow_lite::astroport; -pub const MULTIPLIER: u64 = 1000000; +pub const MULTIPLIER: u64 = 1_000000; + +pub const XASTRO_DENOM: &str = "factory/assembly/xASTRO"; + +pub const OWNER: &str = "owner"; pub struct Helper { + pub app: App, pub owner: Addr, - pub astro_token: Addr, - pub staking_instance: Addr, - pub xastro_token: Addr, - pub voting_instance: Addr, + pub vxastro: Addr, + pub generator_controller: Addr, } impl Helper { - pub fn init(router: &mut App, owner: Addr) -> Self { - let astro_token_contract = Box::new(ContractWrapper::new_with_empty( - astroport_token::contract::execute, - astroport_token::contract::instantiate, - astroport_token::contract::query, - )); - - let astro_token_code_id = router.store_code(astro_token_contract); - - let msg = astro::InstantiateMsg { - name: String::from("Astro token"), - symbol: String::from("ASTRO"), - decimals: 6, - initial_balances: vec![], - mint: Some(MinterResponse { - minter: owner.to_string(), - cap: None, - }), - marketing: None, - }; + pub fn init() -> Self { + let owner = Addr::unchecked(OWNER); + + let mut app = AppBuilder::new() + .with_block(BlockInfo { + height: 1000, + time: Timestamp::from_seconds(EPOCH_START), + chain_id: "cw-multitest-1".to_string(), + }) + .build(|router, _, storage| { + router + .bank + .init_balance(storage, &owner, coins(u128::MAX, XASTRO_DENOM)) + .unwrap() + }); - let astro_token = router - .instantiate_contract( - astro_token_code_id, - owner.clone(), - &msg, - &[], - String::from("ASTRO"), - None, - ) - .unwrap(); + let voting_contract = Box::new(ContractWrapper::new_with_empty( + astroport_voting_escrow_lite::execute::execute, + astroport_voting_escrow_lite::contract::instantiate, + astroport_voting_escrow_lite::query::query, + )); - let staking_contract = Box::new( - ContractWrapper::new_with_empty( - astroport_staking::contract::execute, - astroport_staking::contract::instantiate, - astroport_staking::contract::query, - ) - .with_reply_empty(astroport_staking::contract::reply), - ); + let voting_code_id = app.store_code(voting_contract); - let staking_code_id = router.store_code(staking_contract); + let marketing_info = UpdateMarketingInfo { + project: Some("Astroport".to_string()), + description: Some("Astroport is a decentralized application for managing the supply of space resources.".to_string()), + marketing: Some(owner.to_string()), + logo: Some(Logo::Url("https://astroport.com/logo.png".to_string())), + }; - let msg = xastro::InstantiateMsg { + let msg = InstantiateMsg { owner: owner.to_string(), - token_code_id: astro_token_code_id, - deposit_token_addr: astro_token.to_string(), - marketing: None, + guardian_addr: Some("guardian".to_string()), + deposit_denom: XASTRO_DENOM.to_string(), + marketing: Some(marketing_info), + logo_urls_whitelist: vec!["https://astroport.com/".to_string()], + generator_controller_addr: None, + outpost_addr: None, }; - let staking_instance = router + let vxastro = app .instantiate_contract( - staking_code_id, + voting_code_id, owner.clone(), &msg, &[], - String::from("xASTRO"), + String::from("vxASTRO"), None, ) .unwrap(); - let res = router - .wrap() - .query::(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: staking_instance.to_string(), - msg: to_json_binary(&xastro::QueryMsg::Config {}).unwrap(), - })) - .unwrap(); - let generator_controller = Box::new(ContractWrapper::new_with_empty( astroport_generator_controller::contract::execute, astroport_generator_controller::contract::instantiate, astroport_generator_controller::contract::query, )); - let generator_controller_id = router.store_code(generator_controller); + let generator_controller_id = app.store_code(generator_controller); let msg = astroport_governance::generator_controller_lite::InstantiateMsg { owner: owner.to_string(), assembly_addr: "assembly".to_string(), - escrow_addr: "contract4".to_string(), + escrow_addr: vxastro.to_string(), factory_addr: "factory".to_string(), generator_addr: "generator".to_string(), hub_addr: None, pools_limit: 10, whitelisted_pools: vec![], }; - let generator_controller_instance = router + let generator_controller = app .instantiate_contract( generator_controller_id, owner.clone(), @@ -121,129 +107,57 @@ impl Helper { ) .unwrap(); - let voting_contract = Box::new(ContractWrapper::new_with_empty( - voting_escrow_lite::execute::execute, - voting_escrow_lite::contract::instantiate, - voting_escrow_lite::query::query, - )); - - let voting_code_id = router.store_code(voting_contract); - - let marketing_info = UpdateMarketingInfo { - project: Some("Astroport".to_string()), - description: Some("Astroport is a decentralized application for managing the supply of space resources.".to_string()), - marketing: Some(owner.to_string()), - logo: Some(Logo::Url("https://astroport.com/logo.png".to_string())), - }; - - let msg = InstantiateMsg { - owner: owner.to_string(), - guardian_addr: Some("guardian".to_string()), - deposit_token_addr: res.share_token_addr.to_string(), - marketing: Some(marketing_info), - logo_urls_whitelist: vec!["https://astroport.com/".to_string()], - generator_controller_addr: Some(generator_controller_instance.to_string()), - outpost_addr: None, - }; - let voting_instance = router - .instantiate_contract( - voting_code_id, - owner.clone(), - &msg, - &[], - String::from("vxASTRO"), - None, - ) - .unwrap(); + app.execute_contract( + owner.clone(), + vxastro.clone(), + &ExecuteMsg::UpdateConfig { + new_guardian: None, + generator_controller: Some(generator_controller.to_string()), + outpost: None, + }, + &[], + ) + .unwrap(); Self { + app, owner, - xastro_token: res.share_token_addr, - astro_token, - staking_instance, - voting_instance, + vxastro, + generator_controller, } } - pub fn mint_xastro(&self, router: &mut App, to: &str, amount: u64) { - let amount = amount * MULTIPLIER; - let msg = cw20::Cw20ExecuteMsg::Mint { - recipient: String::from(to), - amount: Uint128::from(amount), - }; - let res = router - .execute_contract(self.owner.clone(), self.astro_token.clone(), &msg, &[]) - .unwrap(); - assert_eq!(res.events[1].attributes[1], attr("action", "mint")); - assert_eq!(res.events[1].attributes[2], attr("to", String::from(to))); - assert_eq!( - res.events[1].attributes[3], - attr("amount", Uint128::from(amount)) - ); - - let to_addr = Addr::unchecked(to); - let msg = Cw20ExecuteMsg::Send { - contract: self.staking_instance.to_string(), - msg: to_json_binary(&xastro::Cw20HookMsg::Enter {}).unwrap(), - amount: Uint128::from(amount), - }; - router - .execute_contract(to_addr, self.astro_token.clone(), &msg, &[]) - .unwrap(); - } - - #[allow(dead_code)] - pub fn check_xastro_balance(&self, router: &mut App, user: &str, amount: u64) { - let amount = amount * MULTIPLIER; - let res: BalanceResponse = router - .wrap() - .query_wasm_smart( - self.xastro_token.clone(), - &Cw20QueryMsg::Balance { - address: user.to_string(), - }, + pub fn mint_xastro(&mut self, to: &str, amount: impl Into + Copy) { + self.app + .send_tokens( + self.owner.clone(), + Addr::unchecked(to), + &coins(amount.into(), XASTRO_DENOM), ) .unwrap(); - assert_eq!(res.balance.u128(), amount as u128); } - #[allow(dead_code)] - pub fn check_astro_balance(&self, router: &mut App, user: &str, amount: u64) { + pub fn check_xastro_balance(&self, user: &str, amount: u64) { let amount = amount * MULTIPLIER; - let res: BalanceResponse = router + let balance = self + .app .wrap() - .query_wasm_smart( - self.astro_token.clone(), - &Cw20QueryMsg::Balance { - address: user.to_string(), - }, - ) - .unwrap(); - assert_eq!(res.balance.u128(), amount as u128); + .query_balance(user, XASTRO_DENOM) + .unwrap() + .amount; + assert_eq!(balance.u128(), amount as u128); } - pub fn create_lock( - &self, - router: &mut App, - user: &str, - time: u64, - amount: f32, - ) -> Result { - let amount = (amount * MULTIPLIER as f32) as u64; - let cw20msg = Cw20ExecuteMsg::Send { - contract: self.voting_instance.to_string(), - amount: Uint128::from(amount), - msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), - }; - router.execute_contract( + pub fn create_lock(&mut self, user: &str, amount: f32) -> Result { + let amount = (amount * MULTIPLIER as f32) as u128; + self.app.execute_contract( Addr::unchecked(user), - self.xastro_token.clone(), - &cw20msg, - &[], + self.vxastro.clone(), + &ExecuteMsg::CreateLock {}, + &coins(amount, XASTRO_DENOM), ) } - #[allow(dead_code)] pub fn create_lock_u128( &self, router: &mut App, @@ -252,13 +166,13 @@ impl Helper { amount: u128, ) -> Result { let cw20msg = Cw20ExecuteMsg::Send { - contract: self.voting_instance.to_string(), + contract: self.vxastro.to_string(), amount: Uint128::from(amount), msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), }; router.execute_contract( Addr::unchecked(user), - self.xastro_token.clone(), + self.xastro_denom.clone(), &cw20msg, &[], ) @@ -272,23 +186,22 @@ impl Helper { ) -> Result { let amount = (amount * MULTIPLIER as f32) as u64; let cw20msg = Cw20ExecuteMsg::Send { - contract: self.voting_instance.to_string(), + contract: self.vxastro.to_string(), amount: Uint128::from(amount), msg: to_json_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), }; router.execute_contract( Addr::unchecked(user), - self.xastro_token.clone(), + self.xastro_denom.clone(), &cw20msg, &[], ) } - #[allow(dead_code)] pub fn relock(&self, router: &mut App, user: &str) -> Result { router.execute_contract( Addr::unchecked("outpost"), - self.voting_instance.clone(), + self.vxastro.clone(), &ExecuteMsg::Relock { user: user.to_string(), }, @@ -296,7 +209,6 @@ impl Helper { ) } - #[allow(dead_code)] pub fn deposit_for( &self, router: &mut App, @@ -306,7 +218,7 @@ impl Helper { ) -> Result { let amount = (amount * MULTIPLIER as f32) as u64; let cw20msg = Cw20ExecuteMsg::Send { - contract: self.voting_instance.to_string(), + contract: self.vxastro.to_string(), amount: Uint128::from(amount), msg: to_json_binary(&Cw20HookMsg::DepositFor { user: to.to_string(), @@ -315,17 +227,16 @@ impl Helper { }; router.execute_contract( Addr::unchecked(from), - self.xastro_token.clone(), + self.xastro_denom.clone(), &cw20msg, &[], ) } - #[allow(dead_code)] pub fn unlock(&self, router: &mut App, user: &str) -> Result { router.execute_contract( Addr::unchecked(user), - self.voting_instance.clone(), + self.vxastro.clone(), &ExecuteMsg::Unlock {}, &[], ) @@ -334,7 +245,7 @@ impl Helper { pub fn withdraw(&self, router: &mut App, user: &str) -> Result { router.execute_contract( Addr::unchecked(user), - self.voting_instance.clone(), + self.vxastro.clone(), &ExecuteMsg::Withdraw {}, &[], ) @@ -348,7 +259,7 @@ impl Helper { ) -> Result { router.execute_contract( Addr::unchecked("owner"), - self.voting_instance.clone(), + self.vxastro.clone(), &ExecuteMsg::UpdateBlacklist { append_addrs, remove_addrs, @@ -357,7 +268,6 @@ impl Helper { ) } - #[allow(dead_code)] pub fn update_outpost_address( &self, router: &mut App, @@ -365,7 +275,7 @@ impl Helper { ) -> Result { router.execute_contract( Addr::unchecked("owner"), - self.voting_instance.clone(), + self.vxastro.clone(), &ExecuteMsg::UpdateConfig { new_guardian: None, generator_controller: None, @@ -375,12 +285,11 @@ impl Helper { ) } - #[allow(dead_code)] pub fn query_user_vp(&self, router: &mut App, user: &str) -> StdResult { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::UserVotingPower { user: user.to_string(), }, @@ -388,12 +297,11 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_user_emissions_vp(&self, router: &mut App, user: &str) -> StdResult { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::UserEmissionsVotingPower { user: user.to_string(), }, @@ -401,12 +309,11 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_exact_user_vp(&self, router: &mut App, user: &str) -> StdResult { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::UserVotingPower { user: user.to_string(), }, @@ -414,12 +321,11 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128()) } - #[allow(dead_code)] pub fn query_exact_user_emissions_vp(&self, router: &mut App, user: &str) -> StdResult { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::UserEmissionsVotingPower { user: user.to_string(), }, @@ -427,12 +333,11 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128()) } - #[allow(dead_code)] pub fn query_user_vp_at(&self, router: &mut App, user: &str, time: u64) -> StdResult { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::UserVotingPowerAt { user: user.to_string(), time, @@ -441,7 +346,6 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_user_emissions_vp_at( &self, router: &mut App, @@ -451,7 +355,7 @@ impl Helper { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::UserEmissionsVotingPowerAt { user: user.to_string(), time, @@ -460,7 +364,6 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_user_vp_at_period( &self, router: &mut App, @@ -470,7 +373,7 @@ impl Helper { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::UserVotingPowerAtPeriod { user: user.to_string(), period, @@ -479,52 +382,44 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_total_vp(&self, router: &mut App) -> StdResult { router .wrap() - .query_wasm_smart(self.voting_instance.clone(), &QueryMsg::TotalVotingPower {}) + .query_wasm_smart(self.vxastro.clone(), &QueryMsg::TotalVotingPower {}) .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_total_emissions_vp(&self, router: &mut App) -> StdResult { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::TotalEmissionsVotingPower {}, ) .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_exact_total_vp(&self, router: &mut App) -> StdResult { router .wrap() - .query_wasm_smart(self.voting_instance.clone(), &QueryMsg::TotalVotingPower {}) + .query_wasm_smart(self.vxastro.clone(), &QueryMsg::TotalVotingPower {}) .map(|vp: VotingPowerResponse| vp.voting_power.u128()) } - #[allow(dead_code)] pub fn query_exact_total_emissions_vp(&self, router: &mut App) -> StdResult { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::TotalEmissionsVotingPower {}, ) .map(|vp: VotingPowerResponse| vp.voting_power.u128()) } - #[allow(dead_code)] pub fn query_total_vp_at(&self, router: &mut App, time: u64) -> StdResult { router .wrap() - .query_wasm_smart( - self.voting_instance.clone(), - &QueryMsg::TotalVotingPowerAt { time }, - ) + .query_wasm_smart(self.vxastro.clone(), &QueryMsg::TotalVotingPowerAt { time }) .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } @@ -532,24 +427,22 @@ impl Helper { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::TotalEmissionsVotingPowerAt { time }, ) .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_total_vp_at_period(&self, router: &mut App, period: u64) -> StdResult { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::TotalVotingPowerAtPeriod { period }, ) .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_total_emissions_vp_at_period( &self, router: &mut App, @@ -558,13 +451,12 @@ impl Helper { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::TotalEmissionsVotingPowerAt { time: timestamp }, ) .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_locked_balance_at( &self, router: &mut App, @@ -574,7 +466,7 @@ impl Helper { router .wrap() .query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::UserDepositAt { user: user.to_string(), timestamp, @@ -583,7 +475,6 @@ impl Helper { .map(|vp: Uint128| vp.u128() as f32 / MULTIPLIER as f32) } - #[allow(dead_code)] pub fn query_blacklisted_voters( &self, router: &mut App, @@ -591,35 +482,19 @@ impl Helper { limit: Option, ) -> StdResult> { router.wrap().query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::BlacklistedVoters { start_after, limit }, ) } - #[allow(dead_code)] pub fn check_voters_are_blacklisted( &self, router: &mut App, voters: Vec, ) -> StdResult { router.wrap().query_wasm_smart( - self.voting_instance.clone(), + self.vxastro.clone(), &QueryMsg::CheckVotersAreBlacklisted { voters }, ) } } - -pub fn mock_app() -> App { - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(EPOCH_START); - let api = MockApi::default(); - let bank = BankKeeper::new(); - let storage = MockStorage::new(); - - AppBuilder::new() - .with_api(api) - .with_block(env.block) - .with_bank(bank) - .with_storage(storage) - .build(|_, _, _| {}) -} diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 7d74bd46..878b0647 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -15,9 +15,9 @@ homepage = "https://astroport.fi" backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cw20 = "0.15" -cosmwasm-std = { version = "1.1", features = ["ibc3"] } +cw20 = "1.1" +cosmwasm-std = { version = "1.5", features = ["ibc3"] } cw-storage-plus = "0.15" -cosmwasm-schema = "1.1" +cosmwasm-schema = "1.5" astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "3" } -thiserror = { version = "1.0" } +thiserror = "1" diff --git a/packages/astroport-governance/src/voting_escrow_lite.rs b/packages/astroport-governance/src/voting_escrow_lite.rs index 9d6db04e..e66a8fc7 100644 --- a/packages/astroport-governance/src/voting_escrow_lite.rs +++ b/packages/astroport-governance/src/voting_escrow_lite.rs @@ -1,14 +1,13 @@ +use std::fmt; + +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, QuerierWrapper, StdResult, Uint128, Uint64}; +use cw20::{BalanceResponse, DownloadLogoResponse, Logo, MarketingInfoResponse, TokenInfoResponse}; + use crate::voting_escrow_lite::QueryMsg::{ LockInfo, TotalVotingPower, TotalVotingPowerAt, UserDepositAt, UserEmissionsVotingPower, UserVotingPower, UserVotingPowerAt, }; -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Binary, QuerierWrapper, StdResult, Uint128, Uint64}; -use cw20::{ - BalanceResponse, Cw20ReceiveMsg, DownloadLogoResponse, Logo, MarketingInfoResponse, - TokenInfoResponse, -}; -use std::fmt; /// ## Pagination settings /// The maximum amount of items that can be read at once from @@ -40,7 +39,7 @@ pub struct InstantiateMsg { /// Address that's allowed to black or whitelist contracts pub guardian_addr: Option, /// xASTRO token address - pub deposit_token_addr: String, + pub deposit_denom: String, /// Marketing info for vxASTRO pub marketing: Option, /// The list of whitelisted logo urls prefixes @@ -54,9 +53,12 @@ pub struct InstantiateMsg { /// This structure describes the execute functions in the contract. #[cw_serde] pub enum ExecuteMsg { - /// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received - /// template. - Receive(Cw20ReceiveMsg), + /// Create a vxASTRO position and lock xASTRO for `time` amount of time + CreateLock {}, + /// Deposit xASTRO in another user's vxASTRO position + DepositFor { user: String }, + /// Add more xASTRO to your vxASTRO position + ExtendLockAmount {}, /// Unlock xASTRO from the vxASTRO contract Unlock {}, /// Relock all xASTRO from an unlocking position if the Hub could not be notified @@ -71,8 +73,10 @@ pub enum ExecuteMsg { ClaimOwnership {}, /// Add or remove accounts from the blacklist UpdateBlacklist { - append_addrs: Option>, - remove_addrs: Option>, + #[serde(default)] + append_addrs: Vec, + #[serde(default)] + remove_addrs: Vec, }, /// Update the marketing info for the vxASTRO contract UpdateMarketing { @@ -95,17 +99,6 @@ pub enum ExecuteMsg { SetLogoUrlsWhitelist { whitelist: Vec }, } -/// This structure describes a CW20 hook message. -#[cw_serde] -pub enum Cw20HookMsg { - /// Create a vxASTRO position and lock xASTRO for `time` amount of time - CreateLock { time: u64 }, - /// Deposit xASTRO in another user's vxASTRO position - DepositFor { user: String }, - /// Add more xASTRO to your vxASTRO position - ExtendLockAmount {}, -} - /// This enum describes voters status. #[cw_serde] pub enum BlacklistedVotersResponse { @@ -216,7 +209,7 @@ pub struct Config { /// Address that can only blacklist vxASTRO stakers and remove their governance power pub guardian_addr: Option, /// The xASTRO token contract address - pub deposit_token_addr: Addr, + pub deposit_denom: String, /// The list of whitelisted logo urls prefixes pub logo_urls_whitelist: Vec, /// Minimum unlock wait time in seconds diff --git a/packages/astroport-tests-lite/Cargo.toml b/packages/astroport-tests-lite/Cargo.toml index 0f62c867..6c27a081 100644 --- a/packages/astroport-tests-lite/Cargo.toml +++ b/packages/astroport-tests-lite/Cargo.toml @@ -22,7 +22,7 @@ astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } astroport-escrow-fee-distributor = { path = "../../contracts/escrow_fee_distributor" } astroport-governance = { path = "../astroport-governance" } -voting-escrow-lite = { path = "../../contracts/voting_escrow_lite" } +voting-escrow-lite = { package = "astroport-voting-escrow-lite", path = "../../contracts/voting_escrow_lite" } generator-controller-lite = { path = "../../contracts/generator_controller_lite" } astro-assembly = { path = "../../contracts/assembly" } astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core" } diff --git a/packages/astroport-tests-lite/src/base.rs b/packages/astroport-tests-lite/src/base.rs index 85c08e65..adfdd7cd 100644 --- a/packages/astroport-tests-lite/src/base.rs +++ b/packages/astroport-tests-lite/src/base.rs @@ -151,7 +151,7 @@ impl BaseAstroportTestPackage { guardian_addr: Some("guardian".to_string()), marketing: None, owner: owner.to_string(), - deposit_token_addr: self.get_staking_xastro(router).to_string(), + deposit_denom: self.get_staking_xastro(router).to_string(), logo_urls_whitelist: vec![], generator_controller_addr: None, outpost_addr: None, diff --git a/packages/astroport-tests-lite/src/escrow_helper.rs b/packages/astroport-tests-lite/src/escrow_helper.rs index 68f60e64..a22ded4b 100644 --- a/packages/astroport-tests-lite/src/escrow_helper.rs +++ b/packages/astroport-tests-lite/src/escrow_helper.rs @@ -98,7 +98,7 @@ impl EscrowHelper { let msg = InstantiateMsg { owner: owner.to_string(), guardian_addr: Some("guardian".to_string()), - deposit_token_addr: res.share_token_addr.to_string(), + deposit_denom: res.share_token_addr.to_string(), marketing: None, logo_urls_whitelist: vec![], generator_controller_addr: None, From 4c4a1563d63cd0f2876fe5e3317d6aa7e538e793 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:21:54 +0400 Subject: [PATCH 17/47] rework vxastro to support native xASTRO --- Cargo.toml | 10 +- contracts/voting_escrow_lite/Cargo.toml | 2 +- contracts/voting_escrow_lite/src/execute.rs | 26 +- .../voting_escrow_lite/tests/test_utils.rs | 206 ++--- ...gration.rs => vxastro_lite_integration.rs} | 710 ++++++++---------- 5 files changed, 393 insertions(+), 561 deletions(-) rename contracts/voting_escrow_lite/tests/{integration.rs => vxastro_lite_integration.rs} (50%) diff --git a/Cargo.toml b/Cargo.toml index 3fcf3643..9eb838d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,14 @@ [workspace] resolver = "2" -members = ["packages/*", "contracts/*"] +members = [ + "packages/*", + "contracts/assembly", + "contracts/builder_unlock", + "contracts/generator_controller_lite", + "contracts/hub", + "contracts/outpost", + "contracts/voting_escrow_lite", +] [profile.release] opt-level = "z" diff --git a/contracts/voting_escrow_lite/Cargo.toml b/contracts/voting_escrow_lite/Cargo.toml index b358ce2c..8e2e7447 100644 --- a/contracts/voting_escrow_lite/Cargo.toml +++ b/contracts/voting_escrow_lite/Cargo.toml @@ -26,6 +26,7 @@ library = [] [dependencies] cw2 = "1.1" cw20 = "1.1" +cw20-base = { version = "1.1", features = ["library"] } cw-utils = "1" cosmwasm-std = "1.5" cw-storage-plus = "0.15" @@ -37,6 +38,5 @@ cosmwasm-schema = "1.5" cw-multi-test = "0.20" astroport-generator-controller = { path = "../../contracts/generator_controller_lite", package = "generator-controller-lite" } astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } -cw20-base = { version = "1.1", features = ["library"] } anyhow = "1" proptest = "1.0" diff --git a/contracts/voting_escrow_lite/src/execute.rs b/contracts/voting_escrow_lite/src/execute.rs index f5235615..78c84f3b 100644 --- a/contracts/voting_escrow_lite/src/execute.rs +++ b/contracts/voting_escrow_lite/src/execute.rs @@ -1,11 +1,9 @@ -use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, to_json_binary, Addr, CosmosMsg, DepsMut, Env, MessageInfo, Response, StdError, - StdResult, Storage, Uint128, WasmMsg, + attr, coins, to_json_binary, Addr, BankMsg, CosmosMsg, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Storage, Uint128, WasmMsg, }; -use cw20::Cw20ExecuteMsg; use cw20_base::contract::{execute_update_marketing, execute_upload_logo}; use cw20_base::state::MARKETING_INFO; use cw_utils::must_pay; @@ -13,8 +11,9 @@ use cw_utils::must_pay; use astroport_governance::voting_escrow_lite::{Config, ExecuteMsg}; use astroport_governance::{generator_controller_lite, outpost}; -use crate::astroport; -use crate::astroport::common::validate_addresses; +use crate::astroport::common::{ + claim_ownership, drop_ownership_proposal, propose_new_owner, validate_addresses, +}; use crate::error::ContractError; use crate::marketing_validation::{validate_marketing_info, validate_whitelist_links}; use crate::state::{Lock, BLACKLIST, CONFIG, LOCKED, OWNERSHIP_PROPOSAL, VOTING_POWER_HISTORY}; @@ -61,6 +60,8 @@ pub fn execute( create_lock(deps, env, info.sender, amount) } ExecuteMsg::DepositFor { user } => { + blacklist_check(deps.storage, &info.sender)?; + let addr = deps.api.addr_validate(&user)?; blacklist_check(deps.storage, &addr)?; @@ -361,14 +362,11 @@ fn withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> Result + Copy) { + pub fn mint_xastro(&mut self, to: &str, amount: u128) { + let amount = amount * MULTIPLIER; self.app .send_tokens( self.owner.clone(), Addr::unchecked(to), - &coins(amount.into(), XASTRO_DENOM), + &coins(amount, XASTRO_DENOM), ) .unwrap(); } - pub fn check_xastro_balance(&self, user: &str, amount: u64) { + pub fn check_xastro_balance(&self, user: &str, amount: u128) { let amount = amount * MULTIPLIER; let balance = self .app @@ -145,7 +144,7 @@ impl Helper { .query_balance(user, XASTRO_DENOM) .unwrap() .amount; - assert_eq!(balance.u128(), amount as u128); + assert_eq!(balance.u128(), amount); } pub fn create_lock(&mut self, user: &str, amount: f32) -> Result { @@ -158,48 +157,27 @@ impl Helper { ) } - pub fn create_lock_u128( - &self, - router: &mut App, - user: &str, - time: u64, - amount: u128, - ) -> Result { - let cw20msg = Cw20ExecuteMsg::Send { - contract: self.vxastro.to_string(), - amount: Uint128::from(amount), - msg: to_json_binary(&Cw20HookMsg::CreateLock { time }).unwrap(), - }; - router.execute_contract( + pub fn create_lock_u128(&mut self, user: &str, amount: u128) -> Result { + self.app.execute_contract( Addr::unchecked(user), - self.xastro_denom.clone(), - &cw20msg, - &[], + self.vxastro.clone(), + &ExecuteMsg::CreateLock {}, + &coins(amount, XASTRO_DENOM), ) } - pub fn extend_lock_amount( - &self, - router: &mut App, - user: &str, - amount: f32, - ) -> Result { - let amount = (amount * MULTIPLIER as f32) as u64; - let cw20msg = Cw20ExecuteMsg::Send { - contract: self.vxastro.to_string(), - amount: Uint128::from(amount), - msg: to_json_binary(&Cw20HookMsg::ExtendLockAmount {}).unwrap(), - }; - router.execute_contract( + pub fn extend_lock_amount(&mut self, user: &str, amount: f32) -> Result { + let amount = (amount * MULTIPLIER as f32) as u128; + self.app.execute_contract( Addr::unchecked(user), - self.xastro_denom.clone(), - &cw20msg, - &[], + self.vxastro.clone(), + &ExecuteMsg::ExtendLockAmount {}, + &coins(amount, XASTRO_DENOM), ) } - pub fn relock(&self, router: &mut App, user: &str) -> Result { - router.execute_contract( + pub fn relock(&mut self, user: &str) -> Result { + self.app.execute_contract( Addr::unchecked("outpost"), self.vxastro.clone(), &ExecuteMsg::Relock { @@ -209,32 +187,20 @@ impl Helper { ) } - pub fn deposit_for( - &self, - router: &mut App, - from: &str, - to: &str, - amount: f32, - ) -> Result { - let amount = (amount * MULTIPLIER as f32) as u64; - let cw20msg = Cw20ExecuteMsg::Send { - contract: self.vxastro.to_string(), - amount: Uint128::from(amount), - msg: to_json_binary(&Cw20HookMsg::DepositFor { - user: to.to_string(), - }) - .unwrap(), - }; - router.execute_contract( + pub fn deposit_for(&mut self, from: &str, to: &str, amount: f32) -> Result { + let amount = (amount * MULTIPLIER as f32) as u128; + self.app.execute_contract( Addr::unchecked(from), - self.xastro_denom.clone(), - &cw20msg, - &[], + self.vxastro.clone(), + &ExecuteMsg::DepositFor { + user: to.to_string(), + }, + &coins(amount, XASTRO_DENOM), ) } - pub fn unlock(&self, router: &mut App, user: &str) -> Result { - router.execute_contract( + pub fn unlock(&mut self, user: &str) -> Result { + self.app.execute_contract( Addr::unchecked(user), self.vxastro.clone(), &ExecuteMsg::Unlock {}, @@ -242,8 +208,8 @@ impl Helper { ) } - pub fn withdraw(&self, router: &mut App, user: &str) -> Result { - router.execute_contract( + pub fn withdraw(&mut self, user: &str) -> Result { + self.app.execute_contract( Addr::unchecked(user), self.vxastro.clone(), &ExecuteMsg::Withdraw {}, @@ -252,12 +218,11 @@ impl Helper { } pub fn update_blacklist( - &self, - router: &mut App, - append_addrs: Option>, - remove_addrs: Option>, + &mut self, + append_addrs: Vec, + remove_addrs: Vec, ) -> Result { - router.execute_contract( + self.app.execute_contract( Addr::unchecked("owner"), self.vxastro.clone(), &ExecuteMsg::UpdateBlacklist { @@ -268,12 +233,8 @@ impl Helper { ) } - pub fn update_outpost_address( - &self, - router: &mut App, - new_address: String, - ) -> Result { - router.execute_contract( + pub fn update_outpost_address(&mut self, new_address: String) -> Result { + self.app.execute_contract( Addr::unchecked("owner"), self.vxastro.clone(), &ExecuteMsg::UpdateConfig { @@ -285,8 +246,8 @@ impl Helper { ) } - pub fn query_user_vp(&self, router: &mut App, user: &str) -> StdResult { - router + pub fn query_user_vp(&self, user: &str) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -297,8 +258,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_user_emissions_vp(&self, router: &mut App, user: &str) -> StdResult { - router + pub fn query_user_emissions_vp(&self, user: &str) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -309,8 +270,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_exact_user_vp(&self, router: &mut App, user: &str) -> StdResult { - router + pub fn query_exact_user_vp(&self, user: &str) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -321,8 +282,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128()) } - pub fn query_exact_user_emissions_vp(&self, router: &mut App, user: &str) -> StdResult { - router + pub fn query_exact_user_emissions_vp(&self, user: &str) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -333,8 +294,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128()) } - pub fn query_user_vp_at(&self, router: &mut App, user: &str, time: u64) -> StdResult { - router + pub fn query_user_vp_at(&self, user: &str, time: u64) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -346,13 +307,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_user_emissions_vp_at( - &self, - router: &mut App, - user: &str, - time: u64, - ) -> StdResult { - router + pub fn query_user_emissions_vp_at(&self, user: &str, time: u64) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -364,13 +320,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_user_vp_at_period( - &self, - router: &mut App, - user: &str, - period: u64, - ) -> StdResult { - router + pub fn query_user_vp_at_period(&self, user: &str, period: u64) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -382,15 +333,15 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_total_vp(&self, router: &mut App) -> StdResult { - router + pub fn query_total_vp(&self) -> StdResult { + self.app .wrap() .query_wasm_smart(self.vxastro.clone(), &QueryMsg::TotalVotingPower {}) .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_total_emissions_vp(&self, router: &mut App) -> StdResult { - router + pub fn query_total_emissions_vp(&self) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -399,15 +350,15 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_exact_total_vp(&self, router: &mut App) -> StdResult { - router + pub fn query_exact_total_vp(&self) -> StdResult { + self.app .wrap() .query_wasm_smart(self.vxastro.clone(), &QueryMsg::TotalVotingPower {}) .map(|vp: VotingPowerResponse| vp.voting_power.u128()) } - pub fn query_exact_total_emissions_vp(&self, router: &mut App) -> StdResult { - router + pub fn query_exact_total_emissions_vp(&self) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -416,15 +367,15 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128()) } - pub fn query_total_vp_at(&self, router: &mut App, time: u64) -> StdResult { - router + pub fn query_total_vp_at(&self, time: u64) -> StdResult { + self.app .wrap() .query_wasm_smart(self.vxastro.clone(), &QueryMsg::TotalVotingPowerAt { time }) .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_total_emissions_vp_at(&self, router: &mut App, time: u64) -> StdResult { - router + pub fn query_total_emissions_vp_at(&self, time: u64) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -433,8 +384,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_total_vp_at_period(&self, router: &mut App, period: u64) -> StdResult { - router + pub fn query_total_vp_at_period(&self, period: u64) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -443,12 +394,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_total_emissions_vp_at_period( - &self, - router: &mut App, - timestamp: u64, - ) -> StdResult { - router + pub fn query_total_emissions_vp_at_period(&self, timestamp: u64) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -457,13 +404,8 @@ impl Helper { .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) } - pub fn query_locked_balance_at( - &self, - router: &mut App, - user: &str, - timestamp: Uint64, - ) -> StdResult { - router + pub fn query_locked_balance_at(&self, user: &str, timestamp: Uint64) -> StdResult { + self.app .wrap() .query_wasm_smart( self.vxastro.clone(), @@ -477,11 +419,10 @@ impl Helper { pub fn query_blacklisted_voters( &self, - router: &mut App, start_after: Option, limit: Option, ) -> StdResult> { - router.wrap().query_wasm_smart( + self.app.wrap().query_wasm_smart( self.vxastro.clone(), &QueryMsg::BlacklistedVoters { start_after, limit }, ) @@ -489,10 +430,9 @@ impl Helper { pub fn check_voters_are_blacklisted( &self, - router: &mut App, voters: Vec, ) -> StdResult { - router.wrap().query_wasm_smart( + self.app.wrap().query_wasm_smart( self.vxastro.clone(), &QueryMsg::CheckVotersAreBlacklisted { voters }, ) diff --git a/contracts/voting_escrow_lite/tests/integration.rs b/contracts/voting_escrow_lite/tests/vxastro_lite_integration.rs similarity index 50% rename from contracts/voting_escrow_lite/tests/integration.rs rename to contracts/voting_escrow_lite/tests/vxastro_lite_integration.rs index 65696991..3c1d0743 100644 --- a/contracts/voting_escrow_lite/tests/integration.rs +++ b/contracts/voting_escrow_lite/tests/vxastro_lite_integration.rs @@ -1,7 +1,6 @@ -use astroport::token as astro; -use cosmwasm_std::{attr, to_json_binary, Addr, StdError, Uint128, Uint64}; -use cw20::{Cw20ExecuteMsg, Logo, LogoInfo, MarketingInfoResponse, MinterResponse}; -use cw_multi_test::{next_block, ContractWrapper, Executor}; +use cosmwasm_std::{attr, Addr, StdError, Uint64}; +use cw20::{Logo, LogoInfo, MarketingInfoResponse}; +use cw_multi_test::{next_block, Executor}; use astroport_governance::utils::{get_lite_period, WEEK}; use astroport_governance::voting_escrow_lite::{Config, ExecuteMsg, LockInfoResponse, QueryMsg}; @@ -17,334 +16,268 @@ fn lock_unlock_logic() { helper.mint_xastro("owner", 100); // Mint ASTRO, stake it and mint xASTRO - helper.mint_xastro(router_ref, "user", 100); - helper.check_xastro_balance(router_ref, "user", 100); + helper.mint_xastro("user", 100); + helper.check_xastro_balance("user", 100); // Try to withdraw from a non-existent lock - let err = helper.withdraw(router_ref, "user").unwrap_err(); + let err = helper.withdraw("user").unwrap_err(); assert_eq!(err.root_cause().to_string(), "Lock does not exist"); // Try to deposit more xASTRO in a position that does not already exist // This should create a new lock - helper.extend_lock_amount(router_ref, "user", 1f32).unwrap(); - helper.check_xastro_balance(router_ref, "user", 99); - helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 1); + helper.extend_lock_amount("user", 1f32).unwrap(); + helper.check_xastro_balance("user", 99); + helper.check_xastro_balance(helper.vxastro.as_str(), 1); // Current total voting power is 0 - let vp = helper.query_total_vp(router_ref).unwrap(); + let vp = helper.query_total_vp().unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + let vp = helper.query_total_emissions_vp().unwrap(); assert_eq!(vp, 1.0); // Try to create another voting escrow lock - let err = helper - .create_lock(router_ref, "user", WEEK * 2, 90f32) - .unwrap_err(); + let err = helper.create_lock("user", 90f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "Lock already exists, either unlock and withdraw or extend_lock to add to the lock" ); // Check that 90 xASTRO were not debited - helper.check_xastro_balance(router_ref, "user", 99); - helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 1); + helper.check_xastro_balance("user", 99); + helper.check_xastro_balance(helper.vxastro.as_str(), 1); // Add more xASTRO to the existing position - helper.extend_lock_amount(router_ref, "user", 9f32).unwrap(); - helper.check_xastro_balance(router_ref, "user", 90); - helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 10); + helper.extend_lock_amount("user", 9f32).unwrap(); + helper.check_xastro_balance("user", 90); + helper.check_xastro_balance(helper.vxastro.as_str(), 10); // Try to withdraw from a non-unlocked lock - let err = helper.withdraw(router_ref, "user").unwrap_err(); + let err = helper.withdraw("user").unwrap_err(); assert_eq!( err.root_cause().to_string(), "The lock has not been unlocked, call unlock first" ); - helper.unlock(router_ref, "user").unwrap(); + helper.unlock("user").unwrap(); // Go in the future - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK)); + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK)); // The lock has not yet expired since unlocking has a 2 week waiting time - let err = helper.withdraw(router_ref, "user").unwrap_err(); + let err = helper.withdraw("user").unwrap_err(); assert_eq!( err.root_cause().to_string(), "The lock time has not yet expired" ); // Go to the future again - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK)); + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK)); // Try to add more xASTRO to an expired position - let err = helper - .extend_lock_amount(router_ref, "user", 1f32) - .unwrap_err(); + let err = helper.extend_lock_amount("user", 1f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "The lock expired. Withdraw and create new lock" ); // Imagine the user will withdraw their expired lock in 5 weeks - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(5 * WEEK)); + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(5 * WEEK)); // Time has passed so we can withdraw - helper.withdraw(router_ref, "user").unwrap(); - helper.check_xastro_balance(router_ref, "user", 100); - helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 0); + helper.withdraw("user").unwrap(); + helper.check_xastro_balance("user", 100); + helper.check_xastro_balance(helper.vxastro.as_str(), 0); // Create a new lock - helper - .extend_lock_amount(router_ref, "user", 50f32) - .unwrap(); + helper.extend_lock_amount("user", 50f32).unwrap(); - let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + let vp = helper.query_total_emissions_vp().unwrap(); assert_eq!(vp, 50.0); - let vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + let vp = helper.query_user_emissions_vp("user").unwrap(); assert_eq!(vp, 50.0); // Unlock the lock - helper.unlock(router_ref, "user").unwrap(); + helper.unlock("user").unwrap(); - let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + let vp = helper.query_total_emissions_vp().unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + let vp = helper.query_user_emissions_vp("user").unwrap(); assert_eq!(vp, 0.0); // Relock } -#[test] -fn random_token_lock() { - let owner = Addr::unchecked("owner"); - let helper = Helper::init(router_ref, owner); - - let random_token_contract = Box::new(ContractWrapper::new_with_empty( - astroport_token::contract::execute, - astroport_token::contract::instantiate, - astroport_token::contract::query, - )); - let random_token_code_id = router.store_code(random_token_contract); - - let msg = astro::InstantiateMsg { - name: String::from("Random token"), - symbol: String::from("FOO"), - decimals: 6, - initial_balances: vec![], - mint: Some(MinterResponse { - minter: helper.owner.to_string(), - cap: None, - }), - marketing: None, - }; - - let random_token = router - .instantiate_contract( - random_token_code_id, - helper.owner.clone(), - &msg, - &[], - String::from("FOO"), - None, - ) - .unwrap(); - - let msg = cw20::Cw20ExecuteMsg::Mint { - recipient: String::from("user"), - amount: Uint128::from(100_u128), - }; - - router - .execute_contract(helper.owner.clone(), random_token.clone(), &msg, &[]) - .unwrap(); - - let cw20msg = Cw20ExecuteMsg::Send { - contract: helper.voting_instance.to_string(), - amount: Uint128::from(10_u128), - msg: to_json_binary(&Cw20HookMsg::CreateLock { time: WEEK }).unwrap(), - }; - let err = router - .execute_contract(Addr::unchecked("user"), random_token, &cw20msg, &[]) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Unauthorized"); -} - #[test] fn new_lock_after_unlock() { - let helper = Helper::init(router_ref, Addr::unchecked("owner")); - helper.mint_xastro(router_ref, "owner", 100); + let mut helper = Helper::init(); + helper.mint_xastro("owner", 100); // Mint ASTRO, stake it and mint xASTRO - helper.mint_xastro(router_ref, "user", 100); + helper.mint_xastro("user", 100); - helper - .create_lock(router_ref, "user", WEEK * 2, 50f32) - .unwrap(); + helper.create_lock("user", 50f32).unwrap(); - let vp = helper.query_user_vp(router_ref, "user").unwrap(); + let vp = helper.query_user_vp("user").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_total_vp(router_ref).unwrap(); + let vp = helper.query_total_vp().unwrap(); assert_eq!(vp, 0.0); - let evp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + let evp = helper.query_user_emissions_vp("user").unwrap(); assert_eq!(evp, 50.0); - let evp = helper.query_total_emissions_vp(router_ref).unwrap(); + let evp = helper.query_total_emissions_vp().unwrap(); assert_eq!(evp, 50.0); // Go to the future - router_ref.update_block(next_block); + helper.app.update_block(next_block); - helper.unlock(router_ref, "user").unwrap(); - router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK * 2)); + helper.unlock("user").unwrap(); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK * 2)); - helper.withdraw(router_ref, "user").unwrap(); - helper.check_xastro_balance(router_ref, "user", 100); + helper.withdraw("user").unwrap(); + helper.check_xastro_balance("user", 100); - let vp = helper.query_user_vp(router_ref, "user").unwrap(); + let vp = helper.query_user_vp("user").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_total_vp(router_ref).unwrap(); + let vp = helper.query_total_vp().unwrap(); assert_eq!(vp, 0.0); // Create a new lock in 3 weeks from now - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK * 3)); - + helper.app.update_block(next_block); helper - .create_lock(router_ref, "user", WEEK * 5, 100f32) - .unwrap(); + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK * 3)); - let vp = helper.query_user_vp(router_ref, "user").unwrap(); + helper.create_lock("user", 100f32).unwrap(); + + let vp = helper.query_user_vp("user").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_total_vp(router_ref).unwrap(); + let vp = helper.query_total_vp().unwrap(); assert_eq!(vp, 0.0); - let evp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + let evp = helper.query_user_emissions_vp("user").unwrap(); assert_eq!(evp, 100.0); - let evp = helper.query_total_emissions_vp(router_ref).unwrap(); + let evp = helper.query_total_emissions_vp().unwrap(); assert_eq!(evp, 100.0); } /// Plot for this test case is generated at tests/plots/variable_decay.png #[test] fn emissions_voting_no_decay() { - let helper = Helper::init(router_ref, Addr::unchecked("owner")); - helper.mint_xastro(router_ref, "owner", 100); + let mut helper = Helper::init(); + helper.mint_xastro("owner", 100); // Mint ASTRO, stake it and mint xASTRO - helper.mint_xastro(router_ref, "user", 100); - helper.mint_xastro(router_ref, "user2", 100); + helper.mint_xastro("user", 100); + helper.mint_xastro("user2", 100); - helper - .create_lock(router_ref, "user", WEEK * 10, 30f32) - .unwrap(); + helper.create_lock("user", 30f32).unwrap(); // Go to the future - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK * 5)); + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK * 5)); // Create lock for user2 - helper - .create_lock(router_ref, "user2", WEEK * 6, 50f32) - .unwrap(); - let vp = helper.query_total_vp(router_ref).unwrap(); + helper.create_lock("user2", 50f32).unwrap(); + let vp = helper.query_total_vp().unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + let vp = helper.query_total_emissions_vp().unwrap(); assert_eq!(vp, 80.0); // Go to the future - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK * 4)); - + helper.app.update_block(next_block); helper - .extend_lock_amount(router_ref, "user", 70f32) - .unwrap(); + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK * 4)); - let vp = helper.query_user_vp(router_ref, "user").unwrap(); + helper.extend_lock_amount("user", 70f32).unwrap(); + + let vp = helper.query_user_vp("user").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_user_vp(router_ref, "user2").unwrap(); + let vp = helper.query_user_vp("user2").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_total_vp(router_ref).unwrap(); + let vp = helper.query_total_vp().unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + let vp = helper.query_user_emissions_vp("user").unwrap(); assert_eq!(vp, 100.0); - let vp = helper.query_user_emissions_vp(router_ref, "user2").unwrap(); + let vp = helper.query_user_emissions_vp("user2").unwrap(); assert_eq!(vp, 50.0); - let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + let vp = helper.query_total_emissions_vp().unwrap(); assert_eq!(vp, 150.0); let res = helper - .query_user_vp_at( - router_ref, - "user2", - router_ref.block_info().time.seconds() + 4 * WEEK, - ) + .query_user_vp_at("user2", helper.app.block_info().time.seconds() + 4 * WEEK) .unwrap(); assert_eq!(res, 0.0); let res = helper - .query_total_vp_at(router_ref, router_ref.block_info().time.seconds() + WEEK) + .query_total_vp_at(helper.app.block_info().time.seconds() + WEEK) .unwrap(); assert_eq!(res, 0.0); let res = helper - .query_user_emissions_vp_at( - router_ref, - "user2", - router_ref.block_info().time.seconds() + 4 * WEEK, - ) + .query_user_emissions_vp_at("user2", helper.app.block_info().time.seconds() + 4 * WEEK) .unwrap(); assert_eq!(res, 50.0); let res = helper - .query_total_emissions_vp_at(router_ref, router_ref.block_info().time.seconds() + WEEK) + .query_total_emissions_vp_at(helper.app.block_info().time.seconds() + WEEK) .unwrap(); assert_eq!(res, 150.0); // Go to the future - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(WEEK)); - let vp = helper.query_user_vp(router_ref, "user").unwrap(); + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK)); + let vp = helper.query_user_vp("user").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_user_vp(router_ref, "user2").unwrap(); + let vp = helper.query_user_vp("user2").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_total_vp(router_ref).unwrap(); + let vp = helper.query_total_vp().unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + let vp = helper.query_user_emissions_vp("user").unwrap(); assert_eq!(vp, 100.0); - let vp = helper.query_user_emissions_vp(router_ref, "user2").unwrap(); + let vp = helper.query_user_emissions_vp("user2").unwrap(); assert_eq!(vp, 50.0); - let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + let vp = helper.query_total_emissions_vp().unwrap(); assert_eq!(vp, 150.0); } #[test] fn check_queries() { - let owner = Addr::unchecked("owner"); - let helper = Helper::init(router_ref, owner); - helper.mint_xastro(router_ref, "owner", 100); + let mut helper = Helper::init(); + helper.mint_xastro("owner", 100); // Mint ASTRO, stake it and mint xASTRO - helper.mint_xastro(router_ref, "user", 100); - helper.check_xastro_balance(router_ref, "user", 100); + helper.mint_xastro("user", 100); + helper.check_xastro_balance("user", 100); // Create valid voting escrow lock - helper - .create_lock(router_ref, "user", WEEK * 2, 90f32) - .unwrap(); + helper.create_lock("user", 90f32).unwrap(); // Check that 90 xASTRO were actually debited - helper.check_xastro_balance(router_ref, "user", 10); - helper.check_xastro_balance(router_ref, helper.voting_instance.as_str(), 90); + helper.check_xastro_balance("user", 10); + helper.check_xastro_balance(helper.vxastro.as_str(), 90); // Validate user's lock - let user_lock: LockInfoResponse = router_ref + let user_lock: LockInfoResponse = helper + .app .wrap() .query_wasm_smart( - helper.voting_instance.clone(), + helper.vxastro.clone(), &QueryMsg::LockInfo { user: "user".to_string(), }, @@ -356,74 +289,63 @@ fn check_queries() { // Voting power must be 0 let total_vp_at_ts = helper - .query_total_vp_at(router_ref, router_ref.block_info().time.seconds()) + .query_total_vp_at(helper.app.block_info().time.seconds()) .unwrap(); assert_eq!(total_vp_at_ts, 0.0); // Must always be 0 - let period = get_lite_period(router_ref.block_info().time.seconds()).unwrap(); - let total_vp_at_period = helper.query_total_vp_at_period(router_ref, period).unwrap(); + let period = get_lite_period(helper.app.block_info().time.seconds()).unwrap(); + let total_vp_at_period = helper.query_total_vp_at_period(period).unwrap(); assert_eq!(total_vp_at_period, 0.0); // Must always be 0 let user_vp = helper - .query_user_vp_at(router_ref, "user", router_ref.block_info().time.seconds()) + .query_user_vp_at("user", helper.app.block_info().time.seconds()) .unwrap(); assert_eq!(user_vp, 0.0); // Must always be 0 - let user_vp = helper - .query_user_vp_at_period(router_ref, "user", period) - .unwrap(); + let user_vp = helper.query_user_vp_at_period("user", period).unwrap(); assert_eq!(user_vp, 0.0); // Emissions voting power must be 90 let total_emissions_vp_at_ts = helper - .query_total_emissions_vp_at(router_ref, router_ref.block_info().time.seconds()) + .query_total_emissions_vp_at(helper.app.block_info().time.seconds()) .unwrap(); assert_eq!(total_emissions_vp_at_ts, 90.0); - let user_emissions_vp = helper.query_user_emissions_vp(router_ref, "user").unwrap(); + let user_emissions_vp = helper.query_user_emissions_vp("user").unwrap(); assert_eq!(user_emissions_vp, 90.0); let user_emissions_vp = helper - .query_user_emissions_vp_at(router_ref, "user", router_ref.block_info().time.seconds()) + .query_user_emissions_vp_at("user", helper.app.block_info().time.seconds()) .unwrap(); assert_eq!(user_emissions_vp, 90.0); // Check users' locked xASTRO balance history - helper.mint_xastro(router_ref, "user", 90); + helper.mint_xastro("user", 90); // SnapshotMap checkpoints the data at the next block - let start_time = Uint64::from(router_ref.block_info().time.seconds() + 1); + let start_time = Uint64::from(helper.app.block_info().time.seconds() + 1); - let balance_timestamp = helper - .query_locked_balance_at(router_ref, "user", start_time) - .unwrap(); + let balance_timestamp = helper.query_locked_balance_at("user", start_time).unwrap(); assert_eq!(balance_timestamp, 90f32); - router_ref.update_block(next_block); - helper - .extend_lock_amount(router_ref, "user", 100f32) - .unwrap(); + helper.app.update_block(next_block); + helper.extend_lock_amount("user", 100f32).unwrap(); - let balance_timestamp = helper - .query_locked_balance_at(router_ref, "user", start_time) - .unwrap(); + let balance_timestamp = helper.query_locked_balance_at("user", start_time).unwrap(); assert_eq!(balance_timestamp, 90f32); - router_ref.update_block(|bi| { + helper.app.update_block(|bi| { bi.height += 100000; bi.time = bi.time.plus_seconds(500000); }); - let balance_timestamp = helper - .query_locked_balance_at(router_ref, "user", start_time) - .unwrap(); + let balance_timestamp = helper.query_locked_balance_at("user", start_time).unwrap(); assert_eq!(balance_timestamp, 90f32); let balance_timestamp = helper .query_locked_balance_at( - router_ref, "user", start_time.saturating_add(Uint64::from(10u64)), // Next block adds 5 seconds ) @@ -433,49 +355,46 @@ fn check_queries() { // The user still has 190 xASTRO locked let balance_timestamp = helper .query_locked_balance_at( - router_ref, "user", - Uint64::from(router_ref.block_info().time.seconds()), // Next block adds 5 seconds + Uint64::from(helper.app.block_info().time.seconds()), // Next block adds 5 seconds ) .unwrap(); assert_eq!(balance_timestamp, 190f32); - router_ref.update_block(|bi| { + helper.app.update_block(|bi| { bi.height += 1; bi.time = bi.time.plus_seconds(WEEK * 102); }); - helper.unlock(router_ref, "user").unwrap(); + helper.unlock("user").unwrap(); // Ensure emissions voting power is 0 after unlock let user_emissions_vp = helper - .query_user_emissions_vp_at(router_ref, "user", router_ref.block_info().time.seconds()) + .query_user_emissions_vp_at("user", helper.app.block_info().time.seconds()) .unwrap(); assert_eq!(user_emissions_vp, 0.0); // Forward until after unlock period ends - router_ref.update_block(|bi| { + helper.app.update_block(|bi| { bi.height += 1; bi.time = bi.time.plus_seconds(WEEK * 102); }); // Withdraw - helper.withdraw(router_ref, "user").unwrap(); + helper.withdraw("user").unwrap(); // Now the users' balance is zero // But one block before it had 190 xASTRO locked let balance_timestamp = helper .query_locked_balance_at( - router_ref, "user", - Uint64::from(router_ref.block_info().time.seconds() + 5), // Next block adds 5 seconds + Uint64::from(helper.app.block_info().time.seconds() + 5), // Next block adds 5 seconds ) .unwrap(); assert_eq!(balance_timestamp, 0f32); let balance_timestamp = helper .query_locked_balance_at( - router_ref, "user", - Uint64::from(router_ref.block_info().time.seconds() - 5), // Next block adds 5 seconds + Uint64::from(helper.app.block_info().time.seconds() - 5), // Next block adds 5 seconds ) .unwrap(); assert_eq!(balance_timestamp, 190f32); @@ -483,8 +402,7 @@ fn check_queries() { // add users to the blacklist helper .update_blacklist( - router_ref, - Some(vec![ + vec![ "voter1".to_string(), "voter2".to_string(), "voter3".to_string(), @@ -493,15 +411,13 @@ fn check_queries() { "voter6".to_string(), "voter7".to_string(), "voter8".to_string(), - ]), - None, + ], + vec![], ) .unwrap(); // query all blacklisted voters - let blacklisted_voters = helper - .query_blacklisted_voters(router_ref, None, None) - .unwrap(); + let blacklisted_voters = helper.query_blacklisted_voters(None, None).unwrap(); assert_eq!( blacklisted_voters, vec![ @@ -518,7 +434,7 @@ fn check_queries() { // query not blacklisted voter let err = helper - .query_blacklisted_voters(router_ref, Some("voter9".to_string()), Some(10u32)) + .query_blacklisted_voters(Some("voter9".to_string()), Some(10u32)) .unwrap_err(); assert_eq!( StdError::generic_err( @@ -529,7 +445,7 @@ fn check_queries() { // query voters by specified parameters let blacklisted_voters = helper - .query_blacklisted_voters(router_ref, Some("voter2".to_string()), Some(2u32)) + .query_blacklisted_voters(Some("voter2".to_string()), Some(2u32)) .unwrap(); assert_eq!( blacklisted_voters, @@ -538,16 +454,12 @@ fn check_queries() { // add users to the blacklist helper - .update_blacklist( - router_ref, - Some(vec!["voter0".to_string(), "voter33".to_string()]), - None, - ) + .update_blacklist(vec!["voter0".to_string(), "voter33".to_string()], vec![]) .unwrap(); // query voters by specified parameters let blacklisted_voters = helper - .query_blacklisted_voters(router_ref, Some("voter2".to_string()), Some(2u32)) + .query_blacklisted_voters(Some("voter2".to_string()), Some(2u32)) .unwrap(); assert_eq!( blacklisted_voters, @@ -555,7 +467,7 @@ fn check_queries() { ); let blacklisted_voters = helper - .query_blacklisted_voters(router_ref, Some("voter4".to_string()), Some(10u32)) + .query_blacklisted_voters(Some("voter4".to_string()), Some(10u32)) .unwrap(); assert_eq!( blacklisted_voters, @@ -569,59 +481,52 @@ fn check_queries() { let empty_blacklist: Vec = vec![]; let blacklisted_voters = helper - .query_blacklisted_voters(router_ref, Some("voter8".to_string()), Some(10u32)) + .query_blacklisted_voters(Some("voter8".to_string()), Some(10u32)) .unwrap(); assert_eq!(blacklisted_voters, empty_blacklist); // check if voters are blacklisted let res = helper - .check_voters_are_blacklisted(router_ref, vec!["voter1".to_string(), "voter9".to_string()]) + .check_voters_are_blacklisted(vec!["voter1".to_string(), "voter9".to_string()]) .unwrap(); assert_eq!("Voter is not blacklisted: voter9", res.to_string()); let res = helper - .check_voters_are_blacklisted(router_ref, vec!["voter1".to_string(), "voter8".to_string()]) + .check_voters_are_blacklisted(vec!["voter1".to_string(), "voter8".to_string()]) .unwrap(); assert_eq!("Voters are blacklisted!", res.to_string()); } #[test] fn check_deposit_for() { - let owner = Addr::unchecked("owner"); - let helper = Helper::init(router_ref, owner); - helper.mint_xastro(router_ref, "owner", 100); + let mut helper = Helper::init(); + helper.mint_xastro("owner", 100); // Mint ASTRO, stake it and mint xASTRO - helper.mint_xastro(router_ref, "user1", 100); - helper.check_xastro_balance(router_ref, "user1", 100); - helper.mint_xastro(router_ref, "user2", 100); - helper.check_xastro_balance(router_ref, "user2", 100); + helper.mint_xastro("user1", 100); + helper.check_xastro_balance("user1", 100); + helper.mint_xastro("user2", 100); + helper.check_xastro_balance("user2", 100); // 104 weeks ~ 2 years - helper - .create_lock(router_ref, "user1", 104 * WEEK, 50f32) - .unwrap(); - let vp = helper.query_user_vp(router_ref, "user1").unwrap(); + helper.create_lock("user1", 50f32).unwrap(); + let vp = helper.query_user_vp("user1").unwrap(); assert_eq!(0.0, vp); - let vp = helper.query_user_emissions_vp(router_ref, "user1").unwrap(); + let vp = helper.query_user_emissions_vp("user1").unwrap(); assert_eq!(50.0, vp); - helper - .deposit_for(router_ref, "user2", "user1", 50f32) - .unwrap(); - let vp = helper.query_user_vp(router_ref, "user1").unwrap(); + helper.deposit_for("user2", "user1", 50f32).unwrap(); + let vp = helper.query_user_vp("user1").unwrap(); assert_eq!(0.0, vp); - let vp = helper.query_user_emissions_vp(router_ref, "user1").unwrap(); + let vp = helper.query_user_emissions_vp("user1").unwrap(); assert_eq!(100.0, vp); - helper.check_xastro_balance(router_ref, "user1", 50); - helper.check_xastro_balance(router_ref, "user2", 50); + helper.check_xastro_balance("user1", 50); + helper.check_xastro_balance("user2", 50); } #[test] fn check_update_owner() { - let mut app = mock_app(); - let owner = Addr::unchecked("owner"); - let helper = Helper::init(&mut app, owner); + let mut helper = Helper::init(); let new_owner = String::from("new_owner"); @@ -632,10 +537,11 @@ fn check_update_owner() { }; // Unauthed check - let err = app + let err = helper + .app .execute_contract( Addr::unchecked("not_owner"), - helper.voting_instance.clone(), + helper.vxastro.clone(), &msg, &[], ) @@ -643,10 +549,11 @@ fn check_update_owner() { assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); // Claim before proposal - let err = app + let err = helper + .app .execute_contract( Addr::unchecked(new_owner.clone()), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::ClaimOwnership {}, &[], ) @@ -657,19 +564,17 @@ fn check_update_owner() { ); // Propose new owner - app.execute_contract( - Addr::unchecked("owner"), - helper.voting_instance.clone(), - &msg, - &[], - ) - .unwrap(); + helper + .app + .execute_contract(Addr::unchecked("owner"), helper.vxastro.clone(), &msg, &[]) + .unwrap(); // Claim from invalid addr - let err = app + let err = helper + .app .execute_contract( Addr::unchecked("invalid_addr"), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::ClaimOwnership {}, &[], ) @@ -677,19 +582,22 @@ fn check_update_owner() { assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); // Claim ownership - app.execute_contract( - Addr::unchecked(new_owner.clone()), - helper.voting_instance.clone(), - &ExecuteMsg::ClaimOwnership {}, - &[], - ) - .unwrap(); + helper + .app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.vxastro.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap(); // Let's query the contract state let msg = QueryMsg::Config {}; - let res: Config = app + let res: Config = helper + .app .wrap() - .query_wasm_smart(&helper.voting_instance, &msg) + .query_wasm_smart(&helper.vxastro, &msg) .unwrap(); assert_eq!(res.owner, new_owner) @@ -697,16 +605,15 @@ fn check_update_owner() { #[test] fn check_blacklist() { - let owner = Addr::unchecked("owner"); - let helper = Helper::init(router_ref, owner); + let mut helper = Helper::init(); // Mint ASTRO, stake it and mint xASTRO - helper.mint_xastro(router_ref, "user1", 100); - helper.mint_xastro(router_ref, "user2", 100); - helper.mint_xastro(router_ref, "user3", 100); + helper.mint_xastro("user1", 100); + helper.mint_xastro("user2", 100); + helper.mint_xastro("user3", 100); // Try to execute with empty arrays - let err = helper.update_blacklist(router_ref, None, None).unwrap_err(); + let err = helper.update_blacklist(vec![], vec![]).unwrap_err(); assert_eq!( err.root_cause().to_string(), "Generic error: Append and remove arrays are empty" @@ -714,7 +621,7 @@ fn check_blacklist() { // Blacklisting user2 let res = helper - .update_blacklist(router_ref, Some(vec!["user2".to_string()]), None) + .update_blacklist(vec!["user2".to_string()], vec![]) .unwrap(); assert_eq!( res.events[1].attributes[1], @@ -725,41 +632,35 @@ fn check_blacklist() { attr("added_addresses", "user2") ); - helper - .create_lock(router_ref, "user1", WEEK * 10, 50f32) - .unwrap(); + helper.create_lock("user1", 50f32).unwrap(); // Try to create lock from a blacklisted address - let err = helper - .create_lock(router_ref, "user2", WEEK * 10, 100f32) - .unwrap_err(); + let err = helper.create_lock("user2", 100f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "The user2 address is blacklisted" ); - let err = helper - .deposit_for(router_ref, "user2", "user3", 50f32) - .unwrap_err(); + let err = helper.deposit_for("user2", "user3", 50f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "The user2 address is blacklisted" ); // Since user2 is blacklisted, their xASTRO balance was left unchanged - helper.check_xastro_balance(router_ref, "user2", 100); + helper.check_xastro_balance("user2", 100); // And they did not create a lock, thus we have no information to query - let vp = helper.query_user_vp(router_ref, "user2").unwrap(); + let vp = helper.query_user_vp("user2").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_user_emissions_vp(router_ref, "user2").unwrap(); + let vp = helper.query_user_emissions_vp("user2").unwrap(); assert_eq!(vp, 0.0); // Go to the future - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(2 * WEEK)); + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(2 * WEEK)); // user2 is still blacklisted - let err = helper - .create_lock(router_ref, "user2", WEEK * 10, 100f32) - .unwrap_err(); + let err = helper.create_lock("user2", 100f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "The user2 address is blacklisted" @@ -767,13 +668,14 @@ fn check_blacklist() { // Blacklisting user1 using the guardian let msg = ExecuteMsg::UpdateBlacklist { - append_addrs: Some(vec!["user1".to_string()]), - remove_addrs: None, + append_addrs: vec!["user1".to_string()], + remove_addrs: vec![], }; - let res = router_ref + let res = helper + .app .execute_contract( Addr::unchecked("guardian"), - helper.voting_instance.clone(), + helper.vxastro.clone(), &msg, &[], ) @@ -787,61 +689,53 @@ fn check_blacklist() { attr("added_addresses", "user1") ); - let err = helper - .extend_lock_amount(router_ref, "user1", 10f32) - .unwrap_err(); + let err = helper.extend_lock_amount("user1", 10f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "The user1 address is blacklisted" ); - let err = helper - .deposit_for(router_ref, "user2", "user1", 50f32) - .unwrap_err(); + let err = helper.deposit_for("user2", "user1", 50f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "The user2 address is blacklisted" ); - let err = helper - .deposit_for(router_ref, "user3", "user1", 50f32) - .unwrap_err(); + let err = helper.deposit_for("user3", "user1", 50f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "The user1 address is blacklisted" ); // user1 doesn't have voting power now - let vp = helper.query_user_vp(router_ref, "user1").unwrap(); + let vp = helper.query_user_vp("user1").unwrap(); assert_eq!(vp, 0.0); - let vp = helper.query_user_emissions_vp(router_ref, "user1").unwrap(); + let vp = helper.query_user_emissions_vp("user1").unwrap(); assert_eq!(vp, 0.0); // Voting let vp = helper - .query_user_vp_at( - router_ref, - "user1", - router_ref.block_info().time.seconds() - WEEK, - ) + .query_user_vp_at("user1", helper.app.block_info().time.seconds() - WEEK) .unwrap(); assert_eq!(vp, 0f32); // Total voting power should be zero as well since there was only one vxASTRO position created by user1 - let vp = helper.query_total_vp(router_ref).unwrap(); + let vp = helper.query_total_vp().unwrap(); assert_eq!(vp, 0.0); // Total emissions voting power should be zero as well since there was only one vxASTRO position created by user1 - let vp = helper.query_total_emissions_vp(router_ref).unwrap(); + let vp = helper.query_total_emissions_vp().unwrap(); assert_eq!(vp, 0.0); // The only option available for a blacklisted user is to unlock and withdraw their funds - helper.unlock(router_ref, "user1").unwrap(); + helper.unlock("user1").unwrap(); // Go to the future - router_ref.update_block(next_block); - router_ref.update_block(|block| block.time = block.time.plus_seconds(20 * WEEK)); + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(20 * WEEK)); // The only option available for a blacklisted user is to withdraw their funds - helper.withdraw(router_ref, "user1").unwrap(); + helper.withdraw("user1").unwrap(); // Remove user1 from the blacklist let res = helper - .update_blacklist(router_ref, None, Some(vec!["user1".to_string()])) + .update_blacklist(vec![], vec!["user1".to_string()]) .unwrap(); assert_eq!( res.events[1].attributes[1], @@ -853,75 +747,62 @@ fn check_blacklist() { ); // Now user1 can create a new lock - helper - .create_lock(router_ref, "user1", WEEK, 10f32) - .unwrap(); + helper.create_lock("user1", 10f32).unwrap(); } #[test] fn check_residual() { - let owner = Addr::unchecked("owner"); - let helper = Helper::init(router_ref, owner); - let lock_duration = 104; + let mut helper = Helper::init(); let users_num = 1000; let lock_amount = 100_000_000; - helper.mint_xastro(router_ref, "owner", 100); + helper.mint_xastro("owner", 100); for i in 1..(users_num / 2) { let user = &format!("user{}", i); - helper.mint_xastro(router_ref, user, 100); - helper - .create_lock_u128(router_ref, user, WEEK * lock_duration, lock_amount) - .unwrap(); + helper.mint_xastro(user, 100); + helper.create_lock_u128(user, lock_amount).unwrap(); } let mut sum = 0; for i in 1..=users_num { let user = &format!("user{}", i); - sum += helper.query_exact_user_vp(router_ref, user).unwrap(); + sum += helper.query_exact_user_vp(user).unwrap(); } - assert_eq!(sum, helper.query_exact_total_vp(router_ref).unwrap()); + assert_eq!(sum, helper.query_exact_total_vp().unwrap()); let mut sum = 0; for i in 1..=users_num { let user = &format!("user{}", i); - sum += helper - .query_exact_user_emissions_vp(router_ref, user) - .unwrap(); + sum += helper.query_exact_user_emissions_vp(user).unwrap(); } - assert_eq!( - sum, - helper.query_exact_total_emissions_vp(router_ref).unwrap() - ); + assert_eq!(sum, helper.query_exact_total_emissions_vp().unwrap()); - router_ref.update_block(|bi| { + helper.app.update_block(|bi| { bi.height += 1; bi.time = bi.time.plus_seconds(WEEK); }); for i in (users_num / 2)..users_num { let user = &format!("user{}", i); - helper.mint_xastro(router_ref, user, 1000000); - helper - .create_lock_u128(router_ref, user, WEEK * lock_duration, lock_amount) - .unwrap(); + helper.mint_xastro(user, 1000000); + helper.create_lock_u128(user, lock_amount).unwrap(); } for _ in 1..104 { sum = 0; for i in 1..=users_num { let user = &format!("user{}", i); - sum += helper.query_exact_user_vp(router_ref, user).unwrap(); + sum += helper.query_exact_user_vp(user).unwrap(); } - let ve_vp = helper.query_exact_total_vp(router_ref).unwrap(); + let ve_vp = helper.query_exact_total_vp().unwrap(); let diff = (sum as f64 - ve_vp as f64).abs(); assert_eq!(diff, 0.0, "diff: {}, sum: {}, ve_vp: {}", diff, sum, ve_vp); - router_ref.update_block(|bi| { + helper.app.update_block(|bi| { bi.height += 1; bi.time = bi.time.plus_seconds(WEEK); }); @@ -931,16 +812,14 @@ fn check_residual() { sum = 0; for i in 1..=users_num { let user = &format!("user{}", i); - sum += helper - .query_exact_user_emissions_vp(router_ref, user) - .unwrap(); + sum += helper.query_exact_user_emissions_vp(user).unwrap(); } - let ve_vp = helper.query_exact_total_emissions_vp(router_ref).unwrap(); + let ve_vp = helper.query_exact_total_emissions_vp().unwrap(); let diff = (sum as f64 - ve_vp as f64).abs(); assert_eq!(diff, 0.0, "diff: {}, sum: {}, ve_vp: {}", diff, sum, ve_vp); - router_ref.update_block(|bi| { + helper.app.update_block(|bi| { bi.height += 1; bi.time = bi.time.plus_seconds(WEEK); }); @@ -949,61 +828,58 @@ fn check_residual() { #[test] fn total_vp_multiple_slope_subtraction() { - let owner = Addr::unchecked("owner"); - let helper = Helper::init(router_ref, owner); + let mut helper = Helper::init(); - helper.mint_xastro(router_ref, "user1", 1000); - helper - .create_lock(router_ref, "user1", 2 * WEEK, 100f32) - .unwrap(); - let total = helper.query_total_vp(router_ref).unwrap(); + helper.mint_xastro("user1", 1000); + helper.create_lock("user1", 100f32).unwrap(); + let total = helper.query_total_vp().unwrap(); assert_eq!(total, 0.0); - let total = helper.query_total_emissions_vp(router_ref).unwrap(); + let total = helper.query_total_emissions_vp().unwrap(); assert_eq!(total, 100.0); - router_ref.update_block(|bi| bi.time = bi.time.plus_seconds(2 * WEEK)); + helper + .app + .update_block(|bi| bi.time = bi.time.plus_seconds(2 * WEEK)); // Slope changes have been applied - let total = helper.query_total_vp(router_ref).unwrap(); + let total = helper.query_total_vp().unwrap(); assert_eq!(total, 0.0); - let total = helper.query_total_emissions_vp(router_ref).unwrap(); + let total = helper.query_total_emissions_vp().unwrap(); assert_eq!(total, 100.0); - helper.unlock(router_ref, "user1").unwrap(); + helper.unlock("user1").unwrap(); // Try to manipulate over expired lock 3 weeks later - router_ref.update_block(|bi| bi.time = bi.time.plus_seconds(3 * WEEK)); + helper + .app + .update_block(|bi| bi.time = bi.time.plus_seconds(3 * WEEK)); - let err = helper - .extend_lock_amount(router_ref, "user1", 100f32) - .unwrap_err(); + let err = helper.extend_lock_amount("user1", 100f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "The lock expired. Withdraw and create new lock" ); - let err = helper - .create_lock(router_ref, "user1", 2 * WEEK, 100f32) - .unwrap_err(); + let err = helper.create_lock("user1", 100f32).unwrap_err(); assert_eq!( err.root_cause().to_string(), "Lock already exists, either unlock and withdraw or extend_lock to add to the lock" ); - let total = helper.query_total_vp(router_ref).unwrap(); + let total = helper.query_total_vp().unwrap(); assert_eq!(total, 0f32); - let total = helper.query_total_emissions_vp(router_ref).unwrap(); + let total = helper.query_total_emissions_vp().unwrap(); assert_eq!(total, 0f32); } #[test] fn marketing_info() { - let owner = Addr::unchecked("owner"); - let helper = Helper::init(router_ref, owner); + let mut helper = Helper::init(); - let err = router_ref + let err = helper + .app .execute_contract( helper.owner.clone(), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::SetLogoUrlsWhitelist { whitelist: vec![ "@hello-test-url .com/".to_string(), @@ -1018,10 +894,11 @@ fn marketing_info() { "Generic error: Link contains invalid characters: @hello-test-url .com/" ); - let err = router_ref + let err = helper + .app .execute_contract( helper.owner.clone(), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::SetLogoUrlsWhitelist { whitelist: vec!["example.com".to_string()], }, @@ -1033,10 +910,11 @@ fn marketing_info() { "Marketing info validation error: Whitelist link should end with '/': example.com" ); - router_ref + helper + .app .execute_contract( helper.owner.clone(), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::SetLogoUrlsWhitelist { whitelist: vec!["example.com/".to_string()], }, @@ -1044,10 +922,11 @@ fn marketing_info() { ) .unwrap(); - let err = router_ref + let err = helper + .app .execute_contract( helper.owner.clone(), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::UpdateMarketing { project: Some("".to_string()), description: None, @@ -1062,10 +941,11 @@ fn marketing_info() { "Marketing info validation error: project contains invalid characters: " ); - let err = router_ref + let err = helper + .app .execute_contract( helper.owner.clone(), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::UpdateMarketing { project: None, description: Some("".to_string()), @@ -1079,10 +959,11 @@ fn marketing_info() { "Marketing info validation error: description contains invalid characters: " ); - router_ref + helper + .app .execute_contract( helper.owner.clone(), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::UpdateMarketing { project: Some("Some project".to_string()), description: Some("Some description".to_string()), @@ -1092,14 +973,16 @@ fn marketing_info() { ) .unwrap(); - let config: Config = router_ref + let config: Config = helper + .app .wrap() - .query_wasm_smart(&helper.voting_instance, &QueryMsg::Config {}) + .query_wasm_smart(&helper.vxastro, &QueryMsg::Config {}) .unwrap(); assert_eq!(config.logo_urls_whitelist, vec!["example.com/".to_string()]); - let marketing_info: MarketingInfoResponse = router_ref + let marketing_info: MarketingInfoResponse = helper + .app .wrap() - .query_wasm_smart(&helper.voting_instance, &QueryMsg::MarketingInfo {}) + .query_wasm_smart(&helper.vxastro, &QueryMsg::MarketingInfo {}) .unwrap(); assert_eq!(marketing_info.project, Some("Some project".to_string())); assert_eq!( @@ -1107,10 +990,11 @@ fn marketing_info() { Some("Some description".to_string()) ); - let err = router_ref + let err = helper + .app .execute_contract( helper.owner.clone(), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::UploadLogo(Logo::Url("https://some-website.com/logo.svg".to_string())), &[], ) @@ -1120,18 +1004,20 @@ fn marketing_info() { "Marketing info validation error: Logo link is not whitelisted: https://some-website.com/logo.svg", ); - router_ref + helper + .app .execute_contract( helper.owner.clone(), - helper.voting_instance.clone(), + helper.vxastro.clone(), &ExecuteMsg::UploadLogo(Logo::Url("example.com/logo.svg".to_string())), &[], ) .unwrap(); - let marketing_info: MarketingInfoResponse = router_ref + let marketing_info: MarketingInfoResponse = helper + .app .wrap() - .query_wasm_smart(&helper.voting_instance, &QueryMsg::MarketingInfo {}) + .query_wasm_smart(&helper.vxastro, &QueryMsg::MarketingInfo {}) .unwrap(); assert_eq!( marketing_info.logo.unwrap(), From d66addfd5d73a019210dbb1f8bc926c9375f2e31 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:24:50 +0400 Subject: [PATCH 18/47] bump hub's cw20 dependency --- Cargo.lock | 8 ++++---- contracts/hub/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 11224dec..4e0e6639 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,7 +201,7 @@ dependencies = [ "cw-multi-test 0.16.5", "cw-storage-plus 0.15.1", "cw2 1.1.2", - "cw20 0.15.1", + "cw20 1.1.2", "schemars", "serde", "serde-json-wasm", @@ -236,7 +236,7 @@ dependencies = [ "anyhow", "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "astroport-governance 1.4.0", - "base64 0.13.0", + "base64 0.13.1", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.16.5", @@ -425,9 +425,9 @@ checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" diff --git a/contracts/hub/Cargo.toml b/contracts/hub/Cargo.toml index 887be453..c010a372 100644 --- a/contracts/hub/Cargo.toml +++ b/contracts/hub/Cargo.toml @@ -25,7 +25,7 @@ library = [] [dependencies] cw2 = "1.0.1" -cw20 = "0.15" +cw20 = "1.1" cosmwasm-schema = "1.1.0" cosmwasm-std = { version = "1.1", features = ["iterator", "ibc3"] } cw-storage-plus = "0.15" From 80d742a014d84d3a348c878dc22a39a451a984fd Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:10:59 +0400 Subject: [PATCH 19/47] test proposal lifecycle --- contracts/assembly/src/contract.rs | 19 +- contracts/assembly/src/unit_tests.rs | 2 +- contracts/assembly/src/utils.rs | 9 +- contracts/assembly/tests/common/helper.rs | 180 +++++++++- contracts/assembly/tests/integration.rs | 388 ++++++++++++---------- 5 files changed, 392 insertions(+), 206 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index ddf616ef..f4ca28b5 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -155,6 +155,7 @@ pub fn execute( ), ExecuteMsg::CheckMessages(messages) => check_messages(env, messages), ExecuteMsg::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}), + // TODO: remove this redundant endpoint ExecuteMsg::RemoveCompletedProposal { proposal_id } => { remove_completed_proposal(deps, env, proposal_id) } @@ -274,11 +275,6 @@ pub fn cast_vote( return Err(ContractError::ProposalNotActive {}); } - // TODO: remove this restriction? - if proposal.submitter == info.sender { - return Err(ContractError::Unauthorized {}); - } - if env.block.height > proposal.end_block { return Err(ContractError::VotingPeriodEnded {}); } @@ -357,11 +353,6 @@ pub fn cast_outpost_vote( return Err(ContractError::ProposalNotActive {}); } - // TODO: Remove this restriction? - if proposal.submitter == voter { - return Err(ContractError::Unauthorized {}); - } - if env.block.height > proposal.end_block { return Err(ContractError::VotingPeriodEnded {}); } @@ -490,8 +481,6 @@ pub fn execute_proposal( attr("proposal_id", proposal_id.to_string()), ]); - PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - if let Some(channel) = &proposal.ibc_channel { if !proposal.messages.is_empty() { let config = CONFIG.load(deps.storage)?; @@ -504,7 +493,7 @@ pub fn execute_proposal( &ControllerExecuteMsg::IbcExecuteProposal { channel_id: channel.to_string(), proposal_id, - messages: proposal.messages, + messages: proposal.messages.clone(), }, vec![], )?)) @@ -515,9 +504,11 @@ pub fn execute_proposal( proposal.status = ProposalStatus::Executed; response .messages - .extend(proposal.messages.into_iter().map(SubMsg::new)) + .extend(proposal.messages.iter().cloned().map(SubMsg::new)) } + PROPOSALS.save(deps.storage, proposal_id, &proposal)?; + Ok(response) } diff --git a/contracts/assembly/src/unit_tests.rs b/contracts/assembly/src/unit_tests.rs index d1addbeb..87cd8871 100644 --- a/contracts/assembly/src/unit_tests.rs +++ b/contracts/assembly/src/unit_tests.rs @@ -37,7 +37,7 @@ fn check_proposal_validation( link: Option<&str>, expected_error: Option<&str>, ) { - // Linter is not able to properly parse test_case macro; keep this line + // Linter is not able to properly parse test_case macro; keep these lines let _ = coins(0, "keep_it"); let _ = coin(0, "keep_it"); diff --git a/contracts/assembly/src/utils.rs b/contracts/assembly/src/utils.rs index 7a968be3..3d1dc2a6 100644 --- a/contracts/assembly/src/utils.rs +++ b/contracts/assembly/src/utils.rs @@ -25,7 +25,8 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std &config.xastro_denom_tracking, &tokenfactory_tracker::QueryMsg::BalanceAt { address: sender.clone(), - timestamp: Some(proposal.start_time), + // Get voting power at the block before the proposal starts + timestamp: Some(proposal.start_time - 1), }, )?; @@ -47,6 +48,7 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std voting_escrow_delegator_addr, &AdjustedBalance { account: sender.clone(), + // TODO: why minus WEEK? timestamp: Some(proposal.start_time - WEEK), }, )? @@ -57,7 +59,7 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std &vxastro_token_addr, &VotingEscrowQueryMsg::UserVotingPowerAt { user: sender.clone(), - // TODO: remove - WEEK + // TODO: why minus WEEK? time: proposal.start_time - WEEK, }, )?; @@ -89,7 +91,8 @@ pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult< let mut total: Uint128 = deps.querier.query_wasm_smart( config.xastro_denom_tracking, &tokenfactory_tracker::QueryMsg::TotalSupplyAt { - timestamp: Some(proposal.start_time), + // Get voting power at the block before the proposal starts + timestamp: Some(proposal.start_time - 1), }, )?; diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index cabc23d8..b229392e 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -4,8 +4,8 @@ use anyhow::Result as AnyResult; use astroport::staking; use cosmwasm_std::testing::MockApi; use cosmwasm_std::{ - coins, Addr, Coin, Decimal, DepsMut, Empty, Env, GovMsg, IbcMsg, IbcQuery, MemoryStorage, - MessageInfo, Response, StdResult, Uint128, + coins, from_json, Addr, Coin, CosmosMsg, Decimal, DepsMut, Empty, Env, GovMsg, IbcMsg, + IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, }; use cw_multi_test::{ App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, @@ -13,10 +13,12 @@ use cw_multi_test::{ }; use astroport_governance::assembly::{ - InstantiateMsg, DELAY_INTERVAL, DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, + ExecuteMsg, InstantiateMsg, Proposal, ProposalVoteOption, ProposalVoterResponse, + ProposalVotesResponse, QueryMsg, DELAY_INTERVAL, DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE, MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, VOTING_PERIOD_INTERVAL, }; +use astroport_governance::builder_unlock::AllocationParams; use crate::common::stargate::StargateKeeper; @@ -69,6 +71,9 @@ fn vxastro_contract() -> Box> { } pub const PROPOSAL_REQUIRED_DEPOSIT: Uint128 = Uint128::new(*DEPOSIT_INTERVAL.start()); +pub const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); +pub const PROPOSAL_DELAY: u64 = *DELAY_INTERVAL.start(); +pub const PROPOSAL_EXPIRATION: u64 = *EXPIRATION_PERIOD_INTERVAL.start(); pub fn default_init_msg(staking: &Addr, builder_unlock: &Addr) -> InstantiateMsg { InstantiateMsg { @@ -79,9 +84,9 @@ pub fn default_init_msg(staking: &Addr, builder_unlock: &Addr) -> InstantiateMsg generator_controller_addr: None, hub_addr: None, builder_unlock_addr: builder_unlock.to_string(), - proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), - proposal_effective_delay: *DELAY_INTERVAL.start(), - proposal_expiration_period: *EXPIRATION_PERIOD_INTERVAL.start(), + proposal_voting_period: PROPOSAL_VOTING_PERIOD, + proposal_effective_delay: PROPOSAL_DELAY, + proposal_expiration_period: PROPOSAL_EXPIRATION, proposal_required_deposit: PROPOSAL_REQUIRED_DEPOSIT, proposal_required_quorum: MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE.to_string(), proposal_required_threshold: Decimal::from_atomics( @@ -252,21 +257,64 @@ impl Helper { ) } - pub fn mint_tokens(&mut self, recipient: &Addr, amount: impl Into + Copy) { + pub fn get_xastro(&mut self, recipient: &Addr, amount: impl Into + Copy) -> AppResponse { self.give_astro(amount.into(), recipient); - self.stake(recipient, amount.into()).unwrap(); + self.stake(recipient, amount.into()).unwrap() } - pub fn mint_vxastro(&mut self, recipient: &Addr, amount: impl Into + Copy) { - self.mint_tokens(recipient, amount); - // TODO: stake in voting escrow + pub fn get_vxastro(&mut self, recipient: &Addr, amount: impl Into + Copy) { + let resp = self.get_xastro(recipient, amount); + let xastro_amount = from_json::(&resp.data.unwrap().0) + .unwrap() + .xastro_amount; + + self.app + .execute_contract( + recipient.clone(), + self.vxastro.clone(), + &astroport_governance::voting_escrow_lite::ExecuteMsg::CreateLock {}, + &coins(xastro_amount.u128(), &self.xastro_denom), + ) + .unwrap(); } - pub fn query_balance(&self, sender: &Addr, denom: &str) -> StdResult { + pub fn submit_proposal(&mut self, submitter: &Addr, messages: Vec) { self.app - .wrap() - .query_balance(sender, denom) - .map(|c| c.amount) + .execute_contract( + submitter.clone(), + self.assembly.clone(), + &ExecuteMsg::SubmitProposal { + title: "Test title".to_string(), + description: "Test description".to_string(), + link: None, + messages, + ibc_channel: None, + }, + &coins(PROPOSAL_REQUIRED_DEPOSIT.u128(), &self.xastro_denom), + ) + .unwrap(); + } + + pub fn end_proposal(&mut self, proposal_id: u64) -> AnyResult { + self.app.execute_contract( + Addr::unchecked("permissionless"), + self.assembly.clone(), + &ExecuteMsg::EndProposal { proposal_id }, + &[], + ) + } + + pub fn execute_proposal(&mut self, proposal_id: u64) -> AnyResult { + self.app.execute_contract( + Addr::unchecked("permissionless"), + self.assembly.clone(), + &ExecuteMsg::ExecuteProposal { proposal_id }, + &[], + ) + } + + pub fn query_balance(&self, addr: impl Into, denom: &str) -> StdResult { + self.app.wrap().query_balance(addr, denom).map(|c| c.amount) } pub fn staking_xastro_balance_at( @@ -290,6 +338,77 @@ impl Helper { ) } + pub fn query_xastro_bal_at(&self, user: &Addr, timestamp: Option) -> Uint128 { + self.app + .wrap() + .query_wasm_smart( + &self.staking, + &staking::QueryMsg::BalanceAt { + address: user.to_string(), + timestamp, + }, + ) + .unwrap() + } + + pub fn user_vp(&self, address: &Addr, proposal_id: u64) -> Uint128 { + self.app + .wrap() + .query_wasm_smart( + &self.assembly, + &QueryMsg::UserVotingPower { + user: address.to_string(), + proposal_id, + }, + ) + .unwrap() + } + + pub fn proposal(&self, proposal_id: u64) -> Proposal { + self.app + .wrap() + .query_wasm_smart(&self.assembly, &QueryMsg::Proposal { proposal_id }) + .unwrap() + } + + pub fn proposal_votes(&self, proposal_id: u64) -> ProposalVotesResponse { + self.app + .wrap() + .query_wasm_smart(&self.assembly, &QueryMsg::ProposalVotes { proposal_id }) + .unwrap() + } + + pub fn proposal_voters(&self, proposal_id: u64) -> Vec { + self.app + .wrap() + .query_wasm_smart( + &self.assembly, + &QueryMsg::ProposalVoters { + proposal_id, + start_after: None, + limit: None, + }, + ) + .unwrap() + } + + pub fn cast_vote( + &mut self, + proposal_id: u64, + sender: &Addr, + option: ProposalVoteOption, + ) -> AnyResult { + self.app.execute_contract( + sender.clone(), + self.assembly.clone(), + &ExecuteMsg::CastVote { + proposal_id, + vote: option, + }, + &[], + ) + } + pub fn mint_coin(&mut self, to: &Addr, coin: Coin) { // .init_balance() erases previous balance thus I use such hack and create intermediate "denom admin" let denom_admin = Addr::unchecked(format!("{}_admin", &coin.denom)); @@ -306,10 +425,41 @@ impl Helper { .unwrap(); } + pub fn create_allocations(&mut self, allocations: Vec<(String, AllocationParams)>) { + let amount = allocations + .iter() + .map(|params| params.1.amount.u128()) + .sum(); + + self.app + .execute_contract( + Addr::unchecked("owner"), + self.builder_unlock.clone(), + &astroport_governance::builder_unlock::msg::ExecuteMsg::CreateAllocations { + allocations, + }, + &coins(amount, ASTRO_DENOM), + ) + .unwrap(); + } + + pub fn proposal_total_vp(&self, proposal_id: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart(&self.assembly, &QueryMsg::TotalVotingPower { proposal_id }) + } + pub fn next_block(&mut self, time: u64) { self.app.update_block(|block| { block.time = block.time.plus_seconds(time); block.height += 1 }); } + + pub fn next_block_height(&mut self, height: u64) { + self.app.update_block(|block| { + block.time = block.time.plus_seconds(5 * height); + block.height += height + }); + } } diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 7700901f..d9cb182a 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -1,51 +1,12 @@ -// use std::str::FromStr; -// -// use astroport::{ -// token::InstantiateMsg as TokenInstantiateMsg, xastro_token::QueryMsg as XAstroQueryMsg, -// }; -// use cosmwasm_std::coins; -// use cosmwasm_std::{ -// testing::{mock_env, MockApi, MockStorage}, -// to_json_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, -// Uint64, WasmMsg, WasmQuery, -// }; -// use cw_multi_test::{ -// next_block, App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor, -// }; -// -// use astroport_governance::assembly::{ -// Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalListResponse, ProposalStatus, -// ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, DEPOSIT_INTERVAL, -// VOTING_PERIOD_INTERVAL, -// }; -// use astroport_governance::builder_unlock::msg::{ -// InstantiateMsg as BuilderUnlockInstantiateMsg, ReceiveMsg as BuilderUnlockReceiveMsg, -// }; -// use astroport_governance::builder_unlock::{AllocationParams, Schedule}; -// use astroport_governance::hub::InstantiateMsg as HubInstantiateMsg; -// use astroport_governance::utils::{EPOCH_START, WEEK}; -// use astroport_governance::voting_escrow_lite::{ -// Cw20HookMsg as VXAstroCw20HookMsg, InstantiateMsg as VXAstroInstantiateMsg, -// }; -// -// mod common; -// -// const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); -// const PROPOSAL_EFFECTIVE_DELAY: u64 = 12_342; -// const PROPOSAL_EXPIRATION_PERIOD: u64 = 86_399; -// const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); -// const PROPOSAL_REQUIRED_QUORUM: &str = "0.50"; -// const PROPOSAL_REQUIRED_THRESHOLD: &str = "0.60"; -// - -use astroport_governance::assembly; -use cosmwasm_std::{coins, Addr, Uint128}; +use astro_assembly::error::ContractError; +use cosmwasm_std::{coin, coins, Addr, BankMsg, Uint128}; use cw_multi_test::Executor; -use astroport_governance::assembly::{Config, InstantiateMsg, ProposalListResponse, QueryMsg}; -use astroport_governance::builder_unlock::{AllocationParams, Schedule}; +use astroport_governance::assembly::{Config, InstantiateMsg, ProposalVoteOption, QueryMsg}; -use crate::common::helper::{default_init_msg, Helper, PROPOSAL_REQUIRED_DEPOSIT}; +use crate::common::helper::{ + default_init_msg, Helper, PROPOSAL_DELAY, PROPOSAL_REQUIRED_DEPOSIT, PROPOSAL_VOTING_PERIOD, +}; mod common; @@ -185,6 +146,179 @@ fn test_contract_instantiation() { ); } +#[test] +fn test_proposal_lifecycle() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + + let user = Addr::unchecked("user"); + helper.get_xastro(&user, 2 * PROPOSAL_REQUIRED_DEPOSIT.u128() + 1000); // initial stake consumes 1000 xASTRO + let late_voter = Addr::unchecked("late_voter"); + helper.get_xastro(&late_voter, 2 * PROPOSAL_REQUIRED_DEPOSIT.u128()); + + helper.next_block(10); + + // Proposal messages coins one simple transfer + let assembly = helper.assembly.clone(); + helper.mint_coin(&assembly, coin(1, "some_coin")); + helper.submit_proposal( + &user, + vec![BankMsg::Send { + to_address: "receiver".to_string(), + amount: coins(1, "some_coin"), + } + .into()], + ); + + // Check voting power + assert_eq!( + helper.user_vp(&user, 1).u128(), + 2 * PROPOSAL_REQUIRED_DEPOSIT.u128() + ); + assert_eq!( + helper.user_vp(&late_voter, 1).u128(), + 2 * PROPOSAL_REQUIRED_DEPOSIT.u128() + ); + assert_eq!( + helper.proposal_total_vp(1).unwrap().u128(), + 4 * PROPOSAL_REQUIRED_DEPOSIT.u128() + 1000 // 1000 locked forever in the staking contract + ); + + // Unstake after proposal submission + helper + .unstake(&user, PROPOSAL_REQUIRED_DEPOSIT.u128()) + .unwrap(); + // Current voting power is 0 + assert_eq!(helper.query_xastro_bal_at(&user, None), Uint128::zero()); + + // However voting power for the 1st proposal is still == 2 * PROPOSAL_REQUIRED_DEPOSIT + assert_eq!( + helper.user_vp(&user, 1).u128(), + 2 * PROPOSAL_REQUIRED_DEPOSIT.u128() + ); + + helper.cast_vote(1, &user, ProposalVoteOption::For).unwrap(); + + // One more voter got voting power in the middle of voting period. + // His voting power as well as total xASTRO supply increase are not accounted at the proposal start block. + let behind_voter = Addr::unchecked("behind_voter"); + helper.get_xastro(&behind_voter, 20 * PROPOSAL_REQUIRED_DEPOSIT.u128()); + let err = helper + .cast_vote(1, &behind_voter, ProposalVoteOption::For) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NoVotingPower {} + ); + + helper.next_block(10); + + // Try to vote again + let err = helper + .cast_vote(1, &user, ProposalVoteOption::For) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::UserAlreadyVoted {} + ); + + // Try to vote without voting power + let err = helper + .cast_vote(1, &Addr::unchecked("stranger"), ProposalVoteOption::Against) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NoVotingPower {} + ); + + // Try to end proposal + let err = helper.end_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::VotingPeriodNotEnded {} + ); + + // Try to execute proposal + let err = helper.execute_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotPassed {} + ); + + helper.next_block_height(PROPOSAL_VOTING_PERIOD); + + // Late voter tries to vote after voting period + let err = helper + .cast_vote(1, &late_voter, ProposalVoteOption::Against) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::VotingPeriodEnded {} + ); + + // Try to execute proposal before it is ended + let err = helper.execute_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotPassed {} + ); + + helper.end_proposal(1).unwrap(); + + // Try to end proposal again + let err = helper.end_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotActive {} + ); + + // Submitter received his deposit back + assert_eq!( + helper.query_balance(&user, &helper.xastro_denom).unwrap(), + PROPOSAL_REQUIRED_DEPOSIT + ); + + // Try to execute proposal before the delay is ended + let err = helper.execute_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalDelayNotEnded {} + ); + + // Late voter has no chance to vote + let err = helper + .cast_vote(1, &late_voter, ProposalVoteOption::Against) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotActive {} + ); + + helper.next_block_height(PROPOSAL_DELAY); + + // Finally execute proposal + helper.execute_proposal(1).unwrap(); + + // Try to execute proposal again + let err = helper.execute_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotPassed {} + ); + // Try to end proposal + let err = helper.end_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotActive {} + ); + + // Ensure proposal message was executed + assert_eq!( + helper.query_balance("receiver", "some_coin").unwrap(), + Uint128::one() + ); +} + // #[test] // fn test_successful_proposal() { // let owner = Addr::unchecked("owner"); @@ -255,58 +389,20 @@ fn test_contract_instantiation() { // } // // if vxastro > 0 { -// mint_vxastro( -// &mut app, -// &staking_instance, -// xastro_addr.clone(), -// &vxastro_addr, -// Addr::unchecked(addr), -// vxastro, -// ); +// helper.mint_vxastro(&Addr::unchecked(addr), vxastro); // } // } // -// create_allocations(&mut app, token_addr, builder_unlock_addr, locked_balances); +// helper.create_allocations(locked_balances); // // // Skip period -// app.update_block(|mut block| { +// helper.app.update_block(|mut block| { // block.time = block.time.plus_seconds(WEEK); // block.height += WEEK / 5; // }); // // // Create default proposal -// create_proposal( -// &mut app, -// &xastro_addr, -// &assembly_addr, -// Addr::unchecked("user0"), -// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: assembly_addr.to_string(), -// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { -// xastro_token_addr: None, -// vxastro_token_addr: None, -// voting_escrow_delegator_addr: None, -// ibc_controller: None, -// generator_controller: None, -// hub: None, -// builder_unlock_addr: None, -// proposal_voting_period: Some(PROPOSAL_VOTING_PERIOD + 1000), -// proposal_effective_delay: None, -// proposal_expiration_period: None, -// proposal_required_deposit: None, -// proposal_required_quorum: None, -// proposal_required_threshold: None, -// whitelist_add: Some(vec![ -// "https://some1.link/".to_string(), -// "https://some2.link/".to_string(), -// ]), -// whitelist_remove: Some(vec!["https://some.link/".to_string()]), -// guardian_addr: None, -// }))) -// .unwrap(), -// funds: vec![], -// })]), -// ); +// helper.create_proposal(&Addr::unchecked("user0"), vec![]); // // let votes: Vec<(&str, ProposalVoteOption, u128)> = vec![ // ("user1", ProposalVoteOption::For, 180u128), @@ -323,57 +419,33 @@ fn test_contract_instantiation() { // ("user12", ProposalVoteOption::For, 10000_000000u128), // ]; // -// check_total_vp(&mut app, &assembly_addr, 1, 20000002450); +// let prop_vp = helper.proposal_total_vp(1).unwrap(); +// assert_eq!(prop_vp, 20000002450u128.into()); // // for (addr, option, expected_vp) in votes { // let sender = Addr::unchecked(addr); // -// check_user_vp(&mut app, &assembly_addr, &sender, 1, expected_vp); +// let vp = helper.user_vp(&sender, 1); +// assert_eq!(vp, expected_vp.into()); // -// cast_vote(&mut app, assembly_addr.clone(), 1, sender, option).unwrap(); +// helper.cast_vote(1, sender, option).unwrap(); // } // -// let proposal: Proposal = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::Proposal { proposal_id: 1 }, -// ) -// .unwrap(); +// let proposal = helper.proposal(1); // -// let proposal_votes: ProposalVotesResponse = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::ProposalVotes { proposal_id: 1 }, -// ) -// .unwrap(); +// let proposal_votes = helper.proposal_votes(1); // -// let proposal_for_voters: Vec = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::ProposalVoters { -// proposal_id: 1, -// vote_option: ProposalVoteOption::For, -// start: None, -// limit: None, -// }, -// ) -// .unwrap(); +// let proposal_for_voters = helper +// .proposal_voters(1) +// .into_iter() +// .filter(|v| v.vote_option == ProposalVoteOption::For) +// .collect::>(); // -// let proposal_against_voters: Vec = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::ProposalVoters { -// proposal_id: 1, -// vote_option: ProposalVoteOption::Against, -// start: None, -// limit: None, -// }, -// ) -// .unwrap(); +// let proposal_against_voters = helper +// .proposal_voters(1) +// .into_iter() +// .filter(|v| v.vote_option == ProposalVoteOption::Against) +// .collect::>(); // // // Check proposal votes // assert_eq!(proposal.for_power, Uint128::from(10000001600u128)); @@ -406,34 +478,36 @@ fn test_contract_instantiation() { // ); // // // Skip voting period -// app.update_block(|bi| { +// helper.app.update_block(|bi| { // bi.height += PROPOSAL_VOTING_PERIOD + 1; // bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); // }); // // // Try to vote after voting period -// let err = cast_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// Addr::unchecked("user11"), -// ProposalVoteOption::Against, -// ) -// .unwrap_err(); +// let err = helper +// .cast_vote(1, Addr::unchecked("user11"), ProposalVoteOption::Against) +// .unwrap_err(); // -// assert_eq!(err.root_cause().to_string(), "Voting period ended!"); +// assert_eq!( +// err.downcast::().unwrap(), +// ContractError::VotingPeriodEnded {} +// ); // // // Try to execute the proposal before end_proposal -// let err = app +// let err = helper +// .app // .execute_contract( // Addr::unchecked("user0"), -// assembly_addr.clone(), +// helper.assembly.clone(), // &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, // &[], // ) // .unwrap_err(); // -// assert_eq!(err.root_cause().to_string(), "Proposal not passed!"); +// assert_eq!( +// err.downcast::().unwrap(), +// ContractError::ProposalNotPassed {} +// ); // // // Check the successful completion of the proposal // check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); @@ -1739,38 +1813,6 @@ fn test_contract_instantiation() { // app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); // } // -// fn create_allocations( -// app: &mut App, -// token: Addr, -// builder_unlock_contract_addr: Addr, -// allocations: Vec<(String, AllocationParams)>, -// ) { -// let amount = allocations -// .iter() -// .map(|params| params.1.amount.u128()) -// .sum(); -// -// mint_tokens( -// app, -// &Addr::unchecked("owner"), -// &token, -// &Addr::unchecked("owner"), -// amount, -// ); -// -// app.execute_contract( -// Addr::unchecked("owner"), -// Addr::unchecked(token.to_string()), -// &Cw20ExecuteMsg::Send { -// contract: builder_unlock_contract_addr.to_string(), -// amount: Uint128::from(amount), -// msg: to_json_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }) -// .unwrap(), -// }, -// &[], -// ) -// .unwrap(); -// } // // fn create_proposal( // app: &mut App, From dbddeba170a60bd171c786a615d4b0e70ce1b5b1 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:15:45 +0400 Subject: [PATCH 20/47] remove "remove_proposal" endpoint --- contracts/assembly/src/contract.rs | 31 ------------------- packages/astroport-governance/src/assembly.rs | 5 --- 2 files changed, 36 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index f4ca28b5..f089f381 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -155,10 +155,6 @@ pub fn execute( ), ExecuteMsg::CheckMessages(messages) => check_messages(env, messages), ExecuteMsg::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}), - // TODO: remove this redundant endpoint - ExecuteMsg::RemoveCompletedProposal { proposal_id } => { - remove_completed_proposal(deps, env, proposal_id) - } ExecuteMsg::UpdateConfig(config) => update_config(deps, env, info, config), ExecuteMsg::IBCProposalCompleted { proposal_id, @@ -607,33 +603,6 @@ pub fn check_messages(env: Env, mut messages: Vec) -> Result Result { - let config = CONFIG.load(deps.storage)?; - - let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?; - - if env.block.height - > (proposal.end_block + config.proposal_effective_delay + config.proposal_expiration_period) - { - proposal.status = ProposalStatus::Expired; - } - - if proposal.status != ProposalStatus::Expired && proposal.status != ProposalStatus::Rejected { - return Err(ContractError::ProposalNotCompleted {}); - } - - PROPOSALS.remove(deps.storage, proposal_id); - - Ok(Response::new() - .add_attribute("action", "remove_completed_proposal") - .add_attribute("proposal_id", proposal_id.to_string())) -} - /// Updates Assembly contract parameters. /// /// * **updated_config** new contract configuration. diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index c5b81a20..0f09ebd8 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -116,11 +116,6 @@ pub enum ExecuteMsg { /// If proposal should be executed on a remote chain this field should specify governance channel ibc_channel: Option, }, - /// Remove a proposal that was already executed (or failed/expired) - RemoveCompletedProposal { - /// Proposal identifier - proposal_id: u64, - }, /// Update parameters in the Assembly contract /// ## Executor /// Only the Assembly contract is allowed to update its own parameters From 7aaa986c423a60ee3450ae7ba92bf59ca522f21f Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:01:59 +0400 Subject: [PATCH 21/47] add more tests --- contracts/assembly/src/error.rs | 12 +- contracts/assembly/tests/integration.rs | 333 +++++++++++++++++++++++- 2 files changed, 334 insertions(+), 11 deletions(-) diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index 15751ec5..3b263ae8 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -1,14 +1,18 @@ -use astroport_governance::assembly::ProposalStatus; use cosmwasm_std::{OverflowError, StdError}; use cw_utils::PaymentError; use thiserror::Error; +use astroport_governance::assembly::ProposalStatus; + /// This enum describes Assembly contract errors #[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + OverflowError(#[from] OverflowError), + #[error("Unauthorized")] Unauthorized {}, @@ -87,9 +91,3 @@ pub enum ContractError { #[error("{0}")] PaymentError(#[from] PaymentError), } - -impl From for ContractError { - fn from(o: OverflowError) -> Self { - StdError::from(o).into() - } -} diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index d9cb182a..7a456f20 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -1,11 +1,16 @@ use astro_assembly::error::ContractError; -use cosmwasm_std::{coin, coins, Addr, BankMsg, Uint128}; +use cosmwasm_std::{coin, coins, Addr, BankMsg, Decimal, Uint128}; use cw_multi_test::Executor; +use std::str::FromStr; -use astroport_governance::assembly::{Config, InstantiateMsg, ProposalVoteOption, QueryMsg}; +use astroport_governance::assembly::{ + Config, ExecuteMsg, InstantiateMsg, ProposalVoteOption, QueryMsg, UpdateConfig, DELAY_INTERVAL, + DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, VOTING_PERIOD_INTERVAL, +}; use crate::common::helper::{ - default_init_msg, Helper, PROPOSAL_DELAY, PROPOSAL_REQUIRED_DEPOSIT, PROPOSAL_VOTING_PERIOD, + default_init_msg, Helper, PROPOSAL_DELAY, PROPOSAL_EXPIRATION, PROPOSAL_REQUIRED_DEPOSIT, + PROPOSAL_VOTING_PERIOD, }; mod common; @@ -120,6 +125,26 @@ fn test_contract_instantiation() { "Generic error: The effective delay for a proposal cannot be lower than 6171 or higher than 14400" ); + let err = helper + .app + .instantiate_contract( + assembly_code, + owner.clone(), + &InstantiateMsg { + whitelisted_links: vec![], + ..default_init_msg(&staking, &builder_unlock) + }, + &[], + "Assembly".to_string(), + Some(owner.to_string()), + ) + .unwrap_err(); + + assert_eq!( + err.downcast::().unwrap(), + ContractError::WhitelistEmpty {} + ); + let assembly_instance = helper .app .instantiate_contract( @@ -158,7 +183,7 @@ fn test_proposal_lifecycle() { helper.next_block(10); - // Proposal messages coins one simple transfer + // Proposal messages contain one simple transfer let assembly = helper.assembly.clone(); helper.mint_coin(&assembly, coin(1, "some_coin")); helper.submit_proposal( @@ -319,6 +344,306 @@ fn test_proposal_lifecycle() { ); } +#[test] +fn test_rejected_proposal() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + + let user = Addr::unchecked("user"); + helper.get_xastro(&user, PROPOSAL_REQUIRED_DEPOSIT.u128() + 1000); // initial stake consumes 1000 xASTRO + + helper.next_block(10); + + // Proposal messages contain one simple transfer + let assembly = helper.assembly.clone(); + helper.mint_coin(&assembly, coin(1, "some_coin")); + helper.submit_proposal( + &user, + vec![BankMsg::Send { + to_address: "receiver".to_string(), + amount: coins(1, "some_coin"), + } + .into()], + ); + + helper + .cast_vote(1, &user, ProposalVoteOption::Against) + .unwrap(); + + helper.next_block(10); + + // Try to vote again + let err = helper + .cast_vote(1, &user, ProposalVoteOption::For) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::UserAlreadyVoted {} + ); + + helper.next_block_height(PROPOSAL_VOTING_PERIOD); + + helper.end_proposal(1).unwrap(); + + // Try to end proposal again + let err = helper.end_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotActive {} + ); + + // Submitter received his deposit back + assert_eq!( + helper.query_balance(&user, &helper.xastro_denom).unwrap(), + PROPOSAL_REQUIRED_DEPOSIT + ); + + // Try to execute proposal. It should be rejected. + let err = helper.execute_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotPassed {} + ); + + helper.next_block_height(PROPOSAL_DELAY); + + // Try to execute proposal after delay (which doesn't make sense in reality) + let err = helper.execute_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotPassed {} + ); + + // Try to end proposal + let err = helper.end_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ProposalNotActive {} + ); + + // Ensure proposal message was not executed + assert_eq!( + helper.query_balance("receiver", "some_coin").unwrap(), + Uint128::zero() + ); +} + +#[test] +fn test_expired_proposal() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + + let user = Addr::unchecked("user"); + helper.get_xastro(&user, PROPOSAL_REQUIRED_DEPOSIT.u128() + 1000); // initial stake consumes 1000 xASTRO + + helper.next_block(10); + + // Proposal messages coins one simple transfer + let assembly = helper.assembly.clone(); + helper.mint_coin(&assembly, coin(1, "some_coin")); + helper.submit_proposal( + &user, + vec![BankMsg::Send { + to_address: "receiver".to_string(), + amount: coins(1, "some_coin"), + } + .into()], + ); + + helper.cast_vote(1, &user, ProposalVoteOption::For).unwrap(); + + helper.next_block_height(PROPOSAL_VOTING_PERIOD + PROPOSAL_DELAY + PROPOSAL_EXPIRATION + 1); + + helper.end_proposal(1).unwrap(); + + // Submitter received his deposit back + assert_eq!( + helper.query_balance(&user, &helper.xastro_denom).unwrap(), + PROPOSAL_REQUIRED_DEPOSIT + ); + + // Try to execute proposal. It should be rejected. + let err = helper.execute_proposal(1).unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::ExecuteProposalExpired {} + ); + + // Ensure proposal message was not executed + assert_eq!( + helper.query_balance("receiver", "some_coin").unwrap(), + Uint128::zero() + ); +} + +#[test] +fn test_check_messages() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + + // Prepare for check messages + let assembly = helper.assembly.clone(); + helper.mint_coin(&assembly, coin(1, "some_coin")); + + // Valid message + let err = helper + .app + .execute_contract( + Addr::unchecked("permissionless"), + assembly.clone(), + &ExecuteMsg::CheckMessages(vec![BankMsg::Send { + to_address: "receiver".to_string(), + amount: coins(1, "some_coin"), + } + .into()]), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::MessagesCheckPassed {} + ); + + // Invalid message + let err = helper + .app + .execute_contract( + Addr::unchecked("permissionless"), + assembly.clone(), + &ExecuteMsg::CheckMessages(vec![BankMsg::Send { + to_address: "receiver".to_string(), + amount: coins(1000, "uusdc"), + } + .into()]), + &[], + ) + .unwrap_err(); + // The error must be different + assert_ne!( + err.root_cause().to_string(), + ContractError::MessagesCheckPassed {}.to_string() + ); +} + +#[test] +fn test_update_config() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + let assembly = helper.assembly.clone(); + + let err = helper + .app + .execute_contract( + owner.clone(), + assembly.clone(), + &ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { + xastro_denom: None, + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: None, + proposal_voting_period: None, + proposal_effective_delay: None, + proposal_expiration_period: None, + proposal_required_deposit: None, + proposal_required_quorum: None, + proposal_required_threshold: None, + whitelist_remove: None, + whitelist_add: None, + guardian_addr: None, + })), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + let updated_config = UpdateConfig { + xastro_denom: Some("test".to_string()), + vxastro_token_addr: Some("vxastro_token".to_string()), + voting_escrow_delegator_addr: Some("voting_escrow_delegator".to_string()), + ibc_controller: Some("ibc_controller".to_string()), + generator_controller: Some("generator_controller".to_string()), + hub: Some("hub".to_string()), + builder_unlock_addr: Some("builder_unlock".to_string()), + proposal_voting_period: Some(*VOTING_PERIOD_INTERVAL.end()), + proposal_effective_delay: Some(*DELAY_INTERVAL.end()), + proposal_expiration_period: Some(*EXPIRATION_PERIOD_INTERVAL.end()), + proposal_required_deposit: Some(*DEPOSIT_INTERVAL.end()), + proposal_required_quorum: Some("0.5".to_string()), + proposal_required_threshold: Some("0.5".to_string()), + whitelist_remove: Some(vec!["https://some.link/".to_string()]), + whitelist_add: Some(vec!["https://another.link/".to_string()]), + guardian_addr: Some("guardian".to_string()), + }; + + helper + .app + .execute_contract( + assembly.clone(), // only assembly itself can update config + assembly.clone(), + &ExecuteMsg::UpdateConfig(Box::new(updated_config)), + &[], + ) + .unwrap(); + + let config: Config = helper + .app + .wrap() + .query_wasm_smart(assembly, &QueryMsg::Config {}) + .unwrap(); + + assert_eq!(config.xastro_denom, "test"); + assert_eq!( + config.vxastro_token_addr, + Some(Addr::unchecked("vxastro_token")) + ); + assert_eq!( + config.voting_escrow_delegator_addr, + Some(Addr::unchecked("voting_escrow_delegator")) + ); + assert_eq!( + config.ibc_controller, + Some(Addr::unchecked("ibc_controller")) + ); + assert_eq!( + config.generator_controller, + Some(Addr::unchecked("generator_controller")) + ); + assert_eq!(config.hub, Some(Addr::unchecked("hub"))); + assert_eq!( + config.builder_unlock_addr, + Addr::unchecked("builder_unlock") + ); + assert_eq!(config.proposal_voting_period, *VOTING_PERIOD_INTERVAL.end()); + assert_eq!(config.proposal_effective_delay, *DELAY_INTERVAL.end()); + assert_eq!( + config.proposal_expiration_period, + *EXPIRATION_PERIOD_INTERVAL.end() + ); + assert_eq!( + config.proposal_required_deposit, + Uint128::new(*DEPOSIT_INTERVAL.end()) + ); + assert_eq!( + config.proposal_required_quorum, + Decimal::from_str("0.5").unwrap() + ); + assert_eq!( + config.proposal_required_threshold, + Decimal::from_str("0.5").unwrap() + ); + assert_eq!( + config.whitelisted_links, + vec!["https://another.link/".to_string()] + ); + assert_eq!(config.guardian_addr, Some(Addr::unchecked("guardian"))); +} + // #[test] // fn test_successful_proposal() { // let owner = Addr::unchecked("owner"); From a7ff8810065868f44bceffd3630f04937dbd22ec Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:03:32 +0400 Subject: [PATCH 22/47] seal total voting power on proposal creation --- contracts/assembly/src/contract.rs | 17 ++++-- contracts/assembly/src/queries.rs | 4 +- contracts/assembly/src/unit_tests.rs | 55 +++++++++++++++++-- contracts/assembly/src/utils.rs | 41 ++++++++------ packages/astroport-governance/src/assembly.rs | 2 + 5 files changed, 91 insertions(+), 28 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index f089f381..1ca4fa43 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -166,7 +166,7 @@ pub fn execute( } } -/// Submit a brand new proposal and locks some xASTRO as an anti-spam mechanism. +/// Submit a brand new proposal and lock some xASTRO as an anti-spam mechanism. /// /// * **sender** proposal submitter. /// @@ -236,6 +236,13 @@ pub fn submit_proposal( messages, deposit_amount, ibc_channel, + // Seal total voting power. Query the total voting power one second before the proposal starts because + // this is the last up to date finalized state of token factory tracker contract. + total_voting_power: calc_total_voting_power_at( + deps.querier, + &config, + env.block.time.seconds() - 1, + )?, }; proposal.validate(config.whitelisted_links)?; @@ -420,10 +427,8 @@ pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result StdResult { } QueryMsg::TotalVotingPower { proposal_id } => { let proposal = PROPOSALS.load(deps.storage, proposal_id)?; - to_json_binary(&calc_total_voting_power_at(deps, &proposal)?) + to_json_binary(&proposal.total_voting_power) } QueryMsg::ProposalVoters { proposal_id, diff --git a/contracts/assembly/src/unit_tests.rs b/contracts/assembly/src/unit_tests.rs index 87cd8871..8d0f02bc 100644 --- a/contracts/assembly/src/unit_tests.rs +++ b/contracts/assembly/src/unit_tests.rs @@ -1,7 +1,12 @@ +use std::marker::PhantomData; use std::str::FromStr; -use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{from_json, Addr, Coin, Decimal, Uint64}; +use astroport::tokenfactory_tracker; +use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{coin, coins, to_json_binary, ContractResult, SystemResult, Uint128}; +use cosmwasm_std::{ + from_json, Addr, Coin, Decimal, Empty, OwnedDeps, QuerierResult, Uint64, WasmQuery, +}; use test_case::test_case; use astroport_governance::assembly::{ @@ -13,11 +18,52 @@ use astroport_governance::assembly::{ use crate::contract::submit_proposal; use crate::queries::query; use crate::state::{CONFIG, PROPOSAL_COUNT}; -use cosmwasm_std::{coin, coins}; const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); const XASTRO_DENOM: &str = "xastro"; +// Mocked wasm queries handler +fn custom_wasm_handler(request: &WasmQuery) -> QuerierResult { + match request { + WasmQuery::Smart { msg, .. } => { + if matches!( + from_json(msg), + Ok(tokenfactory_tracker::QueryMsg::TotalSupplyAt { .. }) + ) { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&Uint128::zero()).unwrap(), + )) + } else if matches!( + from_json(msg), + Ok(astroport_governance::builder_unlock::msg::QueryMsg::State {}) + ) { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&astroport_governance::builder_unlock::msg::StateResponse { + total_astro_deposited: Default::default(), + remaining_astro_tokens: Default::default(), + unallocated_astro_tokens: Default::default(), + }) + .unwrap(), + )) + } else { + unimplemented!() + } + } + _ => unimplemented!(), + } +} +fn mock_deps() -> OwnedDeps { + let mut querier = MockQuerier::new(&[]); + querier.update_wasm(custom_wasm_handler); + + OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier, + custom_query_type: PhantomData, + } +} + #[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", "description", None, None ; "valid proposal")] #[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "X", "description", None, Some("Generic error: Title too short!") ; "short title")] #[test_case(coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM), "title", "description", Some("X"), Some("Generic error: Link too short!") ; "short link")] @@ -41,7 +87,7 @@ fn check_proposal_validation( let _ = coins(0, "keep_it"); let _ = coin(0, "keep_it"); - let mut deps = mock_dependencies(); + let mut deps = mock_deps(); let env = mock_env(); // Mocked instantiation @@ -123,6 +169,7 @@ fn check_proposal_validation( messages: vec![], deposit_amount: funds[0].amount, ibc_channel: None, + total_voting_power: Default::default(), } ); } diff --git a/contracts/assembly/src/utils.rs b/contracts/assembly/src/utils.rs index 3d1dc2a6..c3013b70 100644 --- a/contracts/assembly/src/utils.rs +++ b/contracts/assembly/src/utils.rs @@ -1,5 +1,6 @@ use astroport::tokenfactory_tracker; -use cosmwasm_std::{Deps, StdResult, Uint128, Uint64}; +use astroport_governance::assembly::Config; +use cosmwasm_std::{Deps, QuerierWrapper, StdResult, Uint128, Uint64}; use astroport_governance::assembly::Proposal; use astroport_governance::builder_unlock::msg::{ @@ -82,35 +83,43 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std Ok(total) } -/// Calculates the total voting power at a specified block (that is relevant for a specific proposal). +/// Calculates the combined total voting power at a specified timestamp (that is relevant for a specific proposal). +/// Combined voting power includes: +/// * xASTRO total supply +/// * ASTRO tokens which still locked in the builder's unlock contract +/// * vxASTRO total supply /// -/// * **proposal** proposal for which we calculate the total voting power. -pub fn calc_total_voting_power_at(deps: Deps, proposal: &Proposal) -> StdResult { - let config = CONFIG.load(deps.storage)?; - - let mut total: Uint128 = deps.querier.query_wasm_smart( - config.xastro_denom_tracking, +/// ## Parameters +/// * **config** contract settings. +/// * **timestamp** timestamp for which we calculate the total voting power. +pub fn calc_total_voting_power_at( + querier: QuerierWrapper, + config: &Config, + timestamp: u64, +) -> StdResult { + let mut total: Uint128 = querier.query_wasm_smart( + &config.xastro_denom_tracking, &tokenfactory_tracker::QueryMsg::TotalSupplyAt { - // Get voting power at the block before the proposal starts - timestamp: Some(proposal.start_time - 1), + timestamp: Some(timestamp), }, )?; // Total amount of ASTRO locked in the initial builder's unlock schedule - let builder_state: StateResponse = deps - .querier - .query_wasm_smart(config.builder_unlock_addr, &BuilderUnlockQueryMsg::State {})?; + let builder_state: StateResponse = querier.query_wasm_smart( + &config.builder_unlock_addr, + &BuilderUnlockQueryMsg::State {}, + )?; total += builder_state.remaining_astro_tokens; // TODO: remove it since it is always 0? - if let Some(vxastro_token_addr) = config.vxastro_token_addr { + if let Some(vxastro_token_addr) = &config.vxastro_token_addr { // Total vxASTRO voting power // For vxASTRO lite, this will always be 0 - let vxastro: VotingPowerResponse = deps.querier.query_wasm_smart( + let vxastro: VotingPowerResponse = querier.query_wasm_smart( vxastro_token_addr, &VotingEscrowQueryMsg::TotalVotingPowerAt { - time: proposal.start_time - WEEK, + time: timestamp - WEEK, }, )?; diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index 0f09ebd8..29e51b57 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -359,6 +359,8 @@ pub struct Proposal { pub deposit_amount: Uint128, /// IBC channel pub ibc_channel: Option, + /// Total voting power 1 second before the proposal was created + pub total_voting_power: Uint128, } impl Proposal { From 6f53077e29ab06cd61b9dc7b13afa22b098874a2 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 30 Jan 2024 18:48:22 +0400 Subject: [PATCH 23/47] add test to check voting power --- contracts/assembly/src/utils.rs | 6 +- contracts/assembly/tests/common/helper.rs | 63 ++++++----- contracts/assembly/tests/integration.rs | 123 +++++++++++++++++++--- 3 files changed, 149 insertions(+), 43 deletions(-) diff --git a/contracts/assembly/src/utils.rs b/contracts/assembly/src/utils.rs index c3013b70..63f70270 100644 --- a/contracts/assembly/src/utils.rs +++ b/contracts/assembly/src/utils.rs @@ -22,7 +22,7 @@ use crate::state::CONFIG; pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> StdResult { let config = CONFIG.load(deps.storage)?; - let xastro_amount: Uint128 = deps.querier.query_wasm_smart( + let mut total: Uint128 = deps.querier.query_wasm_smart( &config.xastro_denom_tracking, &tokenfactory_tracker::QueryMsg::BalanceAt { address: sender.clone(), @@ -31,8 +31,6 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std }, )?; - let mut total = xastro_amount; - let locked_amount: AllocationResponse = deps.querier.query_wasm_smart( config.builder_unlock_addr, &BuilderUnlockQueryMsg::Allocation { @@ -45,7 +43,7 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std if let Some(vxastro_token_addr) = config.vxastro_token_addr { let vxastro_amount = if let Some(voting_escrow_delegator_addr) = config.voting_escrow_delegator_addr { - deps.querier.query_wasm_smart::( + deps.querier.query_wasm_smart( voting_escrow_delegator_addr, &AdjustedBalance { account: sender.clone(), diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index b229392e..f5b05c5e 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -4,8 +4,8 @@ use anyhow::Result as AnyResult; use astroport::staking; use cosmwasm_std::testing::MockApi; use cosmwasm_std::{ - coins, from_json, Addr, Coin, CosmosMsg, Decimal, DepsMut, Empty, Env, GovMsg, IbcMsg, - IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, + coin, coins, from_json, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Empty, Env, GovMsg, + IbcMsg, IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, }; use cw_multi_test::{ App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, @@ -18,7 +18,7 @@ use astroport_governance::assembly::{ MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE, MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, VOTING_PERIOD_INTERVAL, }; -use astroport_governance::builder_unlock::AllocationParams; +use astroport_governance::builder_unlock::{AllocationParams, Schedule}; use crate::common::stargate::StargateKeeper; @@ -262,6 +262,29 @@ impl Helper { self.stake(recipient, amount.into()).unwrap() } + pub fn create_builder_allocation(&mut self, recipient: &Addr, amount: u128) { + self.app + .execute_contract( + self.owner.clone(), + self.builder_unlock.clone(), + &astroport_governance::builder_unlock::msg::ExecuteMsg::CreateAllocations { + allocations: vec![( + recipient.to_string(), + AllocationParams { + amount: amount.into(), + unlock_schedule: Schedule { + duration: 10, + ..Default::default() + }, + proposed_receiver: None, + }, + )], + }, + &coins(amount.into(), ASTRO_DENOM), + ) + .unwrap(); + } + pub fn get_vxastro(&mut self, recipient: &Addr, amount: impl Into + Copy) { let resp = self.get_xastro(recipient, amount); let xastro_amount = from_json::(&resp.data.unwrap().0) @@ -295,6 +318,19 @@ impl Helper { .unwrap(); } + pub fn submit_sample_proposal(&mut self, submitter: &Addr) { + let assembly = self.assembly.clone(); + self.mint_coin(&assembly, coin(1, "some_coin")); + self.submit_proposal( + &submitter, + vec![BankMsg::Send { + to_address: "receiver".to_string(), + amount: coins(1, "some_coin"), + } + .into()], + ); + } + pub fn end_proposal(&mut self, proposal_id: u64) -> AnyResult { self.app.execute_contract( Addr::unchecked("permissionless"), @@ -317,27 +353,6 @@ impl Helper { self.app.wrap().query_balance(addr, denom).map(|c| c.amount) } - pub fn staking_xastro_balance_at( - &self, - sender: &Addr, - timestamp: Option, - ) -> StdResult { - self.app.wrap().query_wasm_smart( - &self.staking, - &staking::QueryMsg::BalanceAt { - address: sender.to_string(), - timestamp, - }, - ) - } - - pub fn query_xastro_supply_at(&self, timestamp: Option) -> StdResult { - self.app.wrap().query_wasm_smart( - &self.staking, - &staking::QueryMsg::TotalSupplyAt { timestamp }, - ) - } - pub fn query_xastro_bal_at(&self, user: &Addr, timestamp: Option) -> Uint128 { self.app .wrap() diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 7a456f20..1a0f9ade 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -1,11 +1,13 @@ -use astro_assembly::error::ContractError; +use std::collections::HashMap; +use std::str::FromStr; + use cosmwasm_std::{coin, coins, Addr, BankMsg, Decimal, Uint128}; use cw_multi_test::Executor; -use std::str::FromStr; +use astro_assembly::error::ContractError; use astroport_governance::assembly::{ - Config, ExecuteMsg, InstantiateMsg, ProposalVoteOption, QueryMsg, UpdateConfig, DELAY_INTERVAL, - DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, VOTING_PERIOD_INTERVAL, + Config, ExecuteMsg, InstantiateMsg, ProposalStatus, ProposalVoteOption, QueryMsg, UpdateConfig, + DELAY_INTERVAL, DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, VOTING_PERIOD_INTERVAL, }; use crate::common::helper::{ @@ -183,17 +185,7 @@ fn test_proposal_lifecycle() { helper.next_block(10); - // Proposal messages contain one simple transfer - let assembly = helper.assembly.clone(); - helper.mint_coin(&assembly, coin(1, "some_coin")); - helper.submit_proposal( - &user, - vec![BankMsg::Send { - to_address: "receiver".to_string(), - amount: coins(1, "some_coin"), - } - .into()], - ); + helper.submit_sample_proposal(&user); // Check voting power assert_eq!( @@ -644,6 +636,107 @@ fn test_update_config() { assert_eq!(config.guardian_addr, Some(Addr::unchecked("guardian"))); } +#[test] +fn test_voting_power() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + + helper.get_xastro(&owner, 1001u64); + + struct TestBalance { + xastro: u128, + builder_allocation: u128, + } + + let mut total_xastro = 0u128; + let mut total_builder_allocation = 0u128; + + let users_num = 100; + let balances: HashMap = (1..=users_num) + .into_iter() + .map(|i| { + let user = Addr::unchecked(format!("user{i}")); + let balances = TestBalance { + xastro: i * 1_000000, + builder_allocation: if i % 2 == 0 { i * 1_000000 } else { 0 }, + }; + helper.get_xastro(&user, balances.xastro); + if balances.builder_allocation > 0 { + helper.create_builder_allocation(&user, balances.builder_allocation); + } + + total_xastro += balances.xastro; + total_builder_allocation += balances.builder_allocation; + + (user, balances) + }) + .collect(); + + let submitter = balances.iter().last().unwrap().0; + helper.get_xastro(submitter, PROPOSAL_REQUIRED_DEPOSIT.u128()); + total_xastro += PROPOSAL_REQUIRED_DEPOSIT.u128(); + + helper.next_block(10); + + helper.submit_sample_proposal(submitter); + + let proposal = helper.proposal(1); + assert_eq!( + proposal.total_voting_power.u128(), + total_xastro + total_builder_allocation + 1001 + ); + + // First 40 users vote against the proposal + let mut against_power = 0u128; + balances.iter().take(40).for_each(|(addr, balances)| { + helper.next_block(100); + against_power += balances.xastro + balances.builder_allocation; + helper + .cast_vote(1, addr, ProposalVoteOption::Against) + .unwrap(); + }); + + let proposal = helper.proposal(1); + assert_eq!(proposal.against_power.u128(), against_power); + + // Next 40 vote for the proposal + let mut for_power = 0u128; + balances + .iter() + .skip(40) + .take(40) + .for_each(|(addr, balances)| { + helper.next_block(100); + for_power += balances.xastro + balances.builder_allocation; + helper.cast_vote(1, addr, ProposalVoteOption::For).unwrap(); + }); + + let proposal = helper.proposal(1); + assert_eq!(proposal.for_power.u128(), for_power); + + // Total voting power stays the same + let proposal = helper.proposal(1); + assert_eq!( + proposal.total_voting_power.u128(), + total_xastro + total_builder_allocation + 1001 + ); + + helper.next_block_height(PROPOSAL_VOTING_PERIOD); + + helper.end_proposal(1).unwrap(); + + let proposal = helper.proposal(1); + + assert_eq!( + proposal.total_voting_power.u128(), + total_xastro + total_builder_allocation + 1001 + ); + assert_eq!(proposal.submitter, submitter.clone()); + assert_eq!(proposal.status, ProposalStatus::Passed); + assert_eq!(proposal.for_power.u128(), for_power); + assert_eq!(proposal.against_power.u128(), against_power); +} + // #[test] // fn test_successful_proposal() { // let owner = Addr::unchecked("owner"); From b6cf181017e1a5acb77437f9428c5c13d4c59086 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 30 Jan 2024 20:03:15 +0400 Subject: [PATCH 24/47] increase overall coverage --- contracts/assembly/src/unit_tests.rs | 334 ++- contracts/assembly/tests/integration.rs | 1688 +---------- .../assembly/tests/integration.vxastro-full | 2517 ----------------- packages/astroport-governance/src/utils.rs | 29 +- 4 files changed, 392 insertions(+), 4176 deletions(-) delete mode 100644 contracts/assembly/tests/integration.vxastro-full diff --git a/contracts/assembly/src/unit_tests.rs b/contracts/assembly/src/unit_tests.rs index 8d0f02bc..42ddc438 100644 --- a/contracts/assembly/src/unit_tests.rs +++ b/contracts/assembly/src/unit_tests.rs @@ -3,21 +3,25 @@ use std::str::FromStr; use astroport::tokenfactory_tracker; use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}; -use cosmwasm_std::{coin, coins, to_json_binary, ContractResult, SystemResult, Uint128}; +use cosmwasm_std::{ + coin, coins, to_json_binary, BankMsg, ContractResult, CosmosMsg, IbcChannel, IbcEndpoint, + IbcOrder, SystemResult, Uint128, WasmMsg, +}; use cosmwasm_std::{ from_json, Addr, Coin, Decimal, Empty, OwnedDeps, QuerierResult, Uint64, WasmQuery, }; use test_case::test_case; use astroport_governance::assembly::{ - Config, Proposal, ProposalStatus, QueryMsg, DELAY_INTERVAL, DEPOSIT_INTERVAL, + Config, ExecuteMsg, Proposal, ProposalStatus, QueryMsg, DELAY_INTERVAL, DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE, MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, VOTING_PERIOD_INTERVAL, }; -use crate::contract::submit_proposal; +use crate::contract::{execute, execute_proposal, submit_proposal}; +use crate::error::ContractError; use crate::queries::query; -use crate::state::{CONFIG, PROPOSAL_COUNT}; +use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT}; const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); const XASTRO_DENOM: &str = "xastro"; @@ -52,9 +56,32 @@ fn custom_wasm_handler(request: &WasmQuery) -> QuerierResult { _ => unimplemented!(), } } + +const IBC_CONTROLLER: &str = "ibc_controller"; + fn mock_deps() -> OwnedDeps { let mut querier = MockQuerier::new(&[]); querier.update_wasm(custom_wasm_handler); + // mock ibc querier state + let controller_port = format!("wasm.{IBC_CONTROLLER}"); + querier.update_ibc( + &controller_port, + &[IbcChannel::new( + IbcEndpoint { + port_id: controller_port.clone(), + channel_id: "channel-1".to_string(), + }, + // counterparty doesn't matter in our unit tests + IbcEndpoint { + port_id: "".to_string(), + channel_id: "".to_string(), + }, + IbcOrder::Unordered, + // These also don't matter + "".to_string(), + "".to_string(), + )], + ); OwnedDeps { storage: MockStorage::default(), @@ -174,3 +201,302 @@ fn check_proposal_validation( ); } } + +#[test] +fn check_submit_ibc_proposal() { + let mut deps = mock_deps(); + + // Mocked instantiation + PROPOSAL_COUNT + .save(deps.as_mut().storage, &Uint64::zero()) + .unwrap(); + let mut config = Config { + xastro_denom: XASTRO_DENOM.to_string(), + xastro_denom_tracking: "".to_string(), + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: Addr::unchecked(""), + proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), + proposal_effective_delay: *DELAY_INTERVAL.start(), + proposal_expiration_period: *EXPIRATION_PERIOD_INTERVAL.start(), + proposal_required_deposit: PROPOSAL_REQUIRED_DEPOSIT.into(), + proposal_required_quorum: Decimal::from_str(MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE) + .unwrap(), + proposal_required_threshold: Decimal::from_atomics( + MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, + 2, + ) + .unwrap(), + whitelisted_links: vec!["https://some.link/".to_string()], + guardian_addr: None, + }; + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + let err = submit_proposal( + deps.as_mut(), + mock_env(), + mock_info("creator", &coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM)), + "title".to_string(), + "description".to_string(), + Some("https://some.link".to_string()), + vec![], + Some("channel-1".to_string()), + ) + .unwrap_err(); + assert_eq!(err, ContractError::MissingIBCController {}); + + // Set IBC conetroller + config.ibc_controller = Some(Addr::unchecked(IBC_CONTROLLER)); + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + let err = submit_proposal( + deps.as_mut(), + mock_env(), + mock_info("creator", &coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM)), + "title".to_string(), + "description".to_string(), + Some("https://some.link/".to_string()), + vec![], + Some("channel-10".to_string()), + ) + .unwrap_err(); + assert_eq!( + err.to_string(), + "Generic error: The contract does not have channel channel-10" + ); + + // channel-1 works + submit_proposal( + deps.as_mut(), + mock_env(), + mock_info("creator", &coins(PROPOSAL_REQUIRED_DEPOSIT, XASTRO_DENOM)), + "title".to_string(), + "description".to_string(), + Some("https://some.link/".to_string()), + vec![], + Some("channel-1".to_string()), + ) + .unwrap(); +} + +#[test] +fn check_execute_ibc_proposal() { + let mut deps = mock_deps(); + let env = mock_env(); + + let mut config = Config { + xastro_denom: "".to_string(), + xastro_denom_tracking: "".to_string(), + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: Addr::unchecked(""), + proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), + proposal_effective_delay: *DELAY_INTERVAL.start(), + proposal_expiration_period: *EXPIRATION_PERIOD_INTERVAL.start(), + proposal_required_deposit: PROPOSAL_REQUIRED_DEPOSIT.into(), + proposal_required_quorum: Decimal::from_str(MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE) + .unwrap(), + proposal_required_threshold: Decimal::from_atomics( + MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, + 2, + ) + .unwrap(), + whitelisted_links: vec!["https://some.link/".to_string()], + guardian_addr: None, + }; + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + let proposal = Proposal { + proposal_id: 1u8.into(), + submitter: Addr::unchecked(""), + status: ProposalStatus::Passed, + for_power: Default::default(), + outpost_for_power: Default::default(), + against_power: Default::default(), + outpost_against_power: Default::default(), + start_block: 0, + start_time: 0, + end_block: 0, + delayed_end_block: 0, + expiration_block: u64::MAX, + title: "".to_string(), + description: "".to_string(), + link: None, + messages: vec![BankMsg::Send { + to_address: "".to_string(), + amount: coins(1, "some_coin"), + } + .into()], + deposit_amount: Default::default(), + ibc_channel: Some("channel-1".to_string()), + total_voting_power: Default::default(), + }; + + // Mocked proposal + PROPOSALS.save(deps.as_mut().storage, 1, &proposal).unwrap(); + + let err = execute_proposal(deps.as_mut(), env.clone(), 1).unwrap_err(); + assert_eq!(err, ContractError::MissingIBCController {}); + + // Set IBC conetroller + config.ibc_controller = Some(Addr::unchecked(IBC_CONTROLLER)); + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + let resp = execute_proposal(deps.as_mut(), env, 1).unwrap(); + assert_eq!(resp.messages.len(), 1); + assert!( + matches!( + &resp.messages[0].msg, + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + .. + }) if contract_addr == IBC_CONTROLLER + ), + "{:#?}", + resp.messages[0].msg + ); +} + +#[test] +fn check_controller_callback() { + let mut deps = mock_deps(); + + let mut config = Config { + xastro_denom: "".to_string(), + xastro_denom_tracking: "".to_string(), + vxastro_token_addr: None, + voting_escrow_delegator_addr: None, + ibc_controller: None, + generator_controller: None, + hub: None, + builder_unlock_addr: Addr::unchecked(""), + proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), + proposal_effective_delay: *DELAY_INTERVAL.start(), + proposal_expiration_period: *EXPIRATION_PERIOD_INTERVAL.start(), + proposal_required_deposit: PROPOSAL_REQUIRED_DEPOSIT.into(), + proposal_required_quorum: Decimal::from_str(MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE) + .unwrap(), + proposal_required_threshold: Decimal::from_atomics( + MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, + 2, + ) + .unwrap(), + whitelisted_links: vec!["https://some.link/".to_string()], + guardian_addr: None, + }; + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + // Mocked proposal + let mut proposal = Proposal { + proposal_id: 1u8.into(), + submitter: Addr::unchecked(""), + status: ProposalStatus::Active, + for_power: Default::default(), + outpost_for_power: Default::default(), + against_power: Default::default(), + outpost_against_power: Default::default(), + start_block: 0, + start_time: 0, + end_block: 0, + delayed_end_block: 0, + expiration_block: u64::MAX, + title: "".to_string(), + description: "".to_string(), + link: None, + messages: vec![BankMsg::Send { + to_address: "".to_string(), + amount: coins(1, "some_coin"), + } + .into()], + deposit_amount: Default::default(), + ibc_channel: Some("channel-1".to_string()), + total_voting_power: Default::default(), + }; + PROPOSALS.save(deps.as_mut().storage, 1, &proposal).unwrap(); + + // No controller in config + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(IBC_CONTROLLER, &[]), + ExecuteMsg::IBCProposalCompleted { + proposal_id: 1, + status: ProposalStatus::Executed, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::InvalidIBCController {}); + + // Set IBC conetroller + config.ibc_controller = Some(Addr::unchecked(IBC_CONTROLLER)); + CONFIG.save(deps.as_mut().storage, &config).unwrap(); + + // Wrong sender + let err = execute( + deps.as_mut(), + mock_env(), + mock_info("random", &[]), + ExecuteMsg::IBCProposalCompleted { + proposal_id: 1, + status: ProposalStatus::Executed, + }, + ) + .unwrap_err(); + assert_eq!(err, ContractError::InvalidIBCController {}); + + // Invalid current proposal status + let err = execute( + deps.as_mut(), + mock_env(), + mock_info(IBC_CONTROLLER, &[]), + ExecuteMsg::IBCProposalCompleted { + proposal_id: 1, + status: ProposalStatus::Executed, + }, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::WrongIbcProposalStatus(proposal.status.to_string(),) + ); + + proposal.status = ProposalStatus::InProgress; + PROPOSALS.save(deps.as_mut().storage, 1, &proposal).unwrap(); + + // Try to set invalid status + execute( + deps.as_mut(), + mock_env(), + mock_info(IBC_CONTROLLER, &[]), + ExecuteMsg::IBCProposalCompleted { + proposal_id: 1, + status: ProposalStatus::Active, + }, + ) + .unwrap_err(); + assert_eq!( + err, + ContractError::WrongIbcProposalStatus(ProposalStatus::Active.to_string()) + ); + + // Valid callback + execute( + deps.as_mut(), + mock_env(), + mock_info(IBC_CONTROLLER, &[]), + ExecuteMsg::IBCProposalCompleted { + proposal_id: 1, + status: ProposalStatus::Executed, + }, + ) + .unwrap(); + + let proposal = PROPOSALS.load(deps.as_mut().storage, 1).unwrap(); + assert_eq!(proposal.status, ProposalStatus::Executed); +} diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 1a0f9ade..8de40e70 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -6,8 +6,9 @@ use cw_multi_test::Executor; use astro_assembly::error::ContractError; use astroport_governance::assembly::{ - Config, ExecuteMsg, InstantiateMsg, ProposalStatus, ProposalVoteOption, QueryMsg, UpdateConfig, - DELAY_INTERVAL, DEPOSIT_INTERVAL, EXPIRATION_PERIOD_INTERVAL, VOTING_PERIOD_INTERVAL, + Config, ExecuteMsg, InstantiateMsg, ProposalListResponse, ProposalStatus, ProposalVoteOption, + ProposalVoterResponse, QueryMsg, UpdateConfig, DELAY_INTERVAL, DEPOSIT_INTERVAL, + EXPIRATION_PERIOD_INTERVAL, VOTING_PERIOD_INTERVAL, }; use crate::common::helper::{ @@ -735,1644 +736,49 @@ fn test_voting_power() { assert_eq!(proposal.status, ProposalStatus::Passed); assert_eq!(proposal.for_power.u128(), for_power); assert_eq!(proposal.against_power.u128(), against_power); + + let proposal_votes = helper.proposal_votes(1); + assert_eq!(proposal_votes.for_power.u128(), for_power); + assert_eq!(proposal_votes.against_power.u128(), against_power); } -// #[test] -// fn test_successful_proposal() { -// let owner = Addr::unchecked("owner"); -// let mut helper = Helper::new(&owner).unwrap(); -// -// // Init voting power for users -// let balances: Vec<(&str, u128, u128)> = vec![ -// ("user0", PROPOSAL_REQUIRED_DEPOSIT.u128(), 0), // proposal submitter -// ("user1", 20, 80), -// ("user2", 100, 100), -// ("user3", 300, 100), -// ("user4", 200, 50), -// ("user5", 0, 90), -// ("user6", 100, 200), -// ("user7", 30, 0), -// ("user8", 80, 100), -// ("user9", 50, 0), -// ("user10", 0, 90), -// ("user11", 500, 0), -// ("user12", 10000_000000, 0), -// ]; -// -// let default_allocation_params = AllocationParams { -// amount: Uint128::zero(), -// unlock_schedule: Schedule { -// start_time: 12_345, -// cliff: 5, -// duration: 500, -// percent_at_cliff: None, -// }, -// proposed_receiver: None, -// }; -// -// let locked_balances = vec![ -// ( -// "user1".to_string(), -// AllocationParams { -// amount: Uint128::from(80u32), -// ..default_allocation_params.clone() -// }, -// ), -// ( -// "user4".to_string(), -// AllocationParams { -// amount: Uint128::from(50u32), -// ..default_allocation_params.clone() -// }, -// ), -// ( -// "user7".to_string(), -// AllocationParams { -// amount: Uint128::from(100u32), -// ..default_allocation_params.clone() -// }, -// ), -// ( -// "user10".to_string(), -// AllocationParams { -// amount: Uint128::from(30u32), -// ..default_allocation_params -// }, -// ), -// ]; -// -// for (addr, xastro, vxastro) in balances { -// if xastro > 0 { -// helper.mint_tokens(&Addr::unchecked(addr), xastro); -// } -// -// if vxastro > 0 { -// helper.mint_vxastro(&Addr::unchecked(addr), vxastro); -// } -// } -// -// helper.create_allocations(locked_balances); -// -// // Skip period -// helper.app.update_block(|mut block| { -// block.time = block.time.plus_seconds(WEEK); -// block.height += WEEK / 5; -// }); -// -// // Create default proposal -// helper.create_proposal(&Addr::unchecked("user0"), vec![]); -// -// let votes: Vec<(&str, ProposalVoteOption, u128)> = vec![ -// ("user1", ProposalVoteOption::For, 180u128), -// ("user2", ProposalVoteOption::For, 200u128), -// ("user3", ProposalVoteOption::For, 400u128), -// ("user4", ProposalVoteOption::For, 300u128), -// ("user5", ProposalVoteOption::For, 90u128), -// ("user6", ProposalVoteOption::For, 300u128), -// ("user7", ProposalVoteOption::For, 130u128), -// ("user8", ProposalVoteOption::Against, 180u128), -// ("user9", ProposalVoteOption::Against, 50u128), -// ("user10", ProposalVoteOption::Against, 120u128), -// ("user11", ProposalVoteOption::Against, 500u128), -// ("user12", ProposalVoteOption::For, 10000_000000u128), -// ]; -// -// let prop_vp = helper.proposal_total_vp(1).unwrap(); -// assert_eq!(prop_vp, 20000002450u128.into()); -// -// for (addr, option, expected_vp) in votes { -// let sender = Addr::unchecked(addr); -// -// let vp = helper.user_vp(&sender, 1); -// assert_eq!(vp, expected_vp.into()); -// -// helper.cast_vote(1, sender, option).unwrap(); -// } -// -// let proposal = helper.proposal(1); -// -// let proposal_votes = helper.proposal_votes(1); -// -// let proposal_for_voters = helper -// .proposal_voters(1) -// .into_iter() -// .filter(|v| v.vote_option == ProposalVoteOption::For) -// .collect::>(); -// -// let proposal_against_voters = helper -// .proposal_voters(1) -// .into_iter() -// .filter(|v| v.vote_option == ProposalVoteOption::Against) -// .collect::>(); -// -// // Check proposal votes -// assert_eq!(proposal.for_power, Uint128::from(10000001600u128)); -// assert_eq!(proposal.against_power, Uint128::from(850u32)); -// -// assert_eq!(proposal_votes.for_power, Uint128::from(10000001600u128)); -// assert_eq!(proposal_votes.against_power, Uint128::from(850u32)); -// -// assert_eq!( -// proposal_for_voters, -// vec![ -// Addr::unchecked("user1"), -// Addr::unchecked("user2"), -// Addr::unchecked("user3"), -// Addr::unchecked("user4"), -// Addr::unchecked("user5"), -// Addr::unchecked("user6"), -// Addr::unchecked("user7"), -// Addr::unchecked("user12"), -// ] -// ); -// assert_eq!( -// proposal_against_voters, -// vec![ -// Addr::unchecked("user8"), -// Addr::unchecked("user9"), -// Addr::unchecked("user10"), -// Addr::unchecked("user11") -// ] -// ); -// -// // Skip voting period -// helper.app.update_block(|bi| { -// bi.height += PROPOSAL_VOTING_PERIOD + 1; -// bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); -// }); -// -// // Try to vote after voting period -// let err = helper -// .cast_vote(1, Addr::unchecked("user11"), ProposalVoteOption::Against) -// .unwrap_err(); -// -// assert_eq!( -// err.downcast::().unwrap(), -// ContractError::VotingPeriodEnded {} -// ); -// -// // Try to execute the proposal before end_proposal -// let err = helper -// .app -// .execute_contract( -// Addr::unchecked("user0"), -// helper.assembly.clone(), -// &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.downcast::().unwrap(), -// ContractError::ProposalNotPassed {} -// ); -// -// // Check the successful completion of the proposal -// check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); -// -// app.execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::EndProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap(); -// -// check_token_balance( -// &mut app, -// &xastro_addr, -// &Addr::unchecked("user0"), -// PROPOSAL_REQUIRED_DEPOSIT, -// ); -// -// let proposal: Proposal = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::Proposal { proposal_id: 1 }, -// ) -// .unwrap(); -// -// assert_eq!(proposal.status, ProposalStatus::Passed); -// -// // Try to end proposal again -// let err = app -// .execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::EndProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!(err.root_cause().to_string(), "Proposal not active!"); -// -// // Try to execute the proposal before the delay -// let err = app -// .execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!(err.root_cause().to_string(), "Proposal delay not ended!"); -// -// // Skip blocks -// app.update_block(|bi| { -// bi.height += PROPOSAL_EFFECTIVE_DELAY + 1; -// bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EFFECTIVE_DELAY + 1)); -// }); -// -// // Try to execute the proposal after the delay -// app.execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap(); -// -// let config: Config = app -// .wrap() -// .query_wasm_smart(assembly_addr.to_string(), &QueryMsg::Config {}) -// .unwrap(); -// -// let proposal: Proposal = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.to_string(), -// &QueryMsg::Proposal { proposal_id: 1 }, -// ) -// .unwrap(); -// -// // Check execution result -// assert_eq!(config.proposal_voting_period, PROPOSAL_VOTING_PERIOD + 1000); -// assert_eq!( -// config.whitelisted_links, -// vec![ -// "https://some1.link/".to_string(), -// "https://some2.link/".to_string(), -// ] -// ); -// assert_eq!(proposal.status, ProposalStatus::Executed); -// -// // Try to remove proposal before expiration period -// let err = app -// .execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!(err.root_cause().to_string(), "Proposal not completed!"); -// -// // Remove expired proposal -// app.update_block(|bi| { -// bi.height += PROPOSAL_EXPIRATION_PERIOD + 1; -// bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + 1)); -// }); -// -// app.execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap(); -// -// let res: ProposalListResponse = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.to_string(), -// &QueryMsg::Proposals { -// start: None, -// limit: None, -// }, -// ) -// .unwrap(); -// -// assert_eq!(res.proposal_list, vec![]); -// // proposal_count should not be changed after removing a proposal -// assert_eq!(res.proposal_count, Uint64::from(1u32)); -// } -// -// #[test] -// fn test_successful_emissions_proposal() { -// use cosmwasm_std::{coins, BankMsg}; -// -// let mut app = mock_app(); -// let owner = Addr::unchecked("generator_controller"); -// -// let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); -// -// // Provide some funds to the Assembly contract to use in the proposal messages -// app.init_modules(|router, _, storage| { -// router.bank.init_balance( -// storage, -// &Addr::unchecked(assembly_addr.clone()), -// coins(1000, "uluna"), -// ) -// }) -// .unwrap(); -// -// let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { -// title: "Emissions Test title".to_string(), -// description: "Emissions Test description".to_string(), -// // Sample message to use as we don't have IBC or the Generator to set emissions on -// messages: vec![CosmosMsg::Bank(BankMsg::Send { -// to_address: "generator_controller".into(), -// amount: coins(1, "uluna"), -// })], -// ibc_channel: None, -// }; -// -// app.execute_contract( -// Addr::unchecked("generator_controller"), -// assembly_addr.clone(), -// &emissions_proposal_msg, -// &[], -// ) -// .unwrap(); -// -// let proposal: Proposal = app -// .wrap() -// .query_wasm_smart(assembly_addr, &QueryMsg::Proposal { proposal_id: 1 }) -// .unwrap(); -// -// assert_eq!(proposal.status, ProposalStatus::Executed); -// } -// -// #[test] -// fn test_no_generator_controller_emissions_proposal() { -// let mut app = mock_app(); -// let owner = Addr::unchecked("generator_controller"); -// -// let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, false, false); -// let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { -// title: "Emissions Test title!".to_string(), -// description: "Emissions Test description!".to_string(), -// messages: vec![], -// ibc_channel: None, -// }; -// -// let err = app -// .execute_contract( -// Addr::unchecked("generator_controller"), -// assembly_addr, -// &emissions_proposal_msg, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Sender is not the Generator controller installed in the assembly" -// ); -// } -// -// #[test] -// fn test_empty_messages_emissions_proposal() { -// let mut app = mock_app(); -// let owner = Addr::unchecked("generator_controller"); -// -// let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); -// let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { -// title: "Emissions Test title!".to_string(), -// description: "Emissions Test description!".to_string(), -// messages: vec![], -// ibc_channel: None, -// }; -// -// let err = app -// .execute_contract( -// Addr::unchecked("generator_controller"), -// assembly_addr, -// &emissions_proposal_msg, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "The proposal has no messages to execute" -// ); -// } -// -// #[test] -// fn test_unauthorised_emissions_proposal() { -// use cosmwasm_std::BankMsg; -// -// let mut app = mock_app(); -// let owner = Addr::unchecked("generator_controller"); -// -// let (_, _, _, _, _, assembly_addr, _, _) = instantiate_contracts(&mut app, owner, true, false); -// let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { -// title: "Emissions Test title!".to_string(), -// description: "Emissions Test description!".to_string(), -// // Sample message to use as we don't have IBC or the Generator to set emissions on -// messages: vec![CosmosMsg::Bank(BankMsg::Send { -// to_address: "generator_controller".into(), -// amount: coins(1, "uluna"), -// })], -// ibc_channel: None, -// }; -// -// let err = app -// .execute_contract( -// Addr::unchecked("not_generator_controller"), -// assembly_addr, -// &emissions_proposal_msg, -// &[], -// ) -// .unwrap_err(); -// -// assert_eq!(err.root_cause().to_string(), "Unauthorized"); -// } -// -// #[test] -// fn test_voting_power_changes() { -// let mut app = mock_app(); -// -// let owner = Addr::unchecked("owner"); -// -// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = -// instantiate_contracts(&mut app, owner, false, false); -// -// // Mint tokens for submitting proposal -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user0"), -// PROPOSAL_REQUIRED_DEPOSIT, -// ); -// -// // Mint tokens for casting votes at start block -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user1"), -// 40000_000000, -// ); -// -// app.update_block(|mut block| { -// block.time = block.time.plus_seconds(WEEK); -// block.height += WEEK / 5; -// }); -// -// // Create proposal -// create_proposal( -// &mut app, -// &xastro_addr, -// &assembly_addr, -// Addr::unchecked("user0"), -// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: assembly_addr.to_string(), -// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { -// xastro_token_addr: None, -// vxastro_token_addr: None, -// voting_escrow_delegator_addr: None, -// ibc_controller: None, -// generator_controller: None, -// hub: None, -// builder_unlock_addr: None, -// proposal_voting_period: Some(750), -// proposal_effective_delay: None, -// proposal_expiration_period: None, -// proposal_required_deposit: None, -// proposal_required_quorum: None, -// proposal_required_threshold: None, -// whitelist_add: None, -// whitelist_remove: None, -// guardian_addr: None, -// }))) -// .unwrap(), -// funds: vec![], -// })]), -// ); -// // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user2"), -// 5000_000000, -// ); -// -// app.update_block(next_block); -// -// // user1 can vote as he had voting power before the proposal submitting. -// cast_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// Addr::unchecked("user1"), -// ProposalVoteOption::For, -// ) -// .unwrap(); -// // Should panic, because user2 doesn't have any voting power. -// let err = cast_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// Addr::unchecked("user2"), -// ProposalVoteOption::Against, -// ) -// .unwrap_err(); -// -// // user2 doesn't have voting power and doesn't affect on total voting power(total supply at) -// // total supply = 5000 -// assert_eq!( -// err.root_cause().to_string(), -// "You don't have any voting power!" -// ); -// -// app.update_block(next_block); -// -// // Skip voting period and delay -// app.update_block(|bi| { -// bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; -// bi.time = bi -// .time -// .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); -// }); -// -// // End proposal -// app.execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::EndProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap(); -// -// let proposal: Proposal = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::Proposal { proposal_id: 1 }, -// ) -// .unwrap(); -// -// // Check proposal votes -// assert_eq!(proposal.for_power, Uint128::from(40000_000000u128)); -// assert_eq!(proposal.against_power, Uint128::zero()); -// // Should be passed, as total_voting_power=5000, for_votes=40000. -// // So user2 didn't affect the result. Because he had to have xASTRO before the vote was submitted. -// assert_eq!(proposal.status, ProposalStatus::Passed); -// } -// -// #[test] -// fn test_fail_outpost_vote_without_hub() { -// let mut app = mock_app(); -// -// let owner = Addr::unchecked("owner"); -// -// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = -// instantiate_contracts(&mut app, owner, false, false); -// -// // Mint tokens for submitting proposal -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user0"), -// PROPOSAL_REQUIRED_DEPOSIT, -// ); -// -// // Mint tokens for casting votes at start block -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user1"), -// 40000_000000, -// ); -// -// app.update_block(|mut block| { -// block.time = block.time.plus_seconds(WEEK); -// block.height += WEEK / 5; -// }); -// -// // Create proposal -// create_proposal( -// &mut app, -// &xastro_addr, -// &assembly_addr, -// Addr::unchecked("user0"), -// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: assembly_addr.to_string(), -// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { -// xastro_token_addr: None, -// vxastro_token_addr: None, -// voting_escrow_delegator_addr: None, -// ibc_controller: None, -// generator_controller: None, -// hub: None, -// builder_unlock_addr: None, -// proposal_voting_period: Some(750), -// proposal_effective_delay: None, -// proposal_expiration_period: None, -// proposal_required_deposit: None, -// proposal_required_quorum: None, -// proposal_required_threshold: None, -// whitelist_add: None, -// whitelist_remove: None, -// guardian_addr: None, -// }))) -// .unwrap(), -// funds: vec![], -// })]), -// ); -// // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user2"), -// 5000_000000, -// ); -// -// app.update_block(next_block); -// -// // user1 can not vote from an Outpost due to no Hub contract set -// let err = cast_outpost_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// Addr::unchecked("invalid_contract"), -// Addr::unchecked("user1"), -// ProposalVoteOption::For, -// Uint128::from(100u64), -// ) -// .unwrap_err(); -// -// assert_eq!( -// err.root_cause().to_string(), -// "Sender is not the Hub installed in the assembly" -// ); -// } -// -// #[test] -// fn test_outpost_vote() { -// let mut app = mock_app(); -// -// let owner = Addr::unchecked("owner"); -// -// let (astro_token, staking_instance, xastro_addr, _, _, assembly_addr, _, hub_addr) = -// instantiate_contracts(&mut app, owner.clone(), false, true); -// -// let user1_voting_power = 10_000_000_000; -// let user2_voting_power = 5_000_000_000; -// let remote_user1_voting_power = 80_000_000_000u128; -// // let remote_user2_voting_power = 3_000_000_000u128; -// -// let hub_addr = hub_addr.unwrap(); -// -// // Mint tokens for submitting proposal -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user0"), -// PROPOSAL_REQUIRED_DEPOSIT, -// ); -// -// // Mint tokens for casting votes at start block -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user1"), -// user1_voting_power, -// ); -// -// // Mint tokens for casting votes against vote at start block -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user2"), -// user2_voting_power, -// ); -// -// // Mint ASTRO to stake -// mint_tokens( -// &mut app, -// &owner, -// &astro_token, -// &Addr::unchecked("cw20ics20"), -// 1_000_000_000_000u128, -// ); -// -// app.update_block(|mut block| { -// block.time = block.time.plus_seconds(WEEK); -// block.height += WEEK / 5; -// }); -// -// // Create proposal -// create_proposal( -// &mut app, -// &xastro_addr, -// &assembly_addr, -// Addr::unchecked("user0"), -// Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr: assembly_addr.to_string(), -// msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { -// xastro_token_addr: None, -// vxastro_token_addr: None, -// voting_escrow_delegator_addr: None, -// ibc_controller: None, -// generator_controller: None, -// hub: None, -// builder_unlock_addr: None, -// proposal_voting_period: Some(750), -// proposal_effective_delay: None, -// proposal_expiration_period: None, -// proposal_required_deposit: None, -// proposal_required_quorum: None, -// proposal_required_threshold: None, -// whitelist_add: None, -// whitelist_remove: None, -// guardian_addr: None, -// }))) -// .unwrap(), -// funds: vec![], -// })]), -// ); -// -// app.update_block(next_block); -// -// // Outpost votes won't be accepted from other addresses -// let err = cast_outpost_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// Addr::unchecked("other_contract"), -// Addr::unchecked("remote1"), -// ProposalVoteOption::For, -// Uint128::from(remote_user1_voting_power), -// ) -// .unwrap_err(); -// assert_eq!(err.root_cause().to_string(), "Unauthorized"); -// -// // Attempts to vote with no xASTRO minted on Outposts -// let err = cast_outpost_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// hub_addr, -// Addr::unchecked("remote1"), -// ProposalVoteOption::For, -// Uint128::from(remote_user1_voting_power), -// ) -// .unwrap_err(); -// assert_eq!( -// err.root_cause().to_string(), -// "Voting power exceeds maximum Outpost power" -// ); -// -// // Note: Due to cw-multitest not supporting IBC messages we can no longer -// // test voting with Outpost voting power -// -// // app.execute_contract( -// // owner, -// // hub_addr.clone(), -// // &astroport_governance::hub::ExecuteMsg::AddOutpost { -// // outpost_addr: "outpost1".to_string(), -// // outpost_channel: "channel-3".to_string(), -// // cw20_ics20_channel: "channel-1".to_string(), -// // }, -// // &[], -// // ) -// // .unwrap_err(); -// -// // Stake some ASTRO from an Outpost -// // stake_remote_astro( -// // &mut app, -// // Addr::unchecked("cw20ics20".to_string()), -// // hub_addr.clone(), -// // astro_token, -// // Uint128::from(remote_user1_voting_power), -// // ) -// // .unwrap_err(); -// -// // Continue normally -// // cast_outpost_vote( -// // &mut app, -// // assembly_addr.clone(), -// // 1, -// // hub_addr.clone(), -// // Addr::unchecked("remote1"), -// // ProposalVoteOption::For, -// // Uint128::from(remote_user1_voting_power), -// // ) -// // .unwrap(); -// } -// -// #[test] -// fn test_block_height_selection() { -// // Block height is 12345 after app initialization -// let mut app = mock_app(); -// -// let owner = Addr::unchecked("owner"); -// let user1 = Addr::unchecked("user1"); -// let user2 = Addr::unchecked("user2"); -// let user3 = Addr::unchecked("user3"); -// -// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = -// instantiate_contracts(&mut app, owner, false, false); -// -// // Mint tokens for submitting proposal -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked("user0"), -// PROPOSAL_REQUIRED_DEPOSIT, -// ); -// -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &user1, -// 6000_000001, -// ); -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &user2, -// 4000_000000, -// ); -// -// // Skip to the next period -// app.update_block(|mut block| { -// block.time = block.time.plus_seconds(WEEK); -// block.height += WEEK / 5; -// }); -// -// // Create proposal -// create_proposal( -// &mut app, -// &xastro_addr, -// &assembly_addr, -// Addr::unchecked("user0"), -// None, -// ); -// -// cast_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// user1, -// ProposalVoteOption::For, -// ) -// .unwrap(); -// -// // Mint huge amount of xASTRO. These tokens cannot affect on total supply in proposal 1 because -// // they were minted after proposal.start_block - 1 -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &user3, -// 100000_000000, -// ); -// // Mint more xASTRO to user2, who will vote against the proposal, what is enough to make proposal unsuccessful. -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &user2, -// 3000_000000, -// ); -// // Total voting power should be 20k xASTRO (proposal minimum deposit 10k + 4k + 6k users VP) -// check_total_vp(&mut app, &assembly_addr, 1, 20000_000001); -// -// cast_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// user2, -// ProposalVoteOption::Against, -// ) -// .unwrap(); -// -// // Skip voting period -// app.update_block(|bi| { -// bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; -// bi.time = bi -// .time -// .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); -// }); -// -// // End proposal -// app.execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::EndProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap(); -// -// let proposal: Proposal = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::Proposal { proposal_id: 1 }, -// ) -// .unwrap(); -// -// assert_eq!(proposal.for_power, Uint128::new(6000_000001)); -// // Against power is 4000, as user2's balance was increased after proposal.start_block - 1 -// // at which everyone's voting power are considered. -// assert_eq!(proposal.against_power, Uint128::new(4000_000000)); -// // Proposal is passed, as the total supply was increased after proposal.start_block - 1. -// assert_eq!(proposal.status, ProposalStatus::Passed); -// } -// -// #[test] -// fn test_unsuccessful_proposal() { -// let mut app = mock_app(); -// -// let owner = Addr::unchecked("owner"); -// -// let (_, staking_instance, xastro_addr, _, _, assembly_addr, _, _) = -// instantiate_contracts(&mut app, owner, false, false); -// -// // Init voting power for users -// let xastro_balances: Vec<(&str, u128)> = vec![ -// ("user0", PROPOSAL_REQUIRED_DEPOSIT), // proposal submitter -// ("user1", 100), -// ("user2", 200), -// ("user3", 400), -// ("user4", 250), -// ("user5", 90), -// ("user6", 300), -// ("user7", 30), -// ("user8", 180), -// ("user9", 50), -// ("user10", 90), -// ("user11", 500), -// ]; -// -// for (addr, xastro) in xastro_balances { -// mint_tokens( -// &mut app, -// &staking_instance, -// &xastro_addr, -// &Addr::unchecked(addr), -// xastro, -// ); -// } -// -// // Skip period -// app.update_block(|mut block| { -// block.time = block.time.plus_seconds(WEEK); -// block.height += WEEK / 5; -// }); -// -// // Create proposal -// create_proposal( -// &mut app, -// &xastro_addr, -// &assembly_addr, -// Addr::unchecked("user0"), -// None, -// ); -// -// let expected_voting_power: Vec<(&str, ProposalVoteOption)> = vec![ -// ("user1", ProposalVoteOption::For), -// ("user2", ProposalVoteOption::For), -// ("user3", ProposalVoteOption::For), -// ("user4", ProposalVoteOption::Against), -// ("user5", ProposalVoteOption::Against), -// ("user6", ProposalVoteOption::Against), -// ("user7", ProposalVoteOption::Against), -// ("user8", ProposalVoteOption::Against), -// ("user9", ProposalVoteOption::Against), -// ("user10", ProposalVoteOption::Against), -// ]; -// -// for (addr, option) in expected_voting_power { -// cast_vote( -// &mut app, -// assembly_addr.clone(), -// 1, -// Addr::unchecked(addr), -// option, -// ) -// .unwrap(); -// } -// -// // Skip voting period -// app.update_block(|bi| { -// bi.height += PROPOSAL_VOTING_PERIOD + 1; -// bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); -// }); -// -// // Check balance of submitter before and after proposal completion -// check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); -// -// app.execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::EndProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap(); -// -// check_token_balance( -// &mut app, -// &xastro_addr, -// &Addr::unchecked("user0"), -// 10000_000000, -// ); -// -// // Check proposal status -// let proposal: Proposal = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.clone(), -// &QueryMsg::Proposal { proposal_id: 1 }, -// ) -// .unwrap(); -// -// assert_eq!(proposal.status, ProposalStatus::Rejected); -// -// // Remove expired proposal -// app.update_block(|bi| { -// bi.height += PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; -// bi.time = bi -// .time -// .plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); -// }); -// -// app.execute_contract( -// Addr::unchecked("user0"), -// assembly_addr.clone(), -// &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, -// &[], -// ) -// .unwrap(); -// -// let res: ProposalListResponse = app -// .wrap() -// .query_wasm_smart( -// assembly_addr.to_string(), -// &QueryMsg::Proposals { -// start: None, -// limit: None, -// }, -// ) -// .unwrap(); -// -// assert_eq!(res.proposal_list, vec![]); -// // proposal_count should not be changed after removing -// assert_eq!(res.proposal_count, Uint64::from(1u32)); -// } -// -// #[test] -// fn test_check_messages() { -// let mut app = mock_app(); -// let owner = Addr::unchecked("owner"); -// let (_, _, _, vxastro_addr, _, assembly_addr, _, _) = -// instantiate_contracts(&mut app, owner, false, false); -// -// change_owner(&mut app, &vxastro_addr, &assembly_addr); -// let user = Addr::unchecked("user"); -// let into_check_msg = |msgs: Vec<(String, Binary)>| { -// let messages = msgs -// .into_iter() -// .map(|(contract_addr, msg)| { -// CosmosMsg::Wasm(WasmMsg::Execute { -// contract_addr, -// msg, -// funds: vec![], -// }) -// }) -// .collect(); -// ExecuteMsg::CheckMessages { messages } -// }; -// -// let config_before: astroport_governance::voting_escrow_lite::Config = app -// .wrap() -// .query_wasm_smart( -// &vxastro_addr, -// &astroport_governance::voting_escrow_lite::QueryMsg::Config {}, -// ) -// .unwrap(); -// -// let vxastro_blacklist_msg = vec![( -// vxastro_addr.to_string(), -// to_json_binary( -// &astroport_governance::voting_escrow_lite::ExecuteMsg::UpdateConfig { -// new_guardian: None, -// generator_controller: None, -// outpost: None, -// }, -// ) -// .unwrap(), -// )]; -// let err = app -// .execute_contract( -// user, -// assembly_addr.clone(), -// &into_check_msg(vxastro_blacklist_msg), -// &[], -// ) -// .unwrap_err(); -// assert_eq!( -// &err.root_cause().to_string(), -// "Messages check passed. Nothing was committed to the blockchain" -// ); -// -// let config_after: astroport_governance::voting_escrow_lite::Config = app -// .wrap() -// .query_wasm_smart( -// &vxastro_addr, -// &astroport_governance::voting_escrow_lite::QueryMsg::Config {}, -// ) -// .unwrap(); -// assert_eq!(config_before, config_after); -// } -// -// fn mock_app() -> App { -// let mut env = mock_env(); -// env.block.time = Timestamp::from_seconds(EPOCH_START); -// let api = MockApi::default(); -// let bank = BankKeeper::new(); -// let storage = MockStorage::new(); -// -// AppBuilder::new() -// .with_api(api) -// .with_block(env.block) -// .with_bank(bank) -// .with_storage(storage) -// .build(|_, _, _| {}) -// } -// -// fn instantiate_contracts( -// router: &mut App, -// owner: Addr, -// with_generator_controller: bool, -// with_hub: bool, -// ) -> ( -// Addr, -// Addr, -// Addr, -// Addr, -// Addr, -// Addr, -// Option, -// Option, -// ) { -// let token_addr = instantiate_astro_token(router, &owner); -// let (staking_addr, xastro_token_addr) = instantiate_xastro_token(router, &owner, &token_addr); -// let vxastro_token_addr = instantiate_vxastro_token(router, &owner, &xastro_token_addr); -// let builder_unlock_addr = instantiate_builder_unlock_contract(router, &owner, &token_addr); -// -// // If we want to test immediate proposals we need to set the address -// // for the generator controller. Deploying the generator controller in this -// // test would require deploying factory, tokens and pools. That test is -// // better suited in the generator controller itself. Thus, we use the owner -// // address as the generator controller address to test immediate proposals. -// let mut generator_controller_addr = None; -// -// if with_generator_controller { -// generator_controller_addr = Some(owner.to_string()); -// } -// -// let mut hub_addr = None; -// -// if with_hub { -// hub_addr = Some(instantiate_hub( -// router, -// &owner, -// &Addr::unchecked("contract6".to_string()), -// &staking_addr, -// )); -// } -// -// let assembly_addr = instantiate_assembly_contract( -// router, -// &owner, -// &xastro_token_addr, -// &vxastro_token_addr, -// &builder_unlock_addr, -// None, -// generator_controller_addr, -// hub_addr.clone(), -// ); -// -// ( -// token_addr, -// staking_addr, -// xastro_token_addr, -// vxastro_token_addr, -// builder_unlock_addr, -// assembly_addr, -// None, -// hub_addr, -// ) -// } -// -// fn instantiate_astro_token(router: &mut App, owner: &Addr) -> Addr { -// let astro_token_contract = Box::new(ContractWrapper::new_with_empty( -// astroport_token::contract::execute, -// astroport_token::contract::instantiate, -// astroport_token::contract::query, -// )); -// -// let astro_token_code_id = router.store_code(astro_token_contract); -// -// let msg = TokenInstantiateMsg { -// name: String::from("Astro token"), -// symbol: String::from("ASTRO"), -// decimals: 6, -// initial_balances: vec![], -// mint: Some(MinterResponse { -// minter: owner.to_string(), -// cap: None, -// }), -// marketing: None, -// }; -// -// router -// .instantiate_contract( -// astro_token_code_id, -// owner.clone(), -// &msg, -// &[], -// String::from("ASTRO"), -// None, -// ) -// .unwrap() -// } -// -// fn instantiate_xastro_token(router: &mut App, owner: &Addr, astro_token: &Addr) -> (Addr, Addr) { -// let xastro_contract = Box::new(ContractWrapper::new_with_empty( -// astroport_xastro_token::contract::execute, -// astroport_xastro_token::contract::instantiate, -// astroport_xastro_token::contract::query, -// )); -// -// let xastro_code_id = router.store_code(xastro_contract); -// -// let staking_contract = Box::new( -// ContractWrapper::new_with_empty( -// astroport_staking::contract::execute, -// astroport_staking::contract::instantiate, -// astroport_staking::contract::query, -// ) -// .with_reply_empty(astroport_staking::contract::reply), -// ); -// -// let staking_code_id = router.store_code(staking_contract); -// -// let msg = astroport::staking::InstantiateMsg { -// owner: owner.to_string(), -// token_code_id: xastro_code_id, -// deposit_token_addr: astro_token.to_string(), -// marketing: None, -// }; -// let staking_instance = router -// .instantiate_contract( -// staking_code_id, -// owner.clone(), -// &msg, -// &[], -// String::from("xASTRO"), -// None, -// ) -// .unwrap(); -// -// let res = router -// .wrap() -// .query::(&QueryRequest::Wasm(WasmQuery::Smart { -// contract_addr: staking_instance.to_string(), -// msg: to_json_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), -// })) -// .unwrap(); -// -// (staking_instance, res.share_token_addr) -// } -// -// fn instantiate_vxastro_token(router: &mut App, owner: &Addr, xastro: &Addr) -> Addr { -// let vxastro_token_contract = Box::new(ContractWrapper::new_with_empty( -// voting_escrow_lite::execute::execute, -// voting_escrow_lite::contract::instantiate, -// voting_escrow_lite::query::query, -// )); -// -// let vxastro_token_code_id = router.store_code(vxastro_token_contract); -// -// let msg = VXAstroInstantiateMsg { -// owner: owner.to_string(), -// guardian_addr: Some(owner.to_string()), -// deposit_token_addr: xastro.to_string(), -// generator_controller_addr: None, -// outpost_addr: None, -// marketing: None, -// logo_urls_whitelist: vec![], -// }; -// -// router -// .instantiate_contract( -// vxastro_token_code_id, -// owner.clone(), -// &msg, -// &[], -// String::from("vxASTRO"), -// None, -// ) -// .unwrap() -// } -// -// fn instantiate_hub( -// router: &mut App, -// owner: &Addr, -// assembly_addr: &Addr, -// staking_addr: &Addr, -// ) -> Addr { -// let hub_contract = Box::new( -// ContractWrapper::new_with_empty( -// astroport_hub::execute::execute, -// astroport_hub::contract::instantiate, -// astroport_hub::query::query, -// ) -// .with_reply(astroport_hub::reply::reply), -// ); -// -// let hub_code_id = router.store_code(hub_contract); -// -// let msg = HubInstantiateMsg { -// owner: owner.to_string(), -// assembly_addr: assembly_addr.to_string(), -// cw20_ics20_addr: "cw20ics20".to_string(), -// generator_controller_addr: "unknown".to_string(), -// ibc_timeout_seconds: 60, -// staking_addr: staking_addr.to_string(), -// }; -// -// router -// .instantiate_contract( -// hub_code_id, -// owner.clone(), -// &msg, -// &[], -// String::from("Hub"), -// None, -// ) -// .unwrap() -// } -// -// fn instantiate_builder_unlock_contract(router: &mut App, owner: &Addr, astro_token: &Addr) -> Addr { -// let builder_unlock_contract = Box::new(ContractWrapper::new_with_empty( -// builder_unlock::contract::execute, -// builder_unlock::contract::instantiate, -// builder_unlock::contract::query, -// )); -// -// let builder_unlock_code_id = router.store_code(builder_unlock_contract); -// -// let msg = BuilderUnlockInstantiateMsg { -// owner: owner.to_string(), -// astro_token: astro_token.to_string(), -// max_allocations_amount: Uint128::new(300_000_000_000_000u128), -// }; -// -// router -// .instantiate_contract( -// builder_unlock_code_id, -// owner.clone(), -// &msg, -// &[], -// "Builder Unlock contract".to_string(), -// Some(owner.to_string()), -// ) -// .unwrap() -// } -// -// #[allow(clippy::too_many_arguments)] -// fn instantiate_assembly_contract( -// router: &mut App, -// owner: &Addr, -// xastro: &Addr, -// vxastro: &Addr, -// builder: &Addr, -// delegator: Option, -// generator_controller_addr: Option, -// hub_addr: Option, -// ) -> Addr { -// let assembly_contract = Box::new(ContractWrapper::new_with_empty( -// astro_assembly::contract::execute, -// astro_assembly::contract::instantiate, -// astro_assembly::contract::query, -// )); -// -// let assembly_code = router.store_code(assembly_contract); -// -// let hub: Option = hub_addr.as_ref().map(|s| s.to_string()); -// -// let msg = InstantiateMsg { -// xastro_token_addr: xastro.to_string(), -// vxastro_token_addr: Some(vxastro.to_string()), -// voting_escrow_delegator_addr: delegator, -// ibc_controller: None, -// generator_controller_addr, -// hub_addr: hub, -// builder_unlock_addr: builder.to_string(), -// proposal_voting_period: PROPOSAL_VOTING_PERIOD, -// proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, -// proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, -// proposal_required_deposit: Uint128::new(PROPOSAL_REQUIRED_DEPOSIT), -// proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), -// proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), -// whitelisted_links: vec!["https://some.link/".to_string()], -// }; -// -// router -// .instantiate_contract( -// assembly_code, -// owner.clone(), -// &msg, -// &[], -// "Assembly".to_string(), -// Some(owner.to_string()), -// ) -// .unwrap() -// } -// -// -// fn mint_vxastro( -// app: &mut App, -// staking_instance: &Addr, -// xastro: Addr, -// vxastro: &Addr, -// recipient: Addr, -// amount: u128, -// ) { -// mint_tokens(app, staking_instance, &xastro, &recipient, amount); -// -// let msg = Cw20ExecuteMsg::Send { -// contract: vxastro.to_string(), -// amount: Uint128::from(amount), -// msg: to_json_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), -// }; -// -// app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); -// } -// -// -// fn create_proposal( -// app: &mut App, -// token: &Addr, -// assembly: &Addr, -// submitter: Addr, -// msgs: Option>, -// ) { -// let submit_proposal_msg = Cw20HookMsg::SubmitProposal { -// title: "Test title!".to_string(), -// description: "Test description!".to_string(), -// link: None, -// messages: msgs, -// ibc_channel: None, -// }; -// -// app.execute_contract( -// submitter, -// token.clone(), -// &Cw20ExecuteMsg::Send { -// contract: assembly.to_string(), -// amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), -// msg: to_json_binary(&submit_proposal_msg).unwrap(), -// }, -// &[], -// ) -// .unwrap(); -// } -// -// fn check_token_balance(app: &mut App, token: &Addr, address: &Addr, expected: u128) { -// let msg = XAstroQueryMsg::Balance { -// address: address.to_string(), -// }; -// let res: StdResult = app.wrap().query_wasm_smart(token, &msg); -// assert_eq!(res.unwrap().balance, Uint128::from(expected)); -// } -// -// fn check_user_vp(app: &mut App, assembly: &Addr, address: &Addr, proposal_id: u64, expected: u128) { -// let res: Uint128 = app -// .wrap() -// .query_wasm_smart( -// assembly.to_string(), -// &QueryMsg::UserVotingPower { -// user: address.to_string(), -// proposal_id, -// }, -// ) -// .unwrap(); -// -// assert_eq!(res.u128(), expected); -// } -// -// fn check_total_vp(app: &mut App, assembly: &Addr, proposal_id: u64, expected: u128) { -// let res: Uint128 = app -// .wrap() -// .query_wasm_smart( -// assembly.to_string(), -// &QueryMsg::TotalVotingPower { proposal_id }, -// ) -// .unwrap(); -// -// assert_eq!(res.u128(), expected); -// } -// -// fn cast_vote( -// app: &mut App, -// assembly: Addr, -// proposal_id: u64, -// sender: Addr, -// option: ProposalVoteOption, -// ) -> anyhow::Result { -// app.execute_contract( -// sender, -// assembly, -// &ExecuteMsg::CastVote { -// proposal_id, -// vote: option, -// }, -// &[], -// ) -// } -// -// fn cast_outpost_vote( -// app: &mut App, -// assembly: Addr, -// proposal_id: u64, -// sender: Addr, -// voter: Addr, -// option: ProposalVoteOption, -// voting_power: Uint128, -// ) -> anyhow::Result { -// app.execute_contract( -// sender, -// assembly, -// &ExecuteMsg::CastOutpostVote { -// proposal_id, -// voter: voter.to_string(), -// vote: option, -// voting_power, -// }, -// &[], -// ) -// } -// -// // Add back once cw-multitest supports IBC -// // fn stake_remote_astro( -// // app: &mut App, -// // sender: Addr, -// // hub: Addr, -// // astro_token: Addr, -// // amount: Uint128, -// // ) -> anyhow::Result { -// // let cw20_msg = to_json_binary(&astroport_governance::hub::Cw20HookMsg::OutpostMemo { -// // channel: "channel-1".to_string(), -// // sender: "remoteuser1".to_string(), -// // receiver: hub.to_string(), -// // memo: "{\"stake\":{}}".to_string(), -// // }) -// // .unwrap(); -// -// // let msg = Cw20ExecuteMsg::Send { -// // contract: hub.to_string(), -// // amount, -// // msg: cw20_msg, -// // }; -// -// // app.execute_contract(sender, astro_token, &msg, &[]) -// // } -// -// fn change_owner(app: &mut App, contract: &Addr, assembly: &Addr) { -// let msg = astroport_governance::voting_escrow_lite::ExecuteMsg::ProposeNewOwner { -// new_owner: assembly.to_string(), -// expires_in: 100, -// }; -// app.execute_contract(Addr::unchecked("owner"), contract.clone(), &msg, &[]) -// .unwrap(); -// -// app.execute_contract( -// assembly.clone(), -// contract.clone(), -// &astroport_governance::voting_escrow_lite::ExecuteMsg::ClaimOwnership {}, -// &[], -// ) -// .unwrap(); -// } +#[test] +fn test_queries() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + let assembly = helper.assembly.clone(); + + helper.get_xastro(&owner, 10 * PROPOSAL_REQUIRED_DEPOSIT.u128() + 1000); + + for i in 1..=10 { + helper.next_block(100); + helper.submit_sample_proposal(&owner); + helper + .cast_vote(i, &owner, ProposalVoteOption::For) + .unwrap(); + } + + let proposal_voters = helper.proposal_voters(5); + assert_eq!( + proposal_voters, + [ProposalVoterResponse { + address: owner.to_string(), + vote_option: ProposalVoteOption::For + }] + ); + + let proposals = helper + .app + .wrap() + .query_wasm_smart::( + &assembly, + &QueryMsg::Proposals { + start: None, + limit: None, + }, + ) + .unwrap() + .proposal_list; + + assert_eq!(proposals.len(), 10); +} diff --git a/contracts/assembly/tests/integration.vxastro-full b/contracts/assembly/tests/integration.vxastro-full deleted file mode 100644 index a0e4af10..00000000 --- a/contracts/assembly/tests/integration.vxastro-full +++ /dev/null @@ -1,2517 +0,0 @@ -use astro_assembly::astroport; -use astroport::{ - token::InstantiateMsg as TokenInstantiateMsg, xastro_token::QueryMsg as XAstroQueryMsg, -}; -use astroport_governance::assembly::{ - Config, Cw20HookMsg, ExecuteMsg, InstantiateMsg, Proposal, ProposalListResponse, - ProposalStatus, ProposalVoteOption, ProposalVotesResponse, QueryMsg, UpdateConfig, - DEPOSIT_INTERVAL, VOTING_PERIOD_INTERVAL, -}; -use cosmwasm_std::coins; - -use std::str::FromStr; - -use astroport_governance::voting_escrow::{ - Cw20HookMsg as VXAstroCw20HookMsg, InstantiateMsg as VXAstroInstantiateMsg, -}; - -use astroport_governance::builder_unlock::msg::{ - InstantiateMsg as BuilderUnlockInstantiateMsg, ReceiveMsg as BuilderUnlockReceiveMsg, -}; -use astroport_governance::builder_unlock::{AllocationParams, Schedule}; -use astroport_governance::utils::{EPOCH_START, WEEK}; -use astroport_governance::voting_escrow_delegation::{ - ExecuteMsg as DelegatorExecuteMsg, InstantiateMsg as DelegatorInstantiateMsg, - QueryMsg as DelegatorQueryMsg, -}; -use cosmwasm_std::{ - testing::{mock_env, MockApi, MockStorage}, - to_json_binary, Addr, Binary, CosmosMsg, Decimal, QueryRequest, StdResult, Timestamp, Uint128, - Uint64, WasmMsg, WasmQuery, -}; -use cw20::{BalanceResponse, Cw20ExecuteMsg, MinterResponse}; -use cw_multi_test::{ - next_block, App, AppBuilder, AppResponse, BankKeeper, ContractWrapper, Executor, -}; - -const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); -const PROPOSAL_EFFECTIVE_DELAY: u64 = 12_342; -const PROPOSAL_EXPIRATION_PERIOD: u64 = 86_399; -const PROPOSAL_REQUIRED_DEPOSIT: u128 = *DEPOSIT_INTERVAL.start(); -const PROPOSAL_REQUIRED_QUORUM: &str = "0.50"; -const PROPOSAL_REQUIRED_THRESHOLD: &str = "0.60"; - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_contract_instantiation() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - // Instantiate needed contracts - let token_addr = instantiate_astro_token(&mut app, &owner); - let (_, xastro_token_addr) = instantiate_xastro_token(&mut app, &owner, &token_addr); - let vxastro_token_addr = instantiate_vxastro_token(&mut app, &owner, &xastro_token_addr); - let builder_unlock_addr = instantiate_builder_unlock_contract(&mut app, &owner, &token_addr); - - let assembly_contract = Box::new(ContractWrapper::new_with_empty( - astro_assembly::contract::execute, - astro_assembly::contract::instantiate, - astro_assembly::contract::query, - )); - - let assembly_code = app.store_code(assembly_contract); - - let assembly_default_instantiate_msg = InstantiateMsg { - xastro_token_addr: xastro_token_addr.to_string(), - vxastro_token_addr: Some(vxastro_token_addr.to_string()), - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller_addr: None, - hub_addr: None, - builder_unlock_addr: builder_unlock_addr.to_string(), - proposal_voting_period: PROPOSAL_VOTING_PERIOD, - proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, - proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, - proposal_required_deposit: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), - proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), - whitelisted_links: vec!["https://some.link/".to_string()], - }; - - // Try to instantiate assembly with wrong threshold - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_required_threshold: "0.3".to_string(), - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" - ); - - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_required_threshold: "1.1".to_string(), - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The required threshold for a proposal cannot be lower than 33% or higher than 100%" - ); - - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_required_quorum: "1.1".to_string(), - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The required quorum for a proposal cannot be lower than 1% or higher than 100%" - ); - - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_expiration_period: 500, - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The expiration period for a proposal cannot be lower than 12342 or higher than 100800" - ); - - let err = app - .instantiate_contract( - assembly_code, - owner.clone(), - &InstantiateMsg { - proposal_effective_delay: 400, - ..assembly_default_instantiate_msg.clone() - }, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: The effective delay for a proposal cannot be lower than 6171 or higher than 14400" - ); - - let assembly_instance = app - .instantiate_contract( - assembly_code, - owner.clone(), - &assembly_default_instantiate_msg, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap(); - - let res: Config = app - .wrap() - .query_wasm_smart(assembly_instance, &QueryMsg::Config {}) - .unwrap(); - - assert_eq!(res.xastro_token_addr, xastro_token_addr); - assert_eq!(res.builder_unlock_addr, builder_unlock_addr); - assert_eq!(res.proposal_voting_period, PROPOSAL_VOTING_PERIOD); - assert_eq!(res.proposal_effective_delay, PROPOSAL_EFFECTIVE_DELAY); - assert_eq!(res.proposal_expiration_period, PROPOSAL_EXPIRATION_PERIOD); - assert_eq!( - res.proposal_required_deposit, - Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) - ); - assert_eq!( - res.proposal_required_quorum, - Decimal::from_str(PROPOSAL_REQUIRED_QUORUM).unwrap() - ); - assert_eq!( - res.proposal_required_threshold, - Decimal::from_str(PROPOSAL_REQUIRED_THRESHOLD).unwrap() - ); - assert_eq!( - res.whitelisted_links, - vec!["https://some.link/".to_string(),] - ); -} - -#[test] -fn test_proposal_submitting() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - let user = Addr::unchecked("user1"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, false, false); - - let proposals: ProposalListResponse = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposals { - start: None, - limit: None, - }, - ) - .unwrap(); - - assert_eq!(proposals.proposal_count, Uint64::from(0u32)); - assert_eq!(proposals.proposal_list, vec![]); - - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user, - PROPOSAL_REQUIRED_DEPOSIT, - ); - - check_token_balance(&mut app, &xastro_addr, &user, PROPOSAL_REQUIRED_DEPOSIT); - - // Try to create proposal with insufficient token deposit - let submit_proposal_msg = Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from("https://some.link")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT - 1), - }; - - let err = app - .execute_contract(user.clone(), xastro_addr.clone(), &submit_proposal_msg, &[]) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Insufficient token deposit!"); - - // Try to create a proposal with wrong title - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("X"), - description: String::from("Description"), - link: Some(String::from("https://some.link/")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Title too short!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from_utf8(vec![b'X'; 65]).unwrap(), - description: String::from("Description"), - link: Some(String::from("https://some.link/")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Title too long!" - ); - - // Try to create a proposal with wrong description - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("X"), - link: Some(String::from("https://some.link/")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Description too short!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from_utf8(vec![b'X'; 1025]).unwrap(), - link: Some(String::from("https://some.link/")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Description too long!" - ); - - // Try to create a proposal with wrong link - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from("X")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Link too short!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from_utf8(vec![b'X'; 129]).unwrap()), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Link too long!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from("https://some1.link")), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Link is not whitelisted!" - ); - - let err = app - .execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from( - "https://some.link/", - )), - messages: None, - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Generic error: Link is not properly formatted or contains unsafe characters!" - ); - - // Valid proposal submission - app.execute_contract( - user.clone(), - xastro_addr.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly_addr.to_string(), - msg: to_json_binary(&Cw20HookMsg::SubmitProposal { - title: String::from("Title"), - description: String::from("Description"), - link: Some(String::from("https://some.link/q/")), - messages: Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - }))) - .unwrap(), - funds: vec![], - })]), - ibc_channel: None, - }) - .unwrap(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.proposal_id, Uint64::from(1u64)); - assert_eq!(proposal.submitter, user); - assert_eq!(proposal.status, ProposalStatus::Active); - assert_eq!(proposal.for_power, Uint128::zero()); - assert_eq!(proposal.against_power, Uint128::zero()); - assert_eq!(proposal.start_block, 12_345); - assert_eq!(proposal.end_block, 12_345 + PROPOSAL_VOTING_PERIOD); - assert_eq!(proposal.title, String::from("Title")); - assert_eq!(proposal.description, String::from("Description")); - assert_eq!(proposal.link, Some(String::from("https://some.link/q/"))); - assert_eq!( - proposal.messages, - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - }))) - .unwrap(), - funds: vec![], - })]) - ); - assert_eq!( - proposal.deposit_amount, - Uint128::from(PROPOSAL_REQUIRED_DEPOSIT) - ) -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_successful_proposal() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let ( - token_addr, - staking_instance, - xastro_addr, - vxastro_addr, - builder_unlock_addr, - assembly_addr, - _, - ) = instantiate_contracts(&mut app, owner, false, false, false); - - // Init voting power for users - let balances: Vec<(&str, u128, u128)> = vec![ - ("user0", PROPOSAL_REQUIRED_DEPOSIT, 0), // proposal submitter - ("user1", 20, 80), - ("user2", 100, 100), - ("user3", 300, 100), - ("user4", 200, 50), - ("user5", 0, 90), - ("user6", 100, 200), - ("user7", 30, 0), - ("user8", 80, 100), - ("user9", 50, 0), - ("user10", 0, 90), - ("user11", 500, 0), - ("user12", 10000_000000, 0), - ]; - - let default_allocation_params = AllocationParams { - amount: Uint128::zero(), - unlock_schedule: Schedule { - start_time: 12_345, - cliff: 5, - duration: 500, - }, - proposed_receiver: None, - }; - - let locked_balances = vec![ - ( - "user1".to_string(), - AllocationParams { - amount: Uint128::from(80u32), - ..default_allocation_params.clone() - }, - ), - ( - "user4".to_string(), - AllocationParams { - amount: Uint128::from(50u32), - ..default_allocation_params.clone() - }, - ), - ( - "user7".to_string(), - AllocationParams { - amount: Uint128::from(100u32), - ..default_allocation_params.clone() - }, - ), - ( - "user10".to_string(), - AllocationParams { - amount: Uint128::from(30u32), - ..default_allocation_params - }, - ), - ]; - - for (addr, xastro, vxastro) in balances { - if xastro > 0 { - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked(addr), - xastro, - ); - } - - if vxastro > 0 { - mint_vxastro( - &mut app, - &staking_instance, - xastro_addr.clone(), - &vxastro_addr, - Addr::unchecked(addr), - vxastro, - ); - } - } - - create_allocations(&mut app, token_addr, builder_unlock_addr, locked_balances); - - // Skip period - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create default proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(PROPOSAL_VOTING_PERIOD + 1000), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: Some(vec![ - "https://some1.link/".to_string(), - "https://some2.link/".to_string(), - ]), - whitelist_remove: Some(vec!["https://some.link/".to_string()]), - }))) - .unwrap(), - funds: vec![], - })]), - ); - - let votes: Vec<(&str, ProposalVoteOption, u128)> = vec![ - ("user1", ProposalVoteOption::For, 280u128), - ("user2", ProposalVoteOption::For, 350u128), - ("user3", ProposalVoteOption::For, 550u128), - ("user4", ProposalVoteOption::For, 350u128), - ("user5", ProposalVoteOption::For, 240u128), - ("user6", ProposalVoteOption::For, 600u128), - ("user7", ProposalVoteOption::For, 130u128), - ("user8", ProposalVoteOption::Against, 330u128), - ("user9", ProposalVoteOption::Against, 50u128), - ("user10", ProposalVoteOption::Against, 270u128), - ("user11", ProposalVoteOption::Against, 500u128), - ("user12", ProposalVoteOption::For, 10000_000000u128), - ]; - - check_total_vp(&mut app, &assembly_addr, 1, 20000003650); - - for (addr, option, expected_vp) in votes { - let sender = Addr::unchecked(addr); - - check_user_vp(&mut app, &assembly_addr, &sender, 1, expected_vp); - - cast_vote(&mut app, assembly_addr.clone(), 1, sender, option).unwrap(); - } - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - let proposal_votes: ProposalVotesResponse = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::ProposalVotes { proposal_id: 1 }, - ) - .unwrap(); - - let proposal_for_voters: Vec = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::ProposalVoters { - proposal_id: 1, - vote_option: ProposalVoteOption::For, - start: None, - limit: None, - }, - ) - .unwrap(); - - let proposal_against_voters: Vec = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::ProposalVoters { - proposal_id: 1, - vote_option: ProposalVoteOption::Against, - start: None, - limit: None, - }, - ) - .unwrap(); - - // Check proposal votes - assert_eq!(proposal.for_power, Uint128::from(10000002500u128)); - assert_eq!(proposal.against_power, Uint128::from(1150u32)); - - assert_eq!(proposal_votes.for_power, Uint128::from(10000002500u128)); - assert_eq!(proposal_votes.against_power, Uint128::from(1150u32)); - - assert_eq!( - proposal_for_voters, - vec![ - Addr::unchecked("user1"), - Addr::unchecked("user2"), - Addr::unchecked("user3"), - Addr::unchecked("user4"), - Addr::unchecked("user5"), - Addr::unchecked("user6"), - Addr::unchecked("user7"), - Addr::unchecked("user12"), - ] - ); - assert_eq!( - proposal_against_voters, - vec![ - Addr::unchecked("user8"), - Addr::unchecked("user9"), - Addr::unchecked("user10"), - Addr::unchecked("user11") - ] - ); - - // Skip voting period - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); - }); - - // Try to vote after voting period - let err = cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user11"), - ProposalVoteOption::Against, - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Voting period ended!"); - - // Try to execute the proposal before end_proposal - let err = app - .execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Proposal not passed!"); - - // Check the successful completion of the proposal - check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - check_token_balance( - &mut app, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.status, ProposalStatus::Passed); - - // Try to end proposal again - let err = app - .execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Proposal not active!"); - - // Try to execute the proposal before the delay - let err = app - .execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Proposal delay not ended!"); - - // Skip blocks - app.update_block(|bi| { - bi.height += PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - // Try to execute the proposal after the delay - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::ExecuteProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let config: Config = app - .wrap() - .query_wasm_smart(assembly_addr.to_string(), &QueryMsg::Config {}) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.to_string(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - // Check execution result - assert_eq!(config.proposal_voting_period, PROPOSAL_VOTING_PERIOD + 1000); - assert_eq!( - config.whitelisted_links, - vec![ - "https://some1.link/".to_string(), - "https://some2.link/".to_string(), - ] - ); - assert_eq!(proposal.status, ProposalStatus::Executed); - - // Try to remove proposal before expiration period - let err = app - .execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Proposal not completed!"); - - // Remove expired proposal - app.update_block(|bi| { - bi.height += PROPOSAL_EXPIRATION_PERIOD + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + 1)); - }); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let res: ProposalListResponse = app - .wrap() - .query_wasm_smart( - assembly_addr.to_string(), - &QueryMsg::Proposals { - start: None, - limit: None, - }, - ) - .unwrap(); - - assert_eq!(res.proposal_list, vec![]); - // proposal_count should not be changed after removing a proposal - assert_eq!(res.proposal_count, Uint64::from(1u32)); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_successful_emissions_proposal() { - use cosmwasm_std::{coins, BankMsg}; - - let mut app = mock_app(); - let owner = Addr::unchecked("generator_controller"); - - let (_, _, _, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, true, false); - - // Provide some funds to the Assembly contract to use in the proposal messages - app.init_modules(|router, _, storage| { - router.bank.init_balance( - storage, - &Addr::unchecked(assembly_addr.clone()), - coins(1000, "uluna"), - ) - }) - .unwrap(); - - let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { - title: "Emissions Test title".to_string(), - description: "Emissions Test description".to_string(), - // Sample message to use as we don't have IBC or the Generator to set emissions on - messages: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: "generator_controller".into(), - amount: coins(1, "uluna"), - })], - ibc_channel: None, - }; - - app.execute_contract( - Addr::unchecked("generator_controller"), - assembly_addr.clone(), - &emissions_proposal_msg, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart(assembly_addr, &QueryMsg::Proposal { proposal_id: 1 }) - .unwrap(); - - assert_eq!(proposal.status, ProposalStatus::Executed); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_no_generator_controller_emissions_proposal() { - let mut app = mock_app(); - let owner = Addr::unchecked("generator_controller"); - - let (_, _, _, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, false, false); - let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { - title: "Emissions Test title!".to_string(), - description: "Emissions Test description!".to_string(), - messages: vec![], - ibc_channel: None, - }; - - let err = app - .execute_contract( - Addr::unchecked("generator_controller"), - assembly_addr, - &emissions_proposal_msg, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Sender is not the Generator controller installed in the assembly" - ); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_empty_messages_emissions_proposal() { - let mut app = mock_app(); - let owner = Addr::unchecked("generator_controller"); - - let (_, _, _, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, true, false); - let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { - title: "Emissions Test title!".to_string(), - description: "Emissions Test description!".to_string(), - messages: vec![], - ibc_channel: None, - }; - - let err = app - .execute_contract( - Addr::unchecked("generator_controller"), - assembly_addr, - &emissions_proposal_msg, - &[], - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "The proposal has no messages to execute" - ); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_unauthorised_emissions_proposal() { - use cosmwasm_std::BankMsg; - - let mut app = mock_app(); - let owner = Addr::unchecked("generator_controller"); - - let (_, _, _, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, true, false); - let emissions_proposal_msg = ExecuteMsg::ExecuteEmissionsProposal { - title: "Emissions Test title!".to_string(), - description: "Emissions Test description!".to_string(), - // Sample message to use as we don't have IBC or the Generator to set emissions on - messages: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: "generator_controller".into(), - amount: coins(1, "uluna"), - })], - ibc_channel: None, - }; - - let err = app - .execute_contract( - Addr::unchecked("not_generator_controller"), - assembly_addr, - &emissions_proposal_msg, - &[], - ) - .unwrap_err(); - - assert_eq!(err.root_cause().to_string(), "Unauthorized"); -} - -#[test] -fn test_voting_power_changes() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, false, false); - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - // Mint tokens for casting votes at start block - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user1"), - 40000_000000, - ); - - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user2"), - 5000_000000, - ); - - app.update_block(next_block); - - // user1 can vote as he had voting power before the proposal submitting. - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user1"), - ProposalVoteOption::For, - ) - .unwrap(); - // Should panic, because user2 doesn't have any voting power. - let err = cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user2"), - ProposalVoteOption::Against, - ) - .unwrap_err(); - - // user2 doesn't have voting power and doesn't affect on total voting power(total supply at) - // total supply = 5000 - assert_eq!( - err.root_cause().to_string(), - "You don't have any voting power!" - ); - - app.update_block(next_block); - - // Skip voting period and delay - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi - .time - .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - // End proposal - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - // Check proposal votes - assert_eq!(proposal.for_power, Uint128::from(40000_000000u128)); - assert_eq!(proposal.against_power, Uint128::zero()); - // Should be passed, as total_voting_power=5000, for_votes=40000. - // So user2 didn't affect the result. Because he had to have xASTRO before the vote was submitted. - assert_eq!(proposal.status, ProposalStatus::Passed); -} - -#[test] -fn test_fail_outpost_vote_without_hub() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, false, false); - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - // Mint tokens for casting votes at start block - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user1"), - 40000_000000, - ); - - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - // Mint user2's tokens at the same block to increase total supply and add voting power to try to cast vote. - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user2"), - 5000_000000, - ); - - app.update_block(next_block); - - // user1 can not vote from an Outpost due to no Hub contract set - let err = cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("invalid_contract"), - Addr::unchecked("user1"), - ProposalVoteOption::For, - Uint128::from(100u64), - ) - .unwrap_err(); - - assert_eq!( - err.root_cause().to_string(), - "Sender is not the Hub installed in the assembly" - ); -} - -#[test] -fn test_outpost_vote() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner.clone(), false, false, true); - - let user1_voting_power = 10_000_000_000; - let user2_voting_power = 5_000_000_000; - let remote_user1_voting_power = 80_000_000_000u128; - let remote_user2_voting_power = 3_000_000_000u128; - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - // Mint tokens for casting votes at start block - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user1"), - user1_voting_power, - ); - - // Mint tokens for casting votes against vote at start block - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user2"), - user2_voting_power, - ); - - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - - app.update_block(next_block); - - // Outpost votes won't be accepted from other addresses - let err = cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("other_contract"), - Addr::unchecked("remote1"), - ProposalVoteOption::For, - Uint128::from(remote_user1_voting_power), - ) - .unwrap_err(); - assert_eq!(err.root_cause().to_string(), "Unauthorized"); - - cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - owner.clone(), - Addr::unchecked("remote1"), - ProposalVoteOption::For, - Uint128::from(remote_user1_voting_power), - ) - .unwrap(); - - cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - owner.clone(), - Addr::unchecked("remote2"), - ProposalVoteOption::Against, - Uint128::from(remote_user2_voting_power), - ) - .unwrap(); - - let err = cast_outpost_vote( - &mut app, - assembly_addr.clone(), - 1, - owner, - Addr::unchecked("remote1"), - ProposalVoteOption::For, - Uint128::from(remote_user2_voting_power), - ) - .unwrap_err(); - assert_eq!(err.root_cause().to_string(), "User already voted!"); - - // user1 can vote as he had voting power before the proposal submitting. - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user1"), - ProposalVoteOption::For, - ) - .unwrap(); - - let err = cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user1"), - ProposalVoteOption::For, - ) - .unwrap_err(); - assert_eq!(err.root_cause().to_string(), "User already voted!"); - - // user2 can vote as he had voting power before the proposal submitting. - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked("user2"), - ProposalVoteOption::Against, - ) - .unwrap(); - - app.update_block(next_block); - - // Skip voting period and delay - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi - .time - .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - // End proposal - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - // Check proposal votes, Outpost and Hub votes should be counted - let total_for_voting_power = user1_voting_power + remote_user1_voting_power; - let total_against_voting_power = user2_voting_power + remote_user2_voting_power; - assert_eq!(proposal.for_power, Uint128::from(total_for_voting_power)); - assert_eq!( - proposal.against_power, - Uint128::from(total_against_voting_power) - ); - // Should be passed - assert_eq!(proposal.status, ProposalStatus::Passed); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_block_height_selection() { - // Block height is 12345 after app initialization - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - let user1 = Addr::unchecked("user1"); - let user2 = Addr::unchecked("user2"); - let user3 = Addr::unchecked("user3"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, false, false); - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user1, - 6000_000001, - ); - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user2, - 4000_000000, - ); - - // Skip to the next period - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - None, - ); - - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - user1, - ProposalVoteOption::For, - ) - .unwrap(); - - // Mint huge amount of xASTRO. These tokens cannot affect on total supply in proposal 1 because - // they were minted after proposal.start_block - 1 - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user3, - 100000_000000, - ); - // Mint more xASTRO to user2, who will vote against the proposal, what is enough to make proposal unsuccessful. - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &user2, - 3000_000000, - ); - // Total voting power should be 20k xASTRO (proposal minimum deposit 10k + 4k + 6k users VP) - check_total_vp(&mut app, &assembly_addr, 1, 20000_000001); - - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - user2, - ProposalVoteOption::Against, - ) - .unwrap(); - - // Skip voting period - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi - .time - .plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - // End proposal - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.for_power, Uint128::new(6000_000001)); - // Against power is 4000, as user2's balance was increased after proposal.start_block - 1 - // at which everyone's voting power are considered. - assert_eq!(proposal.against_power, Uint128::new(4000_000000)); - // Proposal is passed, as the total supply was increased after proposal.start_block - 1. - assert_eq!(proposal.status, ProposalStatus::Passed); -} - -#[cfg(not(feature = "testnet"))] -#[test] -fn test_unsuccessful_proposal() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, _, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, false, false); - - // Init voting power for users - let xastro_balances: Vec<(&str, u128)> = vec![ - ("user0", PROPOSAL_REQUIRED_DEPOSIT), // proposal submitter - ("user1", 100), - ("user2", 200), - ("user3", 400), - ("user4", 250), - ("user5", 90), - ("user6", 300), - ("user7", 30), - ("user8", 180), - ("user9", 50), - ("user10", 90), - ("user11", 500), - ]; - - for (addr, xastro) in xastro_balances { - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked(addr), - xastro, - ); - } - - // Skip period - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - None, - ); - - let expected_voting_power: Vec<(&str, ProposalVoteOption)> = vec![ - ("user1", ProposalVoteOption::For), - ("user2", ProposalVoteOption::For), - ("user3", ProposalVoteOption::For), - ("user4", ProposalVoteOption::Against), - ("user5", ProposalVoteOption::Against), - ("user6", ProposalVoteOption::Against), - ("user7", ProposalVoteOption::Against), - ("user8", ProposalVoteOption::Against), - ("user9", ProposalVoteOption::Against), - ("user10", ProposalVoteOption::Against), - ]; - - for (addr, option) in expected_voting_power { - cast_vote( - &mut app, - assembly_addr.clone(), - 1, - Addr::unchecked(addr), - option, - ) - .unwrap(); - } - - // Skip voting period - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); - }); - - // Check balance of submitter before and after proposal completion - check_token_balance(&mut app, &xastro_addr, &Addr::unchecked("user0"), 0); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - check_token_balance( - &mut app, - &xastro_addr, - &Addr::unchecked("user0"), - 10000_000000, - ); - - // Check proposal status - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.status, ProposalStatus::Rejected); - - // Remove expired proposal - app.update_block(|bi| { - bi.height += PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1; - bi.time = bi - .time - .plus_seconds(5 * (PROPOSAL_EXPIRATION_PERIOD + PROPOSAL_EFFECTIVE_DELAY + 1)); - }); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::RemoveCompletedProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let res: ProposalListResponse = app - .wrap() - .query_wasm_smart( - assembly_addr.to_string(), - &QueryMsg::Proposals { - start: None, - limit: None, - }, - ) - .unwrap(); - - assert_eq!(res.proposal_list, vec![]); - // proposal_count should not be changed after removing - assert_eq!(res.proposal_count, Uint64::from(1u32)); -} - -#[test] -fn test_check_messages() { - let mut app = mock_app(); - let owner = Addr::unchecked("owner"); - let (_, _, _, vxastro_addr, _, assembly_addr, _) = - instantiate_contracts(&mut app, owner, false, false, false); - - change_owner(&mut app, &vxastro_addr, &assembly_addr); - let user = Addr::unchecked("user"); - let into_check_msg = |msgs: Vec<(String, Binary)>| { - let messages = msgs - .into_iter() - .map(|(contract_addr, msg)| { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr, - msg, - funds: vec![], - }) - }) - .collect(); - ExecuteMsg::CheckMessages { messages } - }; - - let config_before: astroport_governance::voting_escrow::ConfigResponse = app - .wrap() - .query_wasm_smart( - &vxastro_addr, - &astroport_governance::voting_escrow::QueryMsg::Config {}, - ) - .unwrap(); - - let vxastro_blacklist_msg = vec![( - vxastro_addr.to_string(), - to_json_binary( - &astroport_governance::voting_escrow::ExecuteMsg::UpdateConfig { new_guardian: None }, - ) - .unwrap(), - )]; - let err = app - .execute_contract( - user, - assembly_addr.clone(), - &into_check_msg(vxastro_blacklist_msg), - &[], - ) - .unwrap_err(); - assert_eq!( - &err.root_cause().to_string(), - "Messages check passed. Nothing was committed to the blockchain" - ); - - let config_after: astroport_governance::voting_escrow::ConfigResponse = app - .wrap() - .query_wasm_smart( - &vxastro_addr, - &astroport_governance::voting_escrow::QueryMsg::Config {}, - ) - .unwrap(); - assert_eq!(config_before, config_after); -} - -#[test] -fn test_delegated_vp() { - let mut app = mock_app(); - - let owner = Addr::unchecked("owner"); - - let (_, staking_instance, xastro_addr, vxastro_addr, _, assembly_addr, delegator) = - instantiate_contracts(&mut app, owner, true, false, false); - let delegator = delegator.unwrap(); - - let users = vec![ - ( - "user1", - 103_000_000_000u128, - 1000u16, - "user4", - 177_278_846_150u128, - ), - ( - "user2", - 612_000_000_000u128, - 2000u16, - "user5", - 1_053_346_153_800u128, - ), - ( - "user3", - 205_000_000_000u128, - 3000u16, - "user6", - 352_836_538_450u128, - ), - ]; - - // Mint tokens for submitting proposal - mint_tokens( - &mut app, - &staking_instance, - &xastro_addr, - &Addr::unchecked("user0"), - PROPOSAL_REQUIRED_DEPOSIT, - ); - - // Mint vxASTRO and delegate it to the other users - for (from, amount, bps, to, exp_vp) in users { - mint_vxastro( - &mut app, - &staking_instance, - xastro_addr.clone(), - &vxastro_addr, - Addr::unchecked(from), - amount, - ); - delegate_vxastro( - &mut app, - delegator.clone(), - Addr::unchecked(from), - Addr::unchecked(to), - bps, - ); - - let from_amount: Uint128 = app - .wrap() - .query_wasm_smart( - &delegator, - &DelegatorQueryMsg::AdjustedBalance { - account: from.to_string(), - timestamp: None, - }, - ) - .unwrap(); - - let to_amount: Uint128 = app - .wrap() - .query_wasm_smart( - &delegator, - &DelegatorQueryMsg::AdjustedBalance { - account: to.to_string(), - timestamp: None, - }, - ) - .unwrap(); - - assert_eq!(from_amount + to_amount, Uint128::from(exp_vp)); - } - - app.update_block(|mut block| { - block.time = block.time.plus_seconds(WEEK); - block.height += WEEK / 5; - }); - - // Create proposal - create_proposal( - &mut app, - &xastro_addr, - &assembly_addr, - Addr::unchecked("user0"), - Some(vec![CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: assembly_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_token_addr: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, - ibc_controller: None, - generator_controller: None, - hub: None, - builder_unlock_addr: None, - proposal_voting_period: Some(750), - proposal_effective_delay: None, - proposal_expiration_period: None, - proposal_required_deposit: None, - proposal_required_quorum: None, - proposal_required_threshold: None, - whitelist_add: None, - whitelist_remove: None, - }))) - .unwrap(), - funds: vec![], - })]), - ); - - let votes: Vec<(&str, ProposalVoteOption)> = vec![ - ("user1", ProposalVoteOption::Against), - ("user2", ProposalVoteOption::For), - ("user3", ProposalVoteOption::Against), - ("user4", ProposalVoteOption::For), - ("user5", ProposalVoteOption::Against), - ("user6", ProposalVoteOption::For), - ]; - - for (user, vote) in votes { - cast_vote( - &mut app, - assembly_addr.clone(), - 1u64, - Addr::unchecked(user), - vote, - ) - .unwrap(); - } - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.for_power, Uint128::from(1_578_255_769_188u128)); - assert_eq!(proposal.against_power, Uint128::from(925_205_769_212u128)); - - // Skip voting period - app.update_block(|bi| { - bi.height += PROPOSAL_VOTING_PERIOD + 1; - bi.time = bi.time.plus_seconds(5 * (PROPOSAL_VOTING_PERIOD + 1)); - }); - - app.execute_contract( - Addr::unchecked("user0"), - assembly_addr.clone(), - &ExecuteMsg::EndProposal { proposal_id: 1 }, - &[], - ) - .unwrap(); - - let proposal: Proposal = app - .wrap() - .query_wasm_smart( - assembly_addr.clone(), - &QueryMsg::Proposal { proposal_id: 1 }, - ) - .unwrap(); - - assert_eq!(proposal.status, ProposalStatus::Passed); -} - -fn mock_app() -> App { - let mut env = mock_env(); - env.block.time = Timestamp::from_seconds(EPOCH_START); - let api = MockApi::default(); - let bank = BankKeeper::new(); - let storage = MockStorage::new(); - - AppBuilder::new() - .with_api(api) - .with_block(env.block) - .with_bank(bank) - .with_storage(storage) - .build(|_, _, _| {}) -} - -fn instantiate_contracts( - router: &mut App, - owner: Addr, - with_delegator: bool, - with_generator_controller: bool, - with_hub: bool, -) -> (Addr, Addr, Addr, Addr, Addr, Addr, Option) { - let token_addr = instantiate_astro_token(router, &owner); - let (staking_addr, xastro_token_addr) = instantiate_xastro_token(router, &owner, &token_addr); - let vxastro_token_addr = instantiate_vxastro_token(router, &owner, &xastro_token_addr); - let builder_unlock_addr = instantiate_builder_unlock_contract(router, &owner, &token_addr); - - let mut delegator_addr = None; - - if with_delegator { - delegator_addr = Some(instantiate_delegator_contract( - router, - &owner, - &vxastro_token_addr, - )); - } - - // If we want to test immediate proposals we need to set the address - // for the generator controller. Deploying the generator controller in this - // test would require deploying factory, tokens and pools. That test is - // better suited in the generator controller itself. Thus, we use the owner - // address as the generator controller address to test immediate proposals. - let mut generator_controller_addr = None; - - if with_generator_controller { - generator_controller_addr = Some(owner.to_string()); - } - - let mut hub_addr = None; - - if with_hub { - hub_addr = Some(owner.to_string()); - } - - let assembly_addr = instantiate_assembly_contract( - router, - &owner, - &xastro_token_addr, - &vxastro_token_addr, - &builder_unlock_addr, - delegator_addr.clone().map(String::from), - generator_controller_addr, - hub_addr, - ); - - ( - token_addr, - staking_addr, - xastro_token_addr, - vxastro_token_addr, - builder_unlock_addr, - assembly_addr, - delegator_addr, - ) -} - -fn instantiate_astro_token(router: &mut App, owner: &Addr) -> Addr { - let astro_token_contract = Box::new(ContractWrapper::new_with_empty( - astroport_token::contract::execute, - astroport_token::contract::instantiate, - astroport_token::contract::query, - )); - - let astro_token_code_id = router.store_code(astro_token_contract); - - let msg = TokenInstantiateMsg { - name: String::from("Astro token"), - symbol: String::from("ASTRO"), - decimals: 6, - initial_balances: vec![], - mint: Some(MinterResponse { - minter: owner.to_string(), - cap: None, - }), - marketing: None, - }; - - router - .instantiate_contract( - astro_token_code_id, - owner.clone(), - &msg, - &[], - String::from("ASTRO"), - None, - ) - .unwrap() -} - -fn instantiate_xastro_token(router: &mut App, owner: &Addr, astro_token: &Addr) -> (Addr, Addr) { - let xastro_contract = Box::new(ContractWrapper::new_with_empty( - astroport_xastro_token::contract::execute, - astroport_xastro_token::contract::instantiate, - astroport_xastro_token::contract::query, - )); - - let xastro_code_id = router.store_code(xastro_contract); - - let staking_contract = Box::new( - ContractWrapper::new_with_empty( - astroport_staking::contract::execute, - astroport_staking::contract::instantiate, - astroport_staking::contract::query, - ) - .with_reply_empty(astroport_staking::contract::reply), - ); - - let staking_code_id = router.store_code(staking_contract); - - let msg = astroport::staking::InstantiateMsg { - owner: owner.to_string(), - token_code_id: xastro_code_id, - deposit_token_addr: astro_token.to_string(), - marketing: None, - }; - let staking_instance = router - .instantiate_contract( - staking_code_id, - owner.clone(), - &msg, - &[], - String::from("xASTRO"), - None, - ) - .unwrap(); - - let res = router - .wrap() - .query::(&QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: staking_instance.to_string(), - msg: to_json_binary(&astroport::staking::QueryMsg::Config {}).unwrap(), - })) - .unwrap(); - - (staking_instance, res.share_token_addr) -} - -fn instantiate_vxastro_token(router: &mut App, owner: &Addr, xastro: &Addr) -> Addr { - let vxastro_token_contract = Box::new(ContractWrapper::new_with_empty( - voting_escrow::contract::execute, - voting_escrow::contract::instantiate, - voting_escrow::contract::query, - )); - - let vxastro_token_code_id = router.store_code(vxastro_token_contract); - - let msg = VXAstroInstantiateMsg { - owner: owner.to_string(), - guardian_addr: Some(owner.to_string()), - deposit_token_addr: xastro.to_string(), - marketing: None, - logo_urls_whitelist: vec![], - }; - - router - .instantiate_contract( - vxastro_token_code_id, - owner.clone(), - &msg, - &[], - String::from("vxASTRO"), - None, - ) - .unwrap() -} - -fn instantiate_builder_unlock_contract(router: &mut App, owner: &Addr, astro_token: &Addr) -> Addr { - let builder_unlock_contract = Box::new(ContractWrapper::new_with_empty( - builder_unlock::contract::execute, - builder_unlock::contract::instantiate, - builder_unlock::contract::query, - )); - - let builder_unlock_code_id = router.store_code(builder_unlock_contract); - - let msg = BuilderUnlockInstantiateMsg { - owner: owner.to_string(), - astro_token: astro_token.to_string(), - max_allocations_amount: Uint128::new(300_000_000_000_000u128), - }; - - router - .instantiate_contract( - builder_unlock_code_id, - owner.clone(), - &msg, - &[], - "Builder Unlock contract".to_string(), - Some(owner.to_string()), - ) - .unwrap() -} - -#[allow(clippy::too_many_arguments)] -fn instantiate_assembly_contract( - router: &mut App, - owner: &Addr, - xastro: &Addr, - vxastro: &Addr, - builder: &Addr, - delegator: Option, - generator_controller_addr: Option, - hub_addr: Option, -) -> Addr { - let assembly_contract = Box::new(ContractWrapper::new_with_empty( - astro_assembly::contract::execute, - astro_assembly::contract::instantiate, - astro_assembly::contract::query, - )); - - let assembly_code = router.store_code(assembly_contract); - - let msg = InstantiateMsg { - xastro_token_addr: xastro.to_string(), - vxastro_token_addr: Some(vxastro.to_string()), - voting_escrow_delegator_addr: delegator, - ibc_controller: None, - generator_controller_addr, - hub_addr, - builder_unlock_addr: builder.to_string(), - proposal_voting_period: PROPOSAL_VOTING_PERIOD, - proposal_effective_delay: PROPOSAL_EFFECTIVE_DELAY, - proposal_expiration_period: PROPOSAL_EXPIRATION_PERIOD, - proposal_required_deposit: Uint128::new(PROPOSAL_REQUIRED_DEPOSIT), - proposal_required_quorum: String::from(PROPOSAL_REQUIRED_QUORUM), - proposal_required_threshold: String::from(PROPOSAL_REQUIRED_THRESHOLD), - whitelisted_links: vec!["https://some.link/".to_string()], - }; - - router - .instantiate_contract( - assembly_code, - owner.clone(), - &msg, - &[], - "Assembly".to_string(), - Some(owner.to_string()), - ) - .unwrap() -} - -fn instantiate_delegator_contract(router: &mut App, owner: &Addr, vxastro: &Addr) -> Addr { - let nft_contract = Box::new(ContractWrapper::new_with_empty( - astroport_nft::contract::execute, - astroport_nft::contract::instantiate, - astroport_nft::contract::query, - )); - - let nft_code_id = router.store_code(nft_contract); - - let delegator_contract = Box::new( - ContractWrapper::new_with_empty( - voting_escrow_delegation::contract::execute, - voting_escrow_delegation::contract::instantiate, - voting_escrow_delegation::contract::query, - ) - .with_reply_empty(voting_escrow_delegation::contract::reply), - ); - - let delegator_code_id = router.store_code(delegator_contract); - - let msg = DelegatorInstantiateMsg { - owner: owner.to_string(), - nft_code_id, - voting_escrow_addr: vxastro.to_string(), - }; - - router - .instantiate_contract( - delegator_code_id, - owner.clone(), - &msg, - &[], - "Voting Escrow Delegator", - Some(owner.to_string()), - ) - .unwrap() -} - -fn mint_tokens(app: &mut App, minter: &Addr, token: &Addr, recipient: &Addr, amount: u128) { - let msg = Cw20ExecuteMsg::Mint { - recipient: recipient.to_string(), - amount: Uint128::from(amount), - }; - - app.execute_contract(minter.clone(), token.to_owned(), &msg, &[]) - .unwrap(); -} - -fn mint_vxastro( - app: &mut App, - staking_instance: &Addr, - xastro: Addr, - vxastro: &Addr, - recipient: Addr, - amount: u128, -) { - mint_tokens(app, staking_instance, &xastro, &recipient, amount); - - let msg = Cw20ExecuteMsg::Send { - contract: vxastro.to_string(), - amount: Uint128::from(amount), - msg: to_json_binary(&VXAstroCw20HookMsg::CreateLock { time: WEEK * 50 }).unwrap(), - }; - - app.execute_contract(recipient, xastro, &msg, &[]).unwrap(); -} - -fn delegate_vxastro(app: &mut App, delegator_addr: Addr, from: Addr, to: Addr, bps: u16) { - let msg = DelegatorExecuteMsg::CreateDelegation { - bps, - expire_time: 2 * 7 * 86400, - token_id: format!("{}-{}-{}", from, to, bps), - recipient: to.to_string(), - }; - - app.execute_contract(from, delegator_addr, &msg, &[]) - .unwrap(); -} - -fn create_allocations( - app: &mut App, - token: Addr, - builder_unlock_contract_addr: Addr, - allocations: Vec<(String, AllocationParams)>, -) { - let amount = allocations - .iter() - .map(|params| params.1.amount.u128()) - .sum(); - - mint_tokens( - app, - &Addr::unchecked("owner"), - &token, - &Addr::unchecked("owner"), - amount, - ); - - app.execute_contract( - Addr::unchecked("owner"), - Addr::unchecked(token.to_string()), - &Cw20ExecuteMsg::Send { - contract: builder_unlock_contract_addr.to_string(), - amount: Uint128::from(amount), - msg: to_json_binary(&BuilderUnlockReceiveMsg::CreateAllocations { allocations }).unwrap(), - }, - &[], - ) - .unwrap(); -} - -fn create_proposal( - app: &mut App, - token: &Addr, - assembly: &Addr, - submitter: Addr, - msgs: Option>, -) { - let submit_proposal_msg = Cw20HookMsg::SubmitProposal { - title: "Test title!".to_string(), - description: "Test description!".to_string(), - link: None, - messages: msgs, - ibc_channel: None, - }; - - app.execute_contract( - submitter, - token.clone(), - &Cw20ExecuteMsg::Send { - contract: assembly.to_string(), - amount: Uint128::from(PROPOSAL_REQUIRED_DEPOSIT), - msg: to_json_binary(&submit_proposal_msg).unwrap(), - }, - &[], - ) - .unwrap(); -} - -fn check_token_balance(app: &mut App, token: &Addr, address: &Addr, expected: u128) { - let msg = XAstroQueryMsg::Balance { - address: address.to_string(), - }; - let res: StdResult = app.wrap().query_wasm_smart(token, &msg); - assert_eq!(res.unwrap().balance, Uint128::from(expected)); -} - -fn check_user_vp(app: &mut App, assembly: &Addr, address: &Addr, proposal_id: u64, expected: u128) { - let res: Uint128 = app - .wrap() - .query_wasm_smart( - assembly.to_string(), - &QueryMsg::UserVotingPower { - user: address.to_string(), - proposal_id, - }, - ) - .unwrap(); - - assert_eq!(res.u128(), expected); -} - -fn check_total_vp(app: &mut App, assembly: &Addr, proposal_id: u64, expected: u128) { - let res: Uint128 = app - .wrap() - .query_wasm_smart( - assembly.to_string(), - &QueryMsg::TotalVotingPower { proposal_id }, - ) - .unwrap(); - - assert_eq!(res.u128(), expected); -} - -fn cast_vote( - app: &mut App, - assembly: Addr, - proposal_id: u64, - sender: Addr, - option: ProposalVoteOption, -) -> anyhow::Result { - app.execute_contract( - sender, - assembly, - &ExecuteMsg::CastVote { - proposal_id, - vote: option, - }, - &[], - ) -} - -fn cast_outpost_vote( - app: &mut App, - assembly: Addr, - proposal_id: u64, - sender: Addr, - voter: Addr, - option: ProposalVoteOption, - voting_power: Uint128, -) -> anyhow::Result { - app.execute_contract( - sender, - assembly, - &ExecuteMsg::CastOutpostVote { - proposal_id, - voter: voter.to_string(), - vote: option, - voting_power, - }, - &[], - ) -} - -fn change_owner(app: &mut App, contract: &Addr, assembly: &Addr) { - let msg = astroport_governance::voting_escrow::ExecuteMsg::ProposeNewOwner { - new_owner: assembly.to_string(), - expires_in: 100, - }; - app.execute_contract(Addr::unchecked("owner"), contract.clone(), &msg, &[]) - .unwrap(); - - app.execute_contract( - assembly.clone(), - contract.clone(), - &astroport_governance::voting_escrow::ExecuteMsg::ClaimOwnership {}, - &[], - ) - .unwrap(); -} diff --git a/packages/astroport-governance/src/utils.rs b/packages/astroport-governance/src/utils.rs index aeea36b3..ec0f261f 100644 --- a/packages/astroport-governance/src/utils.rs +++ b/packages/astroport-governance/src/utils.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - Addr, Decimal, Fraction, IbcQuery, ListChannelsResponse, OverflowError, QuerierWrapper, - QueryRequest, StdError, StdResult, Uint128, Uint256, Uint64, + Addr, ChannelResponse, Decimal, Fraction, IbcQuery, OverflowError, QuerierWrapper, StdError, + StdResult, Uint128, Uint256, Uint64, }; use crate::hub::HubBalance; @@ -139,19 +139,20 @@ pub fn check_contract_supports_channel( querier: QuerierWrapper, contract: &Addr, given_channel: &String, -) -> Result<(), StdError> { +) -> StdResult<()> { let port_id = Some(format!("wasm.{contract}")); - let ListChannelsResponse { channels } = - querier.query(&QueryRequest::Ibc(IbcQuery::ListChannels { port_id }))?; - channels - .iter() - .find(|channel| &channel.endpoint.channel_id == given_channel) - .map(|_| ()) - .ok_or_else(|| { - StdError::generic_err(format!( - "The contract does not have channel {given_channel}" - )) - }) + let ChannelResponse { channel } = querier.query( + &IbcQuery::Channel { + channel_id: given_channel.to_string(), + port_id, + } + .into(), + )?; + channel.map(|_| ()).ok_or_else(|| { + StdError::generic_err(format!( + "The contract does not have channel {given_channel}" + )) + }) } /// Retrieves the total amount of voting power held by all Outposts at a given time From 5f0c8cf0c3b2156216d73f027180193882775f87 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 30 Jan 2024 20:22:52 +0400 Subject: [PATCH 25/47] remove interchain governance and vxastro logic from Assembly don't be upset we'll add them after we move the hub --- Cargo.lock | 66 ---- contracts/assembly/Cargo.toml | 4 - contracts/assembly/src/contract.rs | 307 +----------------- contracts/assembly/src/unit_tests.rs | 20 -- contracts/assembly/src/utils.rs | 62 +--- contracts/assembly/tests/common/helper.rs | 58 +--- contracts/assembly/tests/integration.rs | 24 -- packages/astroport-governance/src/assembly.rs | 59 ---- 8 files changed, 7 insertions(+), 593 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e0e6639..c73a195d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,11 +26,8 @@ dependencies = [ "anyhow", "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", "astroport-governance 1.4.0", - "astroport-hub", - "astroport-nft", "astroport-staking 2.0.0", "astroport-tokenfactory-tracker", - "astroport-voting-escrow-lite", "builder-unlock", "cosmwasm-schema", "cosmwasm-std", @@ -42,7 +39,6 @@ dependencies = [ "osmosis-std", "test-case", "thiserror", - "voting-escrow-delegation", ] [[package]] @@ -217,18 +213,6 @@ dependencies = [ "cosmwasm-schema", ] -[[package]] -name = "astroport-nft" -version = "1.0.0" -dependencies = [ - "astroport-governance 1.4.0", - "cosmwasm-schema", - "cosmwasm-std", - "cw2 0.15.1", - "cw721", - "cw721-base", -] - [[package]] name = "astroport-outpost" version = "0.1.0" @@ -948,36 +932,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cw721" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20dfe04f86e5327956b559ffcc86d9a43167391f37402afd8bf40b0be16bee4d" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-utils 0.15.1", - "schemars", - "serde", -] - -[[package]] -name = "cw721-base" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c3ee3b669fc2a8094301a73fd7be97a7454d4df2650c33599f737e8f254d24" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw2 0.15.1", - "cw721", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "der" version = "0.6.1" @@ -2086,26 +2040,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "voting-escrow-delegation" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport-governance 1.4.0", - "astroport-nft", - "astroport-tests", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.15.1", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw2 0.15.1", - "cw721", - "cw721-base", - "proptest", - "thiserror", -] - [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 1fbf74a7..56737679 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -28,10 +28,6 @@ cw-utils = "1" [dev-dependencies] cw-multi-test = { git = "https://github.com/astroport-fi/cw-multi-test", branch = "feat/bank_with_send_hooks", features = ["cosmwasm_1_1"] } osmosis-std = "0.21" -astroport-hub = { path = "../hub" } -voting-escrow-lite = { package = "astroport-voting-escrow-lite", path = "../../contracts/voting_escrow_lite" } -voting-escrow-delegation = { path = "../voting_escrow_delegation" } -astroport-nft = { path = "../nft" } astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } astroport-tokenfactory-tracker = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } builder-unlock = { path = "../builder_unlock" } diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 1ca4fa43..6e5a25d6 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -4,7 +4,7 @@ use astroport::asset::addr_opt_validate; use astroport::staking; use cosmwasm_std::{ attr, coins, entry_point, wasm_execute, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, - Response, StdError, StdResult, SubMsg, Uint128, Uint64, + Response, StdError, SubMsg, Uint128, Uint64, }; use cw2::set_contract_version; use cw_utils::must_pay; @@ -14,9 +14,7 @@ use astroport_governance::assembly::{ helpers::validate_links, Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalStatus, ProposalVoteOption, UpdateConfig, }; -use astroport_governance::utils::{ - check_contract_supports_channel, get_total_outpost_voting_power_at, -}; +use astroport_governance::utils::check_contract_supports_channel; use crate::error::ContractError; use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_VOTERS}; @@ -54,14 +52,7 @@ pub fn instantiate( let config = Config { xastro_denom: staking_config.xastro_denom, xastro_denom_tracking: tracker_config.tracker_addr, - vxastro_token_addr: addr_opt_validate(deps.api, &msg.vxastro_token_addr)?, - voting_escrow_delegator_addr: addr_opt_validate( - deps.api, - &msg.voting_escrow_delegator_addr, - )?, ibc_controller: addr_opt_validate(deps.api, &msg.ibc_controller)?, - generator_controller: addr_opt_validate(deps.api, &msg.generator_controller_addr)?, - hub: addr_opt_validate(deps.api, &msg.hub_addr)?, builder_unlock_addr: deps.api.addr_validate(&msg.builder_unlock_addr)?, proposal_voting_period: msg.proposal_voting_period, proposal_effective_delay: msg.proposal_effective_delay, @@ -70,8 +61,6 @@ pub fn instantiate( proposal_required_quorum: Decimal::from_str(&msg.proposal_required_quorum)?, proposal_required_threshold: Decimal::from_str(&msg.proposal_required_threshold)?, whitelisted_links: msg.whitelisted_links, - // Guardian is set to None so that Assembly must explicitly allow it - guardian_addr: None, }; #[cfg(not(feature = "testnet"))] @@ -131,28 +120,8 @@ pub fn execute( ibc_channel, ), ExecuteMsg::CastVote { proposal_id, vote } => cast_vote(deps, env, info, proposal_id, vote), - ExecuteMsg::CastOutpostVote { - proposal_id, - voter, - vote, - voting_power, - } => cast_outpost_vote(deps, env, info, proposal_id, voter, vote, voting_power), ExecuteMsg::EndProposal { proposal_id } => end_proposal(deps, env, proposal_id), ExecuteMsg::ExecuteProposal { proposal_id } => execute_proposal(deps, env, proposal_id), - ExecuteMsg::ExecuteEmissionsProposal { - title, - description, - messages, - ibc_channel, - } => submit_execute_emissions_proposal( - deps, - env, - info, - title, - description, - messages, - ibc_channel, - ), ExecuteMsg::CheckMessages(messages) => check_messages(env, messages), ExecuteMsg::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}), ExecuteMsg::UpdateConfig(config) => update_config(deps, env, info, config), @@ -160,9 +129,6 @@ pub fn execute( proposal_id, status, } => update_ibc_proposal_status(deps, info, proposal_id, status), - ExecuteMsg::RemoveOutpostVotes { proposal_id } => { - remove_outpost_votes(deps, env, info, proposal_id) - } } } @@ -317,97 +283,6 @@ pub fn cast_vote( ])) } -/// Cast a vote on a proposal from an Outpost. -/// This is a special case of `cast_vote` that allows Outposts to forward votes on -/// behalf of their users. The Hub contract is the only one allowed to call this method. -/// -/// * **proposal_id** is the identifier of the proposal. -/// -/// * **voter** is the address of the voter on the Outpost. -/// -/// * **vote_option** contains the vote option. -/// -/// * **voting_power** contains the voting power applied to this vote. -pub fn cast_outpost_vote( - deps: DepsMut, - env: Env, - info: MessageInfo, - proposal_id: u64, - voter: String, - vote_option: ProposalVoteOption, - voting_power: Uint128, -) -> Result { - let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?; - - let config = CONFIG.load(deps.storage)?; - - // We only allow the Hub to submit votes on behalf of Outpost user - // The Hub is responsible for validating the Hub vote with the Outpost - let hub = match config.hub { - Some(hub) => hub, - None => return Err(ContractError::InvalidHub {}), - }; - - if info.sender != hub { - return Err(ContractError::Unauthorized {}); - } - - if proposal.status != ProposalStatus::Active { - return Err(ContractError::ProposalNotActive {}); - } - - if env.block.height > proposal.end_block { - return Err(ContractError::VotingPeriodEnded {}); - } - - if PROPOSAL_VOTERS.has(deps.storage, (proposal_id, voter.clone())) { - return Err(ContractError::UserAlreadyVoted {}); - } - - if voting_power.is_zero() { - return Err(ContractError::NoVotingPower {}); - } - - // Voting power provided is used as is from the Hub. Validation of the voting - // power is done by the Hub contract with the Outpost. - // We track voting power from Outposts separately as well so as to have a - // way to cancel votes should a vulnerability be found in IBC or the Hub/Outpost - // implementation - match vote_option { - ProposalVoteOption::For => { - proposal.for_power = proposal.for_power.checked_add(voting_power)?; - proposal.outpost_for_power = proposal.outpost_for_power.checked_add(voting_power)?; - } - ProposalVoteOption::Against => { - proposal.against_power = proposal.against_power.checked_add(voting_power)?; - proposal.outpost_against_power = - proposal.outpost_against_power.checked_add(voting_power)?; - } - }; - PROPOSAL_VOTERS.save(deps.storage, (proposal_id, voter.clone()), &vote_option)?; - - // Assert that the total amount of power from Outposts is not greater than the - // total amount of power that was available at the time of proposal creation - let current_outpost_power = proposal - .outpost_for_power - .checked_add(proposal.outpost_against_power)?; - let max_outpost_power = - get_total_outpost_voting_power_at(deps.querier, &hub, proposal.start_time)?; - if current_outpost_power > max_outpost_power { - return Err(ContractError::InvalidVotingPower {}); - } - - PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - - Ok(Response::new().add_attributes(vec![ - attr("action", "cast_outpost_vote"), - attr("proposal_id", proposal_id.to_string()), - attr("voter", &voter), - attr("vote", vote_option.to_string()), - attr("voting_power", voting_power), - ])) -} - /// Ends proposal voting period, sets the proposal status by id and returns /// xASTRO submitted for the proposal. pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result { @@ -513,86 +388,6 @@ pub fn execute_proposal( Ok(response) } -/// Load and execute a special emissions proposal. This proposal is passed -/// immediately and is not subject to voting as it is coming from the -/// generator controller based on emission votes. -#[allow(clippy::too_many_arguments)] -pub fn submit_execute_emissions_proposal( - deps: DepsMut, - env: Env, - info: MessageInfo, - title: String, - description: String, - messages: Vec, - ibc_channel: Option, -) -> Result { - let config = CONFIG.load(deps.storage)?; - - // Verify that only the generator controller has been set - let generator_controller = match config.generator_controller { - Some(config_generator_controller) => config_generator_controller, - None => return Err(ContractError::InvalidGeneratorController {}), - }; - - // Only the generator controller may create these proposals. These proposals - // are typically for setting alloc points on Outposts - if info.sender != generator_controller { - return Err(ContractError::Unauthorized {}); - } - - // Ensure that we have messages to execute - if messages.is_empty() { - return Err(ContractError::InvalidProposalMessages {}); - } - - // Check that controller exists and it supports this channel - if let Some(ibc_channel) = &ibc_channel { - if let Some(ibc_controller) = &config.ibc_controller { - check_contract_supports_channel(deps.querier, ibc_controller, ibc_channel)?; - } else { - return Err(ContractError::MissingIBCController {}); - } - } - - // Update the proposal count - let count = PROPOSAL_COUNT.update(deps.storage, |c| -> StdResult<_> { - Ok(c.checked_add(Uint64::new(1))?) - })?; - - let proposal = Proposal { - proposal_id: count, - submitter: info.sender, - status: ProposalStatus::Passed, - for_power: Uint128::zero(), - outpost_for_power: Uint128::zero(), - against_power: Uint128::zero(), - outpost_against_power: Uint128::zero(), - start_block: env.block.height, - start_time: env.block.time.seconds(), - end_block: env.block.height, - delayed_end_block: env.block.height, - expiration_block: env.block.height + config.proposal_expiration_period, - title, - description, - link: None, - messages, - deposit_amount: Uint128::zero(), - ibc_channel, - total_voting_power: Default::default(), - }; - PROPOSAL_VOTERS.save( - deps.storage, - (proposal.proposal_id.u64(), generator_controller.to_string()), - &ProposalVoteOption::For, - )?; - - proposal.validate(config.whitelisted_links)?; - - PROPOSALS.save(deps.storage, count.u64(), &proposal)?; - - execute_proposal(deps, env, proposal.proposal_id.u64()) -} - /// Checks that proposal messages are correct. pub fn check_messages(env: Env, mut messages: Vec) -> Result { messages.push( @@ -629,27 +424,10 @@ pub fn update_config( config.xastro_denom = xastro_denom; } - if let Some(vxastro_token_addr) = updated_config.vxastro_token_addr { - config.vxastro_token_addr = Some(deps.api.addr_validate(&vxastro_token_addr)?); - } - - if let Some(voting_escrow_delegator_addr) = updated_config.voting_escrow_delegator_addr { - config.voting_escrow_delegator_addr = - Some(deps.api.addr_validate(&voting_escrow_delegator_addr)?) - } - if let Some(ibc_controller) = updated_config.ibc_controller { config.ibc_controller = Some(deps.api.addr_validate(&ibc_controller)?) } - if let Some(generator_controller) = updated_config.generator_controller { - config.generator_controller = Some(deps.api.addr_validate(&generator_controller)?) - } - - if let Some(hub) = updated_config.hub { - config.hub = Some(deps.api.addr_validate(&hub)?) - } - if let Some(builder_unlock_addr) = updated_config.builder_unlock_addr { config.builder_unlock_addr = deps.api.addr_validate(&builder_unlock_addr)?; } @@ -699,10 +477,6 @@ pub fn update_config( } } - if let Some(guardian_addr) = updated_config.guardian_addr { - config.guardian_addr = Some(deps.api.addr_validate(&guardian_addr)?); - } - #[cfg(not(feature = "testnet"))] config.validate()?; @@ -747,80 +521,3 @@ fn update_ibc_proposal_status( Err(ContractError::InvalidIBCController {}) } } - -/// Remove all votes cast from all Outposts in case of a vulnerability -/// in IBC or the contracts that allow manipulation of governance. This is the -/// last line of defence against a malicious actor. -/// -/// This can only be called by the guardian. -fn remove_outpost_votes( - deps: DepsMut, - env: Env, - info: MessageInfo, - proposal_id: u64, -) -> Result { - let mut proposal = PROPOSALS.load(deps.storage, proposal_id)?; - - let config = CONFIG.load(deps.storage)?; - - // Only the guardian may execute this - if Some(info.sender) != config.guardian_addr { - return Err(ContractError::Unauthorized {}); - } - - // EndProposal must be called first to return xASTRO to the proposer - if proposal.status == ProposalStatus::Active { - return Err(ContractError::ProposalNotInDelayPeriod {}); - } - - // This may only be called during the "delay" period for a proposal. That is, - // the config.proposal_effective_delay blocks between when the voting period - // ends and the proposal can be executed. If we allow the removal of votes during - // the voting period, we can end up in a battle with the attacker where we - // remove the votes and they exploit and vote again. - if env.block.height <= proposal.end_block || env.block.height > proposal.delayed_end_block { - return Err(ContractError::ProposalNotInDelayPeriod {}); - } - - // Remove the voting power from Outposts - let new_for_power = proposal - .for_power - .saturating_sub(proposal.outpost_for_power); - let new_against_power = proposal - .against_power - .saturating_sub(proposal.outpost_against_power); - - proposal.for_power = new_for_power; - proposal.against_power = new_against_power; - - // Zero out the Outpost voting power after removal - proposal.outpost_for_power = Uint128::zero(); - proposal.outpost_against_power = Uint128::zero(); - - let total_votes = proposal.for_power.saturating_add(proposal.against_power); - - // Recalculate proposal state - let proposal_quorum = - Decimal::checked_from_ratio(total_votes, proposal.total_voting_power).unwrap_or_default(); - let proposal_threshold = - Decimal::checked_from_ratio(proposal.for_power, total_votes).unwrap_or_default(); - - // Determine the proposal result - proposal.status = if proposal_quorum >= config.proposal_required_quorum - && proposal_threshold > config.proposal_required_threshold - { - ProposalStatus::Passed - } else { - ProposalStatus::Rejected - }; - - PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - - let response = Response::new().add_attributes([ - attr("action", "remove_outpost_votes"), - attr("proposal_id", proposal_id.to_string()), - attr("proposal_result", proposal.status.to_string()), - ]); - - Ok(response) -} diff --git a/contracts/assembly/src/unit_tests.rs b/contracts/assembly/src/unit_tests.rs index 42ddc438..8e0ef4cc 100644 --- a/contracts/assembly/src/unit_tests.rs +++ b/contracts/assembly/src/unit_tests.rs @@ -124,11 +124,7 @@ fn check_proposal_validation( let config = Config { xastro_denom: XASTRO_DENOM.to_string(), xastro_denom_tracking: "".to_string(), - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, ibc_controller: None, - generator_controller: None, - hub: None, builder_unlock_addr: Addr::unchecked(""), proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), proposal_effective_delay: *DELAY_INTERVAL.start(), @@ -142,7 +138,6 @@ fn check_proposal_validation( ) .unwrap(), whitelisted_links: vec!["https://some.link/".to_string()], - guardian_addr: None, }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -213,11 +208,7 @@ fn check_submit_ibc_proposal() { let mut config = Config { xastro_denom: XASTRO_DENOM.to_string(), xastro_denom_tracking: "".to_string(), - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, ibc_controller: None, - generator_controller: None, - hub: None, builder_unlock_addr: Addr::unchecked(""), proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), proposal_effective_delay: *DELAY_INTERVAL.start(), @@ -231,7 +222,6 @@ fn check_submit_ibc_proposal() { ) .unwrap(), whitelisted_links: vec!["https://some.link/".to_string()], - guardian_addr: None, }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -290,11 +280,7 @@ fn check_execute_ibc_proposal() { let mut config = Config { xastro_denom: "".to_string(), xastro_denom_tracking: "".to_string(), - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, ibc_controller: None, - generator_controller: None, - hub: None, builder_unlock_addr: Addr::unchecked(""), proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), proposal_effective_delay: *DELAY_INTERVAL.start(), @@ -308,7 +294,6 @@ fn check_execute_ibc_proposal() { ) .unwrap(), whitelisted_links: vec!["https://some.link/".to_string()], - guardian_addr: None, }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -370,11 +355,7 @@ fn check_controller_callback() { let mut config = Config { xastro_denom: "".to_string(), xastro_denom_tracking: "".to_string(), - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, ibc_controller: None, - generator_controller: None, - hub: None, builder_unlock_addr: Addr::unchecked(""), proposal_voting_period: *VOTING_PERIOD_INTERVAL.start(), proposal_effective_delay: *DELAY_INTERVAL.start(), @@ -388,7 +369,6 @@ fn check_controller_callback() { ) .unwrap(), whitelisted_links: vec!["https://some.link/".to_string()], - guardian_addr: None, }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); diff --git a/contracts/assembly/src/utils.rs b/contracts/assembly/src/utils.rs index 63f70270..611e8eb9 100644 --- a/contracts/assembly/src/utils.rs +++ b/contracts/assembly/src/utils.rs @@ -1,16 +1,11 @@ use astroport::tokenfactory_tracker; -use astroport_governance::assembly::Config; -use cosmwasm_std::{Deps, QuerierWrapper, StdResult, Uint128, Uint64}; +use cosmwasm_std::{Deps, QuerierWrapper, StdResult, Uint128}; +use astroport_governance::assembly::Config; use astroport_governance::assembly::Proposal; use astroport_governance::builder_unlock::msg::{ AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, StateResponse, }; -use astroport_governance::utils::WEEK; -use astroport_governance::voting_escrow_delegation::QueryMsg::AdjustedBalance; -use astroport_governance::voting_escrow_lite::{ - QueryMsg as VotingEscrowQueryMsg, VotingPowerResponse, -}; use crate::state::CONFIG; @@ -40,44 +35,6 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std total += locked_amount.params.amount - locked_amount.status.astro_withdrawn; - if let Some(vxastro_token_addr) = config.vxastro_token_addr { - let vxastro_amount = - if let Some(voting_escrow_delegator_addr) = config.voting_escrow_delegator_addr { - deps.querier.query_wasm_smart( - voting_escrow_delegator_addr, - &AdjustedBalance { - account: sender.clone(), - // TODO: why minus WEEK? - timestamp: Some(proposal.start_time - WEEK), - }, - )? - } else { - // TODO: why? - // For vxASTRO lite, this will always be 0 - let res: VotingPowerResponse = deps.querier.query_wasm_smart( - &vxastro_token_addr, - &VotingEscrowQueryMsg::UserVotingPowerAt { - user: sender.clone(), - // TODO: why minus WEEK? - time: proposal.start_time - WEEK, - }, - )?; - res.voting_power - }; - - total += vxastro_amount; - - let locked_xastro: Uint128 = deps.querier.query_wasm_smart( - vxastro_token_addr, - &VotingEscrowQueryMsg::UserDepositAt { - user: sender, - timestamp: Uint64::from(proposal.start_time), - }, - )?; - - total += locked_xastro; - } - Ok(total) } @@ -85,7 +42,6 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std /// Combined voting power includes: /// * xASTRO total supply /// * ASTRO tokens which still locked in the builder's unlock contract -/// * vxASTRO total supply /// /// ## Parameters /// * **config** contract settings. @@ -110,19 +66,5 @@ pub fn calc_total_voting_power_at( total += builder_state.remaining_astro_tokens; - // TODO: remove it since it is always 0? - if let Some(vxastro_token_addr) = &config.vxastro_token_addr { - // Total vxASTRO voting power - // For vxASTRO lite, this will always be 0 - let vxastro: VotingPowerResponse = querier.query_wasm_smart( - vxastro_token_addr, - &VotingEscrowQueryMsg::TotalVotingPowerAt { - time: timestamp - WEEK, - }, - )?; - - total += vxastro.voting_power; - } - Ok(total) } diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index f5b05c5e..7a4e1916 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -4,8 +4,8 @@ use anyhow::Result as AnyResult; use astroport::staking; use cosmwasm_std::testing::MockApi; use cosmwasm_std::{ - coin, coins, from_json, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Empty, Env, GovMsg, - IbcMsg, IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, + coin, coins, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Empty, Env, GovMsg, IbcMsg, + IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, }; use cw_multi_test::{ App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, @@ -62,14 +62,6 @@ fn builder_contract() -> Box> { )) } -fn vxastro_contract() -> Box> { - Box::new(ContractWrapper::new_with_empty( - voting_escrow_lite::execute::execute, - voting_escrow_lite::contract::instantiate, - voting_escrow_lite::query::query, - )) -} - pub const PROPOSAL_REQUIRED_DEPOSIT: Uint128 = Uint128::new(*DEPOSIT_INTERVAL.start()); pub const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); pub const PROPOSAL_DELAY: u64 = *DELAY_INTERVAL.start(); @@ -118,7 +110,6 @@ pub struct Helper { pub staking: Addr, pub assembly: Addr, pub builder_unlock: Addr, - pub vxastro: Addr, pub xastro_denom: String, pub assembly_code_id: u64, } @@ -180,37 +171,11 @@ impl Helper { ) .unwrap(); - let vxastro_code_id = app.store_code(vxastro_contract()); - - let msg = astroport_governance::voting_escrow_lite::InstantiateMsg { - owner: owner.to_string(), - guardian_addr: Some(owner.to_string()), - deposit_denom: xastro_denom.to_string(), - generator_controller_addr: None, - outpost_addr: None, - marketing: None, - logo_urls_whitelist: vec![], - }; - - let vxastro = app - .instantiate_contract( - vxastro_code_id, - owner.clone(), - &msg, - &[], - "vxASTRO".to_string(), - None, - ) - .unwrap(); - let assembly = app .instantiate_contract( assembly_code_id, owner.clone(), - &InstantiateMsg { - vxastro_token_addr: Some(vxastro.to_string()), - ..default_init_msg(&staking, &builder_unlock) - }, + &default_init_msg(&staking, &builder_unlock), &[], String::from("Astroport Assembly"), None, @@ -223,7 +188,6 @@ impl Helper { staking, assembly, builder_unlock, - vxastro, xastro_denom, assembly_code_id, }) @@ -285,22 +249,6 @@ impl Helper { .unwrap(); } - pub fn get_vxastro(&mut self, recipient: &Addr, amount: impl Into + Copy) { - let resp = self.get_xastro(recipient, amount); - let xastro_amount = from_json::(&resp.data.unwrap().0) - .unwrap() - .xastro_amount; - - self.app - .execute_contract( - recipient.clone(), - self.vxastro.clone(), - &astroport_governance::voting_escrow_lite::ExecuteMsg::CreateLock {}, - &coins(xastro_amount.u128(), &self.xastro_denom), - ) - .unwrap(); - } - pub fn submit_proposal(&mut self, submitter: &Addr, messages: Vec) { self.app .execute_contract( diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 8de40e70..3053f2d7 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -531,11 +531,7 @@ fn test_update_config() { assembly.clone(), &ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { xastro_denom: None, - vxastro_token_addr: None, - voting_escrow_delegator_addr: None, ibc_controller: None, - generator_controller: None, - hub: None, builder_unlock_addr: None, proposal_voting_period: None, proposal_effective_delay: None, @@ -545,7 +541,6 @@ fn test_update_config() { proposal_required_threshold: None, whitelist_remove: None, whitelist_add: None, - guardian_addr: None, })), &[], ) @@ -557,11 +552,7 @@ fn test_update_config() { let updated_config = UpdateConfig { xastro_denom: Some("test".to_string()), - vxastro_token_addr: Some("vxastro_token".to_string()), - voting_escrow_delegator_addr: Some("voting_escrow_delegator".to_string()), ibc_controller: Some("ibc_controller".to_string()), - generator_controller: Some("generator_controller".to_string()), - hub: Some("hub".to_string()), builder_unlock_addr: Some("builder_unlock".to_string()), proposal_voting_period: Some(*VOTING_PERIOD_INTERVAL.end()), proposal_effective_delay: Some(*DELAY_INTERVAL.end()), @@ -571,7 +562,6 @@ fn test_update_config() { proposal_required_threshold: Some("0.5".to_string()), whitelist_remove: Some(vec!["https://some.link/".to_string()]), whitelist_add: Some(vec!["https://another.link/".to_string()]), - guardian_addr: Some("guardian".to_string()), }; helper @@ -591,23 +581,10 @@ fn test_update_config() { .unwrap(); assert_eq!(config.xastro_denom, "test"); - assert_eq!( - config.vxastro_token_addr, - Some(Addr::unchecked("vxastro_token")) - ); - assert_eq!( - config.voting_escrow_delegator_addr, - Some(Addr::unchecked("voting_escrow_delegator")) - ); assert_eq!( config.ibc_controller, Some(Addr::unchecked("ibc_controller")) ); - assert_eq!( - config.generator_controller, - Some(Addr::unchecked("generator_controller")) - ); - assert_eq!(config.hub, Some(Addr::unchecked("hub"))); assert_eq!( config.builder_unlock_addr, Addr::unchecked("builder_unlock") @@ -634,7 +611,6 @@ fn test_update_config() { config.whitelisted_links, vec!["https://another.link/".to_string()] ); - assert_eq!(config.guardian_addr, Some(Addr::unchecked("guardian"))); } #[test] diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index 29e51b57..6cacd9c5 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -82,16 +82,6 @@ pub enum ExecuteMsg { /// Vote option vote: ProposalVoteOption, }, - CastOutpostVote { - /// Proposal identifier - proposal_id: u64, - /// The voter from an Outpost - voter: String, - /// The vote option - vote: ProposalVoteOption, - /// The voting power applied to this vote - voting_power: Uint128, - }, /// Set the status of a proposal that expired EndProposal { /// Proposal identifier @@ -106,16 +96,6 @@ pub enum ExecuteMsg { /// Proposal identifier proposal_id: u64, }, - /// Load and execute a special emissions proposal. This proposal is passed - /// immediately and is not subject to voting as it is coming from the - /// generator controller based on emission votes. - ExecuteEmissionsProposal { - title: String, - description: String, - messages: Vec, - /// If proposal should be executed on a remote chain this field should specify governance channel - ibc_channel: Option, - }, /// Update parameters in the Assembly contract /// ## Executor /// Only the Assembly contract is allowed to update its own parameters @@ -127,14 +107,6 @@ pub enum ExecuteMsg { proposal_id: u64, status: ProposalStatus, }, - /// Remove all votes cast from all Outposts in case of a vulnerability - /// in IBC or the contracts that allow manipulation of governance. - /// - /// This can only be called by the guardian. - RemoveOutpostVotes { - /// Proposal identifier - proposal_id: u64, - }, } /// Thie enum describes all the queries available in the contract. @@ -183,16 +155,8 @@ pub struct Config { pub xastro_denom: String, // xASTRO denom tracking contract pub xastro_denom_tracking: String, - /// vxASTRO token address - pub vxastro_token_addr: Option, - /// Voting Escrow delegator address - pub voting_escrow_delegator_addr: Option, /// Astroport IBC controller contract pub ibc_controller: Option, - /// Generator controller contract capable of immediate proposals - pub generator_controller: Option, - /// Hub contract that handles voting from Outposts - pub hub: Option, /// Builder unlock contract address pub builder_unlock_addr: Addr, /// Proposal voting period @@ -209,9 +173,6 @@ pub struct Config { pub proposal_required_threshold: Decimal, /// Whitelisted links pub whitelisted_links: Vec, - /// Guardian address that may cancel Outpost votes in case of a vulnerability - /// in IBC or the contracts that allow manipulation of governance - pub guardian_addr: Option, } impl Config { @@ -269,12 +230,6 @@ impl Config { ))); } - if self.voting_escrow_delegator_addr.is_some() && self.vxastro_token_addr.is_none() { - return Err(StdError::generic_err( - "The Voting Escrow contract should be specified to use the Voting Escrow Delegator contract." - )); - } - Ok(()) } } @@ -284,16 +239,8 @@ impl Config { pub struct UpdateConfig { /// xASTRO token denom pub xastro_denom: Option, - /// vxASTRO token address - pub vxastro_token_addr: Option, - /// Voting Escrow delegator address - pub voting_escrow_delegator_addr: Option, /// Astroport IBC controller contract pub ibc_controller: Option, - /// Generator controller contract capable of immediate proposals - pub generator_controller: Option, - /// Hub contract that handles voting from Outposts - pub hub: Option, /// Builder unlock contract address pub builder_unlock_addr: Option, /// Proposal voting period @@ -312,8 +259,6 @@ pub struct UpdateConfig { pub whitelist_remove: Option>, /// Links to add to whitelist pub whitelist_add: Option>, - /// Guardian address that may cancel Outpost votes in case of a vulnerability - pub guardian_addr: Option, } /// This structure stores data for a proposal. @@ -333,10 +278,6 @@ pub struct Proposal { pub against_power: Uint128, /// `Against` power of proposal cast from all Outposts pub outpost_against_power: Uint128, - // /// `For` votes for the proposal - // pub for_voters: Vec, - // /// `Against` votes for the proposal - // pub against_voters: Vec, /// Start block of proposal pub start_block: u64, /// Start time of proposal From ed0645a917445685cf212597f5894d92e3b85659 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 30 Jan 2024 20:32:35 +0400 Subject: [PATCH 26/47] disable unused crates; apply linter fixes --- Cargo.lock | 938 +--------------------- Cargo.toml | 12 +- contracts/assembly/src/contract.rs | 2 +- contracts/assembly/src/unit_tests.rs | 2 +- contracts/assembly/tests/common/helper.rs | 4 +- contracts/assembly/tests/integration.rs | 1 - contracts/builder_unlock/src/contract.rs | 3 +- 7 files changed, 38 insertions(+), 924 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c73a195d..10638199 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,7 +26,7 @@ dependencies = [ "anyhow", "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", "astroport-governance 1.4.0", - "astroport-staking 2.0.0", + "astroport-staking", "astroport-tokenfactory-tracker", "builder-unlock", "cosmwasm-schema", @@ -110,56 +110,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "astroport-escrow-fee-distributor" -version = "1.0.2" -dependencies = [ - "astroport-governance 1.4.0", - "astroport-tests", - "astroport-token", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.15.1", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "thiserror", -] - -[[package]] -name = "astroport-factory" -version = "1.7.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" -dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 1.0.3", - "cw2 0.15.1", - "itertools 0.10.5", - "protobuf", - "thiserror", -] - -[[package]] -name = "astroport-generator" -version = "2.3.2" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" -dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "astroport-governance 1.4.0", - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 1.0.3", - "cw1-whitelist", - "cw2 0.15.1", - "cw20 0.15.1", - "protobuf", - "thiserror", -] - [[package]] name = "astroport-governance" version = "1.2.0" @@ -185,25 +135,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "astroport-hub" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "astroport-governance 1.4.0", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.16.5", - "cw-storage-plus 0.15.1", - "cw2 1.1.2", - "cw20 1.1.2", - "schemars", - "serde", - "serde-json-wasm", - "thiserror", -] - [[package]] name = "astroport-ibc" version = "1.2.1" @@ -213,61 +144,6 @@ dependencies = [ "cosmwasm-schema", ] -[[package]] -name = "astroport-outpost" -version = "0.1.0" -dependencies = [ - "anyhow", - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "astroport-governance 1.4.0", - "base64 0.13.1", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.16.5", - "cw-storage-plus 0.15.1", - "cw-utils 1.0.3", - "cw2 1.1.2", - "cw20 0.15.1", - "schemars", - "semver", - "serde", - "serde-json-wasm", - "thiserror", -] - -[[package]] -name = "astroport-pair" -version = "1.5.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" -dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 1.0.3", - "cw2 0.15.1", - "cw20 0.15.1", - "integer-sqrt", - "protobuf", - "thiserror", -] - -[[package]] -name = "astroport-staking" -version = "1.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" -dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 1.0.3", - "cw2 0.15.1", - "cw20 0.15.1", - "protobuf", - "thiserror", -] - [[package]] name = "astroport-staking" version = "2.0.0" @@ -282,67 +158,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "astroport-tests" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "astroport-escrow-fee-distributor", - "astroport-factory", - "astroport-generator", - "astroport-governance 1.4.0", - "astroport-pair", - "astroport-staking 1.1.0", - "astroport-token", - "astroport-whitelist", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "generator-controller", - "voting-escrow", -] - -[[package]] -name = "astroport-tests-lite" -version = "1.0.0" -dependencies = [ - "anyhow", - "astro-assembly", - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "astroport-escrow-fee-distributor", - "astroport-factory", - "astroport-generator", - "astroport-governance 1.4.0", - "astroport-pair", - "astroport-staking 1.1.0", - "astroport-token", - "astroport-voting-escrow-lite", - "astroport-whitelist", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.16.5", - "cw2 0.15.1", - "cw20 0.15.1", - "generator-controller-lite", -] - -[[package]] -name = "astroport-token" -version = "1.1.1" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" -dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "cosmwasm-schema", - "cosmwasm-std", - "cw2 0.15.1", - "cw20 0.15.1", - "cw20-base 0.15.1", - "snafu", -] - [[package]] name = "astroport-tokenfactory-tracker" version = "1.0.0" @@ -356,63 +171,18 @@ dependencies = [ "thiserror", ] -[[package]] -name = "astroport-voting-escrow-lite" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", - "astroport-governance 1.4.0", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cw-storage-plus 0.15.1", - "cw-utils 1.0.3", - "cw2 1.1.2", - "cw20 1.1.2", - "cw20-base 1.1.2", - "generator-controller-lite", - "proptest", - "thiserror", -] - -[[package]] -name = "astroport-whitelist" -version = "1.0.1" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" -dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "cosmwasm-schema", - "cosmwasm-std", - "cw1-whitelist", - "cw2 0.15.1", - "thiserror", -] - [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - [[package]] name = "base16ct" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -431,33 +201,6 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" - [[package]] name = "block-buffer" version = "0.9.0" @@ -536,9 +279,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6aa9f904de106fa16443ad14ec2abe75e94ba003bb61c681c0e43d4c58d2a" dependencies = [ "digest 0.10.7", - "ecdsa 0.16.9", + "ecdsa", "ed25519-zebra", - "k256 0.13.3", + "k256", "rand_core 0.6.4", "thiserror", ] @@ -582,7 +325,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad011ae7447188e26e4a7dbca2fcd0fc186aa21ae5c86df0503ea44c78f9e469" dependencies = [ - "base64 0.21.7", + "base64", "bech32", "bnum", "cosmwasm-crypto", @@ -598,16 +341,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cosmwasm-storage" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66de2ab9db04757bcedef2b5984fbe536903ada4a8a9766717a4a71197ef34f6" -dependencies = [ - "cosmwasm-std", - "serde", -] - [[package]] name = "cpufeatures" version = "0.2.12" @@ -623,18 +356,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - [[package]] name = "crypto-bigint" version = "0.5.5" @@ -670,44 +391,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "cw-multi-test" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e81b4a7821d5eeba0d23f737c16027b39a600742ca8c32eb980895ffd270f4" -dependencies = [ - "anyhow", - "cosmwasm-std", - "cosmwasm-storage", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "derivative", - "itertools 0.10.5", - "prost 0.9.0", - "schemars", - "serde", - "thiserror", -] - -[[package]] -name = "cw-multi-test" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" -dependencies = [ - "anyhow", - "cosmwasm-std", - "cw-storage-plus 1.2.0", - "cw-utils 1.0.3", - "derivative", - "itertools 0.10.5", - "k256 0.11.6", - "prost 0.9.0", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "cw-multi-test" version = "0.20.0" @@ -799,35 +482,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "cw1" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe0783ec4210ba4e0cdfed9874802f469c6db0880f742ad427cb950e940b21c" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "schemars", - "serde", -] - -[[package]] -name = "cw1-whitelist" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233dd13f61495f1336da57c8bdca0536fa9f8dd59c12d2bbfc59928ea580e478" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw1", - "cw2 0.15.1", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "cw2" version = "0.15.1" @@ -882,41 +536,6 @@ dependencies = [ "serde", ] -[[package]] -name = "cw20-base" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0909c56d0c14601fbdc69382189799482799dcad87587926aec1f3aa321abc41" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "schemars", - "semver", - "serde", - "thiserror", -] - -[[package]] -name = "cw20-base" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ad79e86ea3707229bf78df94e08732e8f713207b4a77b2699755596725e7d9" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 1.2.0", - "cw2 1.1.2", - "cw20 1.1.2", - "schemars", - "semver", - "serde", - "thiserror", -] - [[package]] name = "cw3" version = "1.1.2" @@ -932,16 +551,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - [[package]] name = "der" version = "0.7.8" @@ -984,42 +593,24 @@ dependencies = [ "subtle", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "dyn-clone" version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve 0.12.3", - "rfc6979 0.3.1", - "signature 1.6.4", -] - [[package]] name = "ecdsa" version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ - "der 0.7.8", + "der", "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", + "elliptic-curve", + "rfc6979", + "signature", + "spki", ] [[package]] @@ -1043,69 +634,23 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct 0.1.1", - "crypto-bigint 0.4.9", - "der 0.6.1", - "digest 0.10.7", - "ff 0.12.1", - "generic-array", - "group 0.12.1", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "sec1 0.3.0", - "subtle", - "zeroize", -] - [[package]] name = "elliptic-curve" version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", + "base16ct", + "crypto-bigint", "digest 0.10.7", - "ff 0.13.0", + "ff", "generic-array", - "group 0.13.0", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sec1 0.7.3", - "subtle", - "zeroize", -] - -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "fastrand" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ + "group", + "pkcs8", "rand_core 0.6.4", + "sec1", "subtle", + "zeroize", ] [[package]] @@ -1118,68 +663,12 @@ dependencies = [ "subtle", ] -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "forward_ref" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" -[[package]] -name = "generator-controller" -version = "1.3.0" -dependencies = [ - "anyhow", - "astroport-factory", - "astroport-generator", - "astroport-governance 1.4.0", - "astroport-pair", - "astroport-staking 1.1.0", - "astroport-tests", - "astroport-token", - "astroport-whitelist", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.15.1", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "itertools 0.10.5", - "proptest", - "thiserror", - "voting-escrow", -] - -[[package]] -name = "generator-controller-lite" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport-factory", - "astroport-generator", - "astroport-governance 1.4.0", - "astroport-pair", - "astroport-staking 1.1.0", - "astroport-tests-lite", - "astroport-token", - "astroport-whitelist", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.16.5", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "itertools 0.10.5", - "proptest", - "thiserror", - "voting-escrow", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -1202,24 +691,13 @@ dependencies = [ "wasi", ] -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "group" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ - "ff 0.13.0", + "ff", "rand_core 0.6.4", "subtle", ] @@ -1260,15 +738,6 @@ dependencies = [ "cosmwasm-std", ] -[[package]] -name = "integer-sqrt" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" -dependencies = [ - "num-traits", -] - [[package]] name = "itertools" version = "0.10.5" @@ -1302,18 +771,6 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" -[[package]] -name = "k256" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" -dependencies = [ - "cfg-if", - "ecdsa 0.14.8", - "elliptic-curve 0.12.3", - "sha2 0.10.8", -] - [[package]] name = "k256" version = "0.13.3" @@ -1321,37 +778,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if", - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", + "ecdsa", + "elliptic-curve", "once_cell", "sha2 0.10.8", - "signature 2.2.0", + "signature", ] -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - [[package]] name = "libc" version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" -[[package]] -name = "libm" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" - -[[package]] -name = "linux-raw-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" - [[package]] name = "num-traits" version = "0.2.17" @@ -1359,7 +798,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", - "libm", ] [[package]] @@ -1403,32 +841,16 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der 0.6.1", - "spki 0.6.0", -] - [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der 0.7.8", - "spki 0.7.3", + "der", + "spki", ] -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - [[package]] name = "proc-macro2" version = "1.0.78" @@ -1438,36 +860,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proptest" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.4.2", - "lazy_static", - "num-traits", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "prost" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" -dependencies = [ - "bytes", - "prost-derive 0.9.0", -] - [[package]] name = "prost" version = "0.11.9" @@ -1488,19 +880,6 @@ dependencies = [ "prost-derive 0.12.3", ] -[[package]] -name = "prost-derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "prost-derive" version = "0.11.9" @@ -1545,21 +924,6 @@ dependencies = [ "prost 0.12.3", ] -[[package]] -name = "protobuf" -version = "2.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" -dependencies = [ - "bytes", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quote" version = "1.0.35" @@ -1569,27 +933,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - [[package]] name = "rand_core" version = "0.5.1" @@ -1605,41 +948,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_xorshift" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" -dependencies = [ - "rand_core 0.6.4", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "regex-syntax" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" - -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - [[package]] name = "rfc6979" version = "0.4.0" @@ -1650,31 +958,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "rustix" -version = "0.38.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" -dependencies = [ - "bitflags 2.4.2", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rusty-fork" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - [[package]] name = "ryu" version = "1.0.16" @@ -1705,30 +988,16 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct 0.1.1", - "der 0.6.1", - "generic-array", - "pkcs8 0.9.0", - "subtle", - "zeroize", -] - [[package]] name = "sec1" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "base16ct 0.2.0", - "der 0.7.8", + "base16ct", + "der", "generic-array", - "pkcs8 0.10.2", + "pkcs8", "subtle", "zeroize", ] @@ -1823,16 +1092,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - [[package]] name = "signature" version = "2.2.0" @@ -1843,37 +1102,6 @@ dependencies = [ "rand_core 0.6.4", ] -[[package]] -name = "snafu" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" -dependencies = [ - "doc-comment", - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] - [[package]] name = "spki" version = "0.7.3" @@ -1881,7 +1109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der 0.7.8", + "der", ] [[package]] @@ -1918,19 +1146,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tempfile" -version = "3.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" -dependencies = [ - "cfg-if", - "fastrand", - "redox_syscall", - "rustix", - "windows-sys", -] - [[package]] name = "test-case" version = "3.3.1" @@ -2002,12 +1217,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - [[package]] name = "unicode-ident" version = "1.0.12" @@ -2020,107 +1229,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "voting-escrow" -version = "1.3.0" -dependencies = [ - "anyhow", - "astroport-escrow-fee-distributor", - "astroport-governance 1.4.0", - "astroport-staking 1.1.0", - "astroport-token", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test 0.15.1", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "cw20-base 0.15.1", - "proptest", - "thiserror", -] - -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" - [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 9eb838d4..8c6f283e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,13 +1,15 @@ [workspace] resolver = "2" members = [ - "packages/*", + "packages/astroport-governance", +# "packages/astroport-tests", +# "packages/astroport-tests-lite", "contracts/assembly", "contracts/builder_unlock", - "contracts/generator_controller_lite", - "contracts/hub", - "contracts/outpost", - "contracts/voting_escrow_lite", +# "contracts/generator_controller_lite", +# "contracts/hub", +# "contracts/outpost", +# "contracts/voting_escrow_lite", ] [profile.release] diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 6e5a25d6..94f4e3d5 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -392,7 +392,7 @@ pub fn execute_proposal( pub fn check_messages(env: Env, mut messages: Vec) -> Result { messages.push( wasm_execute( - &env.contract.address, + env.contract.address, &ExecuteMsg::CheckMessagesPassed {}, vec![], )? diff --git a/contracts/assembly/src/unit_tests.rs b/contracts/assembly/src/unit_tests.rs index 8e0ef4cc..eec2f119 100644 --- a/contracts/assembly/src/unit_tests.rs +++ b/contracts/assembly/src/unit_tests.rs @@ -163,7 +163,7 @@ fn check_proposal_validation( QueryMsg::Proposal { proposal_id: 1 }, ) .unwrap(); - let proposal: Proposal = from_json(&bin_resp).unwrap(); + let proposal: Proposal = from_json(bin_resp).unwrap(); assert_eq!( proposal, diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index 7a4e1916..e6f63605 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -244,7 +244,7 @@ impl Helper { }, )], }, - &coins(amount.into(), ASTRO_DENOM), + &coins(amount, ASTRO_DENOM), ) .unwrap(); } @@ -270,7 +270,7 @@ impl Helper { let assembly = self.assembly.clone(); self.mint_coin(&assembly, coin(1, "some_coin")); self.submit_proposal( - &submitter, + submitter, vec![BankMsg::Send { to_address: "receiver".to_string(), amount: coins(1, "some_coin"), diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 3053f2d7..a5167f45 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -630,7 +630,6 @@ fn test_voting_power() { let users_num = 100; let balances: HashMap = (1..=users_num) - .into_iter() .map(|i| { let user = Addr::unchecked(format!("user{i}")); let balances = TestBalance { diff --git a/contracts/builder_unlock/src/contract.rs b/contracts/builder_unlock/src/contract.rs index e5bb692a..3f96b428 100644 --- a/contracts/builder_unlock/src/contract.rs +++ b/contracts/builder_unlock/src/contract.rs @@ -98,8 +98,7 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S if info.sender != config.owner { return Err(StdError::generic_err( "Only the contract owner can increase allocations", - ) - .into()); + )); } let deposit_amount = may_pay(&info, &config.astro_denom) .map_err(|err| StdError::generic_err(err.to_string()))?; From c0e95b217af79db00128c3158ae74de40c7e959b Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 30 Jan 2024 20:36:10 +0400 Subject: [PATCH 27/47] bump main crate verson; set min version in dependent contracts --- Cargo.lock | 48 ++++++++++++++++++------ contracts/assembly/Cargo.toml | 2 +- contracts/builder_unlock/Cargo.toml | 2 +- packages/astroport-governance/Cargo.toml | 5 +-- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10638199..1a282332 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,8 +24,8 @@ name = "astro-assembly" version = "2.0.0" dependencies = [ "anyhow", - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", - "astroport-governance 1.4.0", + "astroport 3.8.0", + "astroport-governance 2.0.0", "astroport-staking", "astroport-tokenfactory-tracker", "builder-unlock", @@ -74,12 +74,13 @@ dependencies = [ [[package]] name = "astroport" -version = "3.8.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" +version = "3.10.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#abc23e17bbde99b5d10f0fc0e80517b6e17a4f30" dependencies = [ "astroport-circular-buffer 0.1.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", "cosmwasm-schema", "cosmwasm-std", + "cw-asset", "cw-storage-plus 0.15.1", "cw-utils 1.0.3", "cw20 0.15.1", @@ -102,7 +103,7 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#fc0c427d65690b56e9e9345f6156e48fb4e705db" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#abc23e17bbde99b5d10f0fc0e80517b6e17a4f30" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -125,12 +126,12 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "1.4.0" +version = "2.0.0" dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport 3.10.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", + "cw-storage-plus 1.2.0", "cw20 1.1.2", "thiserror", ] @@ -149,7 +150,7 @@ name = "astroport-staking" version = "2.0.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", + "astroport 3.8.0", "cosmwasm-std", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -163,7 +164,7 @@ name = "astroport-tokenfactory-tracker" version = "1.0.0" source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", + "astroport 3.8.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.2.0", @@ -229,8 +230,8 @@ checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" name = "builder-unlock" version = "3.0.0" dependencies = [ - "astroport 3.8.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", - "astroport-governance 1.4.0", + "astroport 3.10.0", + "astroport-governance 2.0.0", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -391,6 +392,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cw-address-like" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451a4691083a88a3c0630a8a88799e9d4cd6679b7ce8ff22b8da2873ff31d380" +dependencies = [ + "cosmwasm-std", +] + +[[package]] +name = "cw-asset" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "431e57314dceabd29a682c78bb3ff7c641f8bdc8b915400bb9956cb911e8e571" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-address-like", + "cw-storage-plus 1.2.0", + "cw20 1.1.2", + "thiserror", +] + [[package]] name = "cw-multi-test" version = "0.20.0" diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 56737679..c8e92f9b 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -18,7 +18,7 @@ library = [] cw2 = "1" cosmwasm-std = { version = "1.5", features = ["ibc3", "cosmwasm_1_1"] } cw-storage-plus = "1.2.0" -astroport-governance = { path = "../../packages/astroport-governance" } +astroport-governance = { path = "../../packages/astroport-governance", version = "2" } astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } ibc-controller-package = "1.0.0" thiserror = "1" diff --git a/contracts/builder_unlock/Cargo.toml b/contracts/builder_unlock/Cargo.toml index 75917b7c..375f31cd 100644 --- a/contracts/builder_unlock/Cargo.toml +++ b/contracts/builder_unlock/Cargo.toml @@ -19,7 +19,7 @@ cw2 = "1.1" cw-utils = "1" cosmwasm-std = "1.5" cw-storage-plus = "0.15" -astroport-governance = { path = "../../packages/astroport-governance" } +astroport-governance = { path = "../../packages/astroport-governance", version = "2" } astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } cosmwasm-schema = "1.5" diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 878b0647..564acec0 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,7 +1,6 @@ [package] name = "astroport-governance" -# TODO: bump version and set version in all dependent contracts -version = "1.4.0" +version = "2.0.0" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" @@ -17,7 +16,7 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cw20 = "1.1" cosmwasm-std = { version = "1.5", features = ["ibc3"] } -cw-storage-plus = "0.15" +cw-storage-plus = "1.2.0" cosmwasm-schema = "1.5" astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "3" } thiserror = "1" From c58a678bd7983d561115def507efe48f753f59e1 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 30 Jan 2024 21:10:09 +0400 Subject: [PATCH 28/47] fix linter issues --- .github/workflows/check_artifacts.yml | 2 +- contracts/assembly/src/utils.rs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check_artifacts.yml b/.github/workflows/check_artifacts.yml index 53bcb987..3681d5f6 100644 --- a/.github/workflows/check_artifacts.yml +++ b/.github/workflows/check_artifacts.yml @@ -113,4 +113,4 @@ jobs: run: cargo install --debug --version 1.4.0 cosmwasm-check - name: Cosmwasm check run: | - cosmwasm-check $GITHUB_WORKSPACE/artifacts/*.wasm --available-capabilities staking,iterator,stargate + cosmwasm-check $GITHUB_WORKSPACE/artifacts/*.wasm --available-capabilities staking,iterator,stargate,cosmwasm_1_1 diff --git a/contracts/assembly/src/utils.rs b/contracts/assembly/src/utils.rs index 611e8eb9..eed5318d 100644 --- a/contracts/assembly/src/utils.rs +++ b/contracts/assembly/src/utils.rs @@ -28,9 +28,7 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std let locked_amount: AllocationResponse = deps.querier.query_wasm_smart( config.builder_unlock_addr, - &BuilderUnlockQueryMsg::Allocation { - account: sender.clone(), - }, + &BuilderUnlockQueryMsg::Allocation { account: sender }, )?; total += locked_amount.params.amount - locked_amount.status.astro_withdrawn; From 9615e974a56d2534c79e5baae8d63bbb3f51cf24 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 9 Feb 2024 14:05:16 +0400 Subject: [PATCH 29/47] refine comments and validation limits --- contracts/assembly/src/contract.rs | 17 +++++++++-------- contracts/assembly/tests/integration.rs | 4 ++-- packages/astroport-governance/src/assembly.rs | 10 ++++++---- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 94f4e3d5..e57f592b 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -79,22 +79,23 @@ pub fn instantiate( /// * **ExecuteMsg::Receive(cw20_msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes /// it depending on the received template. /// -/// * **ExecuteMsg::CastVote { proposal_id, vote }** Cast a vote on a specific proposal. +/// * **ExecuteMsg::SubmitProposal { title, description, link, messages, ibc_channel }** Submits a new proposal. /// -/// * **ExecuteMsg::CastOutpostVote { proposal_id, voter, vote, voting_power }** Cast a vote on a specific proposal from an Outpost. +/// * **ExecuteMsg::CheckMessages { messages }** Checks if the messages are correct. +/// Executes arbitrary messages on behalf of the Assembly contract. Always appends failing message to the end of the list. /// -/// * **ExecuteMsg::EndProposal { proposal_id }** Sets the status of an expired/finalized proposal. +/// * **ExecuteMsg::CheckMessagesPassed {}** Closing message for the `CheckMessages` endpoint. /// -/// * **ExecuteMsg::ExecuteProposal { proposal_id }** Executes a successful proposal. +/// * **ExecuteMsg::CastVote { proposal_id, vote }** Cast a vote on a specific proposal. /// -/// * **ExecuteMsg::ExecuteEmissionsProposal { title, description, link, messages, ibc_channel }** Loads and executes an -/// emissions proposal from the generator controller +/// * **ExecuteMsg::EndProposal { proposal_id }** Sets the status of an expired/finalized proposal. /// -/// * **ExecuteMsg::RemoveCompletedProposal { proposal_id }** Removes a finalized proposal from the proposal list. +/// * **ExecuteMsg::ExecuteProposal { proposal_id }** Executes a successful proposal. /// /// * **ExecuteMsg::UpdateConfig(config)** Updates the contract configuration. /// -/// * **ExecuteMsg::CancelOutpostVotes(proposal_id)** Removes all votes cast from all Outposts on a specific proposal +/// * **ExecuteMsg::IBCProposalCompleted { proposal_id, status }** Updates proposal status InProgress -> Executed or Failed. +/// This endpoint processes callbacks from the ibc controller. #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index a5167f45..a31e8285 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -105,7 +105,7 @@ fn test_contract_instantiation() { assert_eq!( err.root_cause().to_string(), - "Generic error: The expiration period for a proposal cannot be lower than 12342 or higher than 100800" + format!("Generic error: The expiration period for a proposal cannot be lower than {} or higher than {}", EXPIRATION_PERIOD_INTERVAL.start(), EXPIRATION_PERIOD_INTERVAL.end()) ); let err = helper @@ -125,7 +125,7 @@ fn test_contract_instantiation() { assert_eq!( err.root_cause().to_string(), - "Generic error: The effective delay for a proposal cannot be lower than 6171 or higher than 14400" + format!("Generic error: The effective delay for a proposal cannot be lower than {} or higher than {}", DELAY_INTERVAL.start(), DELAY_INTERVAL.end()) ); let err = helper diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index 6cacd9c5..cc205aca 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -11,10 +11,12 @@ pub const MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 33; pub const MAX_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 100; pub const MAX_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "1"; pub const MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "0.01"; -pub const VOTING_PERIOD_INTERVAL: RangeInclusive = 12342..=7 * 12342; -// from 0.5 to 1 day in blocks (7 seconds per block) -pub const DELAY_INTERVAL: RangeInclusive = 6171..=14400; -pub const EXPIRATION_PERIOD_INTERVAL: RangeInclusive = 12342..=100_800; +/// Voting period must be between 1 and 7 days (Neutron: 2.6s per block) +pub const VOTING_PERIOD_INTERVAL: RangeInclusive = 33230..=7 * 33230; +/// From 0.5 to 2 days in blocks +pub const DELAY_INTERVAL: RangeInclusive = 16615..=33230; +/// From 1 to 14 days in blocks +pub const EXPIRATION_PERIOD_INTERVAL: RangeInclusive = 33230..=14 * 33230; // from 10k to 60k $xASTRO pub const DEPOSIT_INTERVAL: RangeInclusive = 10000000000..=60000000000; From 00fad140fdd619e297d60caa1ac564bc69ad1fa7 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 9 Feb 2024 14:57:07 +0400 Subject: [PATCH 30/47] add neutron satellite -> assembly conversion --- Cargo.lock | 91 ++++++++++++++++++++++++++++- contracts/assembly/Cargo.toml | 1 + contracts/assembly/src/contract.rs | 6 +- contracts/assembly/src/error.rs | 4 ++ contracts/assembly/src/lib.rs | 1 + contracts/assembly/src/migration.rs | 52 +++++++++++++++++ 6 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 contracts/assembly/src/migration.rs diff --git a/Cargo.lock b/Cargo.lock index 1a282332..4fb1e32e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -24,6 +24,7 @@ name = "astro-assembly" version = "2.0.0" dependencies = [ "anyhow", + "astro-satellite", "astroport 3.8.0", "astroport-governance 2.0.0", "astroport-staking", @@ -35,12 +36,40 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", - "ibc-controller-package", + "ibc-controller-package 1.0.0", "osmosis-std", "test-case", "thiserror", ] +[[package]] +name = "astro-satellite" +version = "1.1.0" +source = "git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1#61f3cf90ac7e48de93224e906171ebe206d7f860" +dependencies = [ + "astro-satellite-package", + "astroport-ibc 1.2.1 (git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1)", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw2 0.15.1", + "ibc-controller-package 0.1.0", + "itertools 0.10.5", + "thiserror", +] + +[[package]] +name = "astro-satellite-package" +version = "0.1.0" +source = "git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1#61f3cf90ac7e48de93224e906171ebe206d7f860" +dependencies = [ + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "cosmwasm-schema", + "cosmwasm-std", +] + [[package]] name = "astroport" version = "2.9.5" @@ -56,6 +85,21 @@ dependencies = [ "uint", ] +[[package]] +name = "astroport" +version = "2.10.0" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#11e7a81d4b18a40bed916177061a549633e02b1b" +dependencies = [ + "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" version = "3.8.0" @@ -124,6 +168,18 @@ dependencies = [ "cw20 0.15.1", ] +[[package]] +name = "astroport-governance" +version = "1.2.0" +source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" +dependencies = [ + "astroport 2.10.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw20 0.15.1", +] + [[package]] name = "astroport-governance" version = "2.0.0" @@ -145,6 +201,14 @@ dependencies = [ "cosmwasm-schema", ] +[[package]] +name = "astroport-ibc" +version = "1.2.1" +source = "git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1#61f3cf90ac7e48de93224e906171ebe206d7f860" +dependencies = [ + "cosmwasm-schema", +] + [[package]] name = "astroport-staking" version = "2.0.0" @@ -342,6 +406,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cosmwasm-storage" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66de2ab9db04757bcedef2b5984fbe536903ada4a8a9766717a4a71197ef34f6" +dependencies = [ + "cosmwasm-std", + "serde", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -750,14 +824,25 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "ibc-controller-package" +version = "0.1.0" +source = "git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1#61f3cf90ac7e48de93224e906171ebe206d7f860" +dependencies = [ + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-ibc 1.2.1 (git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1)", + "cosmwasm-schema", + "cosmwasm-std", +] + [[package]] name = "ibc-controller-package" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfcf94f5691716bfecb45e6bb6a82a5c11a392d501c2a695589c5087671f7c33" dependencies = [ - "astroport-governance 1.2.0", - "astroport-ibc", + "astroport-governance 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "astroport-ibc 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "cosmwasm-schema", "cosmwasm-std", ] diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index c8e92f9b..4965ad88 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -20,6 +20,7 @@ cosmwasm-std = { version = "1.5", features = ["ibc3", "cosmwasm_1_1"] } cw-storage-plus = "1.2.0" astroport-governance = { path = "../../packages/astroport-governance", version = "2" } astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } +astro-satellite = { git = "https://github.com/astroport-fi/astroport_ibc", tag = "v1.2.1", features = ["library"] } ibc-controller-package = "1.0.0" thiserror = "1" cosmwasm-schema = "1.5" diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index e57f592b..b3091c3d 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use astroport::asset::addr_opt_validate; use astroport::staking; use cosmwasm_std::{ - attr, coins, entry_point, wasm_execute, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, - Response, StdError, SubMsg, Uint128, Uint64, + attr, coins, wasm_execute, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, + StdError, SubMsg, Uint128, Uint64, }; use cw2::set_contract_version; use cw_utils::must_pay; @@ -19,6 +19,8 @@ use astroport_governance::utils::check_contract_supports_channel; use crate::error::ContractError; use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_VOTERS}; use crate::utils::{calc_total_voting_power_at, calc_voting_power}; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; // Contract name and version used for migration. const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index 3b263ae8..c5c08547 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -1,4 +1,5 @@ use cosmwasm_std::{OverflowError, StdError}; +use cw2::VersionError; use cw_utils::PaymentError; use thiserror::Error; @@ -13,6 +14,9 @@ pub enum ContractError { #[error("{0}")] OverflowError(#[from] OverflowError), + #[error("{0}")] + VersionError(#[from] VersionError), + #[error("Unauthorized")] Unauthorized {}, diff --git a/contracts/assembly/src/lib.rs b/contracts/assembly/src/lib.rs index 0f2efaa2..00d9dc3a 100644 --- a/contracts/assembly/src/lib.rs +++ b/contracts/assembly/src/lib.rs @@ -5,5 +5,6 @@ pub mod state; pub mod queries; pub mod utils; +pub mod migration; #[cfg(test)] mod unit_tests; diff --git a/contracts/assembly/src/migration.rs b/contracts/assembly/src/migration.rs new file mode 100644 index 00000000..7a254abf --- /dev/null +++ b/contracts/assembly/src/migration.rs @@ -0,0 +1,52 @@ +use crate::contract::instantiate; +use crate::error::ContractError; +use astroport_governance::assembly::InstantiateMsg; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{Addr, DepsMut, Env, IbcMsg, MessageInfo, Response, StdError}; + +const EXPECTED_CONTRACT_NAME: &str = "astro-satellite-neutron"; +const EXPECTED_CONTRACT_VERSION: &str = "1.1.0-hubmove"; + +/// This migration is used to convert the satellite contract on Neutron into Assembly. +/// Cosmwasm migration is meant to be executed from multisig controlled by Astroport to prevent abnormal subsequences +/// and be able to react promptly in case of any issues. +/// +/// Mainnet contract which is only subject of this migration: https://neutron.celat.one/neutron-1/contracts/neutron1ffus553eet978k024lmssw0czsxwr97mggyv85lpcsdkft8v9ufsz3sa07 +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, env: Env, msg: InstantiateMsg) -> Result { + cw2::assert_contract_version( + deps.storage, + EXPECTED_CONTRACT_NAME, + EXPECTED_CONTRACT_VERSION, + )?; + + // Clear satellite's state + astro_satellite::state::LATEST_HUB_SIGNAL_TIME.remove(deps.storage); + astro_satellite::state::REPLY_DATA.remove(deps.storage); + astro_satellite::state::RESULTS.clear(deps.storage); + + // Close old governance channel with Terra + let satellite_config = astro_satellite::state::CONFIG.load(deps.storage)?; + let close_msg = IbcMsg::CloseChannel { + channel_id: satellite_config.gov_channel.ok_or_else(|| { + StdError::generic_err("Missing governance channel in satellite config") + })?, + }; + + let cw_admin = deps + .querier + .query_wasm_contract_info(&env.contract.address)? + .admin + .unwrap(); + // Even though info object is ignored in instantiate, we provide it for clarity + let info = MessageInfo { + sender: Addr::unchecked(cw_admin), + funds: vec![], + }; + // Instantiate Assembly state. + // Config and cw2 info will be overwritten. + let response = instantiate(deps, env, info, msg)?.add_message(close_msg); + + Ok(response) +} From 80d24879762d22ed3b500ebeeef3acd32699e4c1 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 9 Feb 2024 15:06:36 +0400 Subject: [PATCH 31/47] small refactoring --- contracts/assembly/src/migration.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/assembly/src/migration.rs b/contracts/assembly/src/migration.rs index 7a254abf..015fcdb5 100644 --- a/contracts/assembly/src/migration.rs +++ b/contracts/assembly/src/migration.rs @@ -46,7 +46,5 @@ pub fn migrate(deps: DepsMut, env: Env, msg: InstantiateMsg) -> Result Date: Sat, 10 Feb 2024 13:48:56 +0400 Subject: [PATCH 32/47] set attributes on migration --- contracts/assembly/src/contract.rs | 4 ++-- contracts/assembly/src/migration.rs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index b3091c3d..3d27e739 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -23,8 +23,8 @@ use crate::utils::{calc_total_voting_power_at, calc_voting_power}; use cosmwasm_std::entry_point; // Contract name and version used for migration. -const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Creates a new contract with the specified parameters in the `msg` variable. #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/assembly/src/migration.rs b/contracts/assembly/src/migration.rs index 015fcdb5..883c3456 100644 --- a/contracts/assembly/src/migration.rs +++ b/contracts/assembly/src/migration.rs @@ -1,4 +1,4 @@ -use crate::contract::instantiate; +use crate::contract::{instantiate, CONTRACT_NAME, CONTRACT_VERSION}; use crate::error::ContractError; use astroport_governance::assembly::InstantiateMsg; #[cfg(not(feature = "library"))] @@ -46,5 +46,17 @@ pub fn migrate(deps: DepsMut, env: Env, msg: InstantiateMsg) -> Result Date: Mon, 12 Feb 2024 12:12:06 +0400 Subject: [PATCH 33/47] filter insecure msgs in check_messages --- contracts/assembly/src/contract.rs | 13 ++++- contracts/assembly/tests/common/helper.rs | 14 +++++- contracts/assembly/tests/integration.rs | 59 ++++++++++++++++++++++- 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 3d27e739..ca42fb49 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -4,7 +4,7 @@ use astroport::asset::addr_opt_validate; use astroport::staking; use cosmwasm_std::{ attr, coins, wasm_execute, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, - StdError, SubMsg, Uint128, Uint64, + StdError, SubMsg, Uint128, Uint64, WasmMsg, }; use cw2::set_contract_version; use cw_utils::must_pay; @@ -393,6 +393,17 @@ pub fn execute_proposal( /// Checks that proposal messages are correct. pub fn check_messages(env: Env, mut messages: Vec) -> Result { + messages.iter().try_for_each(|msg| match msg { + CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr, .. }) + if contract_addr == env.contract.address.as_str() => + { + Err(StdError::generic_err( + "Can't check messages with a migration message of the contract itself", + )) + } + _ => Ok(()), + })?; + messages.push( wasm_execute( env.contract.address, diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index e6f63605..4271badc 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -5,7 +5,7 @@ use astroport::staking; use cosmwasm_std::testing::MockApi; use cosmwasm_std::{ coin, coins, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Empty, Env, GovMsg, IbcMsg, - IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, + IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, WasmMsg, }; use cw_multi_test::{ App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, @@ -178,10 +178,20 @@ impl Helper { &default_init_msg(&staking, &builder_unlock), &[], String::from("Astroport Assembly"), - None, + Some(owner.to_string()), ) .unwrap(); + app.execute( + owner.clone(), + WasmMsg::UpdateAdmin { + contract_addr: assembly.to_string(), + admin: assembly.to_string(), + } + .into(), + ) + .unwrap(); + Ok(Self { app, owner: owner.clone(), diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index a31e8285..566b8ea5 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::str::FromStr; -use cosmwasm_std::{coin, coins, Addr, BankMsg, Decimal, Uint128}; +use cosmwasm_std::{coin, coins, Addr, BankMsg, Decimal, Uint128, WasmMsg}; use cw_multi_test::Executor; use astro_assembly::error::ContractError; @@ -516,6 +516,63 @@ fn test_check_messages() { err.root_cause().to_string(), ContractError::MessagesCheckPassed {}.to_string() ); + + // Try to update contract admin + let err = helper + .app + .execute_contract( + Addr::unchecked("permissionless"), + assembly.clone(), + &ExecuteMsg::CheckMessages(vec![WasmMsg::UpdateAdmin { + contract_addr: assembly.to_string(), + admin: "hacker".to_string(), + } + .into()]), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::MessagesCheckPassed {} + ); + + // Try to clear contract admin + let err = helper + .app + .execute_contract( + Addr::unchecked("permissionless"), + assembly.clone(), + &ExecuteMsg::CheckMessages(vec![WasmMsg::ClearAdmin { + contract_addr: assembly.to_string(), + } + .into()]), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::MessagesCheckPassed {} + ); + + // Can't check assembly migration message + let err = helper + .app + .execute_contract( + Addr::unchecked("permissionless"), + assembly.clone(), + &ExecuteMsg::CheckMessages(vec![WasmMsg::Migrate { + contract_addr: assembly.to_string(), + new_code_id: 100, + msg: Default::default(), + } + .into()]), + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Can't check messages with a migration message of the contract itself" + ); } #[test] From 3fd6ff8b647bdffa086e416fadbe7943216158f6 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:43:09 +0500 Subject: [PATCH 34/47] improve check messages validation --- contracts/assembly/src/contract.rs | 23 +++++++++++------- contracts/assembly/tests/common/stargate.rs | 3 ++- contracts/assembly/tests/integration.rs | 26 +++++++++++++++++---- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index ca42fb49..6af4d61a 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use astroport::asset::addr_opt_validate; use astroport::staking; use cosmwasm_std::{ - attr, coins, wasm_execute, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, - StdError, SubMsg, Uint128, Uint64, WasmMsg, + attr, coins, wasm_execute, Api, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, + Response, StdError, SubMsg, Uint128, Uint64, WasmMsg, }; use cw2::set_contract_version; use cw_utils::must_pay; @@ -125,7 +125,7 @@ pub fn execute( ExecuteMsg::CastVote { proposal_id, vote } => cast_vote(deps, env, info, proposal_id, vote), ExecuteMsg::EndProposal { proposal_id } => end_proposal(deps, env, proposal_id), ExecuteMsg::ExecuteProposal { proposal_id } => execute_proposal(deps, env, proposal_id), - ExecuteMsg::CheckMessages(messages) => check_messages(env, messages), + ExecuteMsg::CheckMessages(messages) => check_messages(deps.api, env, messages), ExecuteMsg::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}), ExecuteMsg::UpdateConfig(config) => update_config(deps, env, info, config), ExecuteMsg::IBCProposalCompleted { @@ -392,15 +392,22 @@ pub fn execute_proposal( } /// Checks that proposal messages are correct. -pub fn check_messages(env: Env, mut messages: Vec) -> Result { +pub fn check_messages( + api: &dyn Api, + env: Env, + mut messages: Vec, +) -> Result { messages.iter().try_for_each(|msg| match msg { - CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr, .. }) - if contract_addr == env.contract.address.as_str() => - { + CosmosMsg::Wasm( + WasmMsg::Migrate { contract_addr, .. } | WasmMsg::UpdateAdmin { contract_addr, .. }, + ) if api.addr_validate(contract_addr)? == env.contract.address => { Err(StdError::generic_err( - "Can't check messages with a migration message of the contract itself", + "Can't check messages with a migration or update admin message of the contract itself", )) } + CosmosMsg::Stargate { type_url, .. } if type_url.contains("MsgGrant") => Err( + StdError::generic_err("Can't check messages with a MsgGrant message"), + ), _ => Ok(()), })?; diff --git a/contracts/assembly/tests/common/stargate.rs b/contracts/assembly/tests/common/stargate.rs index b8282c92..f15f01b2 100644 --- a/contracts/assembly/tests/common/stargate.rs +++ b/contracts/assembly/tests/common/stargate.rs @@ -92,8 +92,9 @@ impl Stargate for StargateKeeper { router.sudo(api, storage, block, bank_sudo.into()) } + "/cosmos.authz.v1beta1.MsgGrant" => Ok(AppResponse::default()), _ => Err(anyhow::anyhow!( - "Unexpected exec msg {type_url} from {sender:?}", + "Unexpected exec msg {type_url} from {sender}", )), } } diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/integration.rs index 566b8ea5..e7d56099 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/integration.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::str::FromStr; -use cosmwasm_std::{coin, coins, Addr, BankMsg, Decimal, Uint128, WasmMsg}; +use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Decimal, Uint128, WasmMsg}; use cw_multi_test::Executor; use astro_assembly::error::ContractError; @@ -532,8 +532,8 @@ fn test_check_messages() { ) .unwrap_err(); assert_eq!( - err.downcast::().unwrap(), - ContractError::MessagesCheckPassed {} + err.root_cause().to_string(), + "Generic error: Can't check messages with a migration or update admin message of the contract itself" ); // Try to clear contract admin @@ -571,7 +571,25 @@ fn test_check_messages() { .unwrap_err(); assert_eq!( err.root_cause().to_string(), - "Generic error: Can't check messages with a migration message of the contract itself" + "Generic error: Can't check messages with a migration or update admin message of the contract itself" + ); + + // Check authz MsgGrant message + let err = helper + .app + .execute_contract( + Addr::unchecked("permissionless"), + assembly.clone(), + &ExecuteMsg::CheckMessages(vec![CosmosMsg::Stargate { + type_url: "/cosmos.authz.v1beta1.MsgGrant".to_string(), + value: Default::default(), + }]), + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Can't check messages with a MsgGrant message" ); } From c470451667fa2c6cfe0715adb80553c2fdd476a6 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 29 Feb 2024 15:45:31 +0500 Subject: [PATCH 35/47] fix(builder_unlock): total contract revamp; add historical queries --- Cargo.lock | 1 + contracts/assembly/src/unit_tests.rs | 4 +- contracts/assembly/src/utils.rs | 17 +- ...integration.rs => assembly_integration.rs} | 59 ++ contracts/assembly/tests/common/helper.rs | 15 +- contracts/builder_unlock/.cargo/config | 6 - contracts/builder_unlock/Cargo.toml | 4 +- contracts/builder_unlock/NOTICE | 14 - .../builder_unlock/examples/unlock_schema.rs | 2 +- contracts/builder_unlock/src/contract.rs | 680 +++++------------- contracts/builder_unlock/src/error.rs | 65 ++ contracts/builder_unlock/src/lib.rs | 2 + contracts/builder_unlock/src/query.rs | 147 ++++ contracts/builder_unlock/src/state.rs | 247 ++++++- .../tests/builder_unlock_integration.rs | 343 +++++---- .../src/builder_unlock.rs | 287 ++++---- 16 files changed, 1054 insertions(+), 839 deletions(-) rename contracts/assembly/tests/{integration.rs => assembly_integration.rs} (92%) delete mode 100644 contracts/builder_unlock/.cargo/config delete mode 100644 contracts/builder_unlock/NOTICE create mode 100644 contracts/builder_unlock/src/error.rs create mode 100644 contracts/builder_unlock/src/query.rs diff --git a/Cargo.lock b/Cargo.lock index 4fb1e32e..274eb5e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw-utils 1.0.3", "cw2 1.1.2", + "thiserror", ] [[package]] diff --git a/contracts/assembly/src/unit_tests.rs b/contracts/assembly/src/unit_tests.rs index eec2f119..85363f5f 100644 --- a/contracts/assembly/src/unit_tests.rs +++ b/contracts/assembly/src/unit_tests.rs @@ -39,10 +39,10 @@ fn custom_wasm_handler(request: &WasmQuery) -> QuerierResult { )) } else if matches!( from_json(msg), - Ok(astroport_governance::builder_unlock::msg::QueryMsg::State {}) + Ok(astroport_governance::builder_unlock::QueryMsg::State { .. }) ) { SystemResult::Ok(ContractResult::Ok( - to_json_binary(&astroport_governance::builder_unlock::msg::StateResponse { + to_json_binary(&astroport_governance::builder_unlock::State { total_astro_deposited: Default::default(), remaining_astro_tokens: Default::default(), unallocated_astro_tokens: Default::default(), diff --git a/contracts/assembly/src/utils.rs b/contracts/assembly/src/utils.rs index eed5318d..3ed2c997 100644 --- a/contracts/assembly/src/utils.rs +++ b/contracts/assembly/src/utils.rs @@ -3,8 +3,8 @@ use cosmwasm_std::{Deps, QuerierWrapper, StdResult, Uint128}; use astroport_governance::assembly::Config; use astroport_governance::assembly::Proposal; -use astroport_governance::builder_unlock::msg::{ - AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, StateResponse, +use astroport_governance::builder_unlock::{ + AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, State, }; use crate::state::CONFIG; @@ -28,10 +28,13 @@ pub fn calc_voting_power(deps: Deps, sender: String, proposal: &Proposal) -> Std let locked_amount: AllocationResponse = deps.querier.query_wasm_smart( config.builder_unlock_addr, - &BuilderUnlockQueryMsg::Allocation { account: sender }, + &BuilderUnlockQueryMsg::Allocation { + account: sender, + timestamp: Some(proposal.start_time - 1), + }, )?; - total += locked_amount.params.amount - locked_amount.status.astro_withdrawn; + total += locked_amount.status.amount - locked_amount.status.astro_withdrawn; Ok(total) } @@ -57,9 +60,11 @@ pub fn calc_total_voting_power_at( )?; // Total amount of ASTRO locked in the initial builder's unlock schedule - let builder_state: StateResponse = querier.query_wasm_smart( + let builder_state: State = querier.query_wasm_smart( &config.builder_unlock_addr, - &BuilderUnlockQueryMsg::State {}, + &BuilderUnlockQueryMsg::State { + timestamp: Some(timestamp), + }, )?; total += builder_state.remaining_astro_tokens; diff --git a/contracts/assembly/tests/integration.rs b/contracts/assembly/tests/assembly_integration.rs similarity index 92% rename from contracts/assembly/tests/integration.rs rename to contracts/assembly/tests/assembly_integration.rs index 566b8ea5..808195ea 100644 --- a/contracts/assembly/tests/integration.rs +++ b/contracts/assembly/tests/assembly_integration.rs @@ -814,3 +814,62 @@ fn test_queries() { assert_eq!(proposals.len(), 10); } + +#[test] +fn test_manipulate_governance_proposal() { + use astroport_governance::builder_unlock::ExecuteMsg as BuilderUnlockExecuteMsg; + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + let builder_unlock = helper.builder_unlock.clone(); + let user1 = Addr::unchecked("user1"); + let user2 = Addr::unchecked("user2"); + let user3 = Addr::unchecked("user3"); + // create allocations for user1 and user2 + helper.create_builder_allocation(&user1, 10_000); + helper.create_builder_allocation(&user2, 10_000); + // advance block + helper.next_block(10); + // create proposal + helper.get_xastro(&user1, PROPOSAL_REQUIRED_DEPOSIT.u128() + 1000_u128); + helper.submit_sample_proposal(&user1); + // user1 votes `yes` + helper + .cast_vote(1, &user1, ProposalVoteOption::For) + .unwrap(); // user2 votes `no` + helper + .cast_vote(1, &user2, ProposalVoteOption::Against) + .unwrap(); + // user1 propose new receiver to user3 + helper + .app + .execute_contract( + user1.clone(), + builder_unlock.clone(), + &BuilderUnlockExecuteMsg::ProposeNewReceiver { + new_receiver: user3.to_string(), + }, + &[], + ) + .unwrap(); + // user3 claim allocation + helper + .app + .execute_contract( + user3.clone(), + builder_unlock.clone(), + &BuilderUnlockExecuteMsg::ClaimReceiver { + prev_receiver: user1.to_string(), + }, + &[], + ) + .unwrap(); + + // user3 tries to vote `yes` but they didn't have any allocation before proposal start + let err = helper + .cast_vote(1, &user3, ProposalVoteOption::For) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NoVotingPower {} + ); +} diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index 4271badc..713faaec 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -18,7 +18,7 @@ use astroport_governance::assembly::{ MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE, MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, VOTING_PERIOD_INTERVAL, }; -use astroport_governance::builder_unlock::{AllocationParams, Schedule}; +use astroport_governance::builder_unlock::{CreateAllocationParams, Schedule}; use crate::common::stargate::StargateKeeper; @@ -58,7 +58,7 @@ fn builder_contract() -> Box> { Box::new(ContractWrapper::new_with_empty( builder_unlock::contract::execute, builder_unlock::contract::instantiate, - builder_unlock::contract::query, + builder_unlock::query::query, )) } @@ -154,7 +154,7 @@ impl Helper { let builder_unlock_code_id = app.store_code(builder_contract()); - let msg = astroport_governance::builder_unlock::msg::InstantiateMsg { + let msg = astroport_governance::builder_unlock::InstantiateMsg { owner: owner.to_string(), astro_denom: ASTRO_DENOM.to_string(), max_allocations_amount: Uint128::new(300_000_000_000000), @@ -241,16 +241,15 @@ impl Helper { .execute_contract( self.owner.clone(), self.builder_unlock.clone(), - &astroport_governance::builder_unlock::msg::ExecuteMsg::CreateAllocations { + &astroport_governance::builder_unlock::ExecuteMsg::CreateAllocations { allocations: vec![( recipient.to_string(), - AllocationParams { + CreateAllocationParams { amount: amount.into(), unlock_schedule: Schedule { duration: 10, ..Default::default() }, - proposed_receiver: None, }, )], }, @@ -398,7 +397,7 @@ impl Helper { .unwrap(); } - pub fn create_allocations(&mut self, allocations: Vec<(String, AllocationParams)>) { + pub fn create_allocations(&mut self, allocations: Vec<(String, CreateAllocationParams)>) { let amount = allocations .iter() .map(|params| params.1.amount.u128()) @@ -408,7 +407,7 @@ impl Helper { .execute_contract( Addr::unchecked("owner"), self.builder_unlock.clone(), - &astroport_governance::builder_unlock::msg::ExecuteMsg::CreateAllocations { + &astroport_governance::builder_unlock::ExecuteMsg::CreateAllocations { allocations, }, &coins(amount, ASTRO_DENOM), diff --git a/contracts/builder_unlock/.cargo/config b/contracts/builder_unlock/.cargo/config deleted file mode 100644 index a79b8fdb..00000000 --- a/contracts/builder_unlock/.cargo/config +++ /dev/null @@ -1,6 +0,0 @@ -[alias] -wasm = "build --release --target wasm32-unknown-unknown" -wasm-debug = "build --target wasm32-unknown-unknown" -unit-test = "test --lib" -integration-test = "test --test integration" -schema = "run --example vesting_schema" diff --git a/contracts/builder_unlock/Cargo.toml b/contracts/builder_unlock/Cargo.toml index 375f31cd..0a96a5e3 100644 --- a/contracts/builder_unlock/Cargo.toml +++ b/contracts/builder_unlock/Cargo.toml @@ -10,7 +10,6 @@ homepage = "https://astroport.fi" crate-type = ["cdylib", "rlib"] [features] -backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all init/handle/query exports library = [] @@ -20,8 +19,9 @@ cw-utils = "1" cosmwasm-std = "1.5" cw-storage-plus = "0.15" astroport-governance = { path = "../../packages/astroport-governance", version = "2" } -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "3" } cosmwasm-schema = "1.5" +thiserror = "1" [dev-dependencies] cw-multi-test = "0.20" \ No newline at end of file diff --git a/contracts/builder_unlock/NOTICE b/contracts/builder_unlock/NOTICE deleted file mode 100644 index 84b1c210..00000000 --- a/contracts/builder_unlock/NOTICE +++ /dev/null @@ -1,14 +0,0 @@ -CW20-Base: A reference implementation for fungible token on CosmWasm -Copyright (C) 2020 Confio OÃœ - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/contracts/builder_unlock/examples/unlock_schema.rs b/contracts/builder_unlock/examples/unlock_schema.rs index 636cd24b..3aefb2f7 100644 --- a/contracts/builder_unlock/examples/unlock_schema.rs +++ b/contracts/builder_unlock/examples/unlock_schema.rs @@ -1,4 +1,4 @@ -use astroport_governance::builder_unlock::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use astroport_governance::builder_unlock::{ExecuteMsg, InstantiateMsg, QueryMsg}; use cosmwasm_schema::write_api; fn main() { diff --git a/contracts/builder_unlock/src/contract.rs b/contracts/builder_unlock/src/contract.rs index 3f96b428..01dc9d7c 100644 --- a/contracts/builder_unlock/src/contract.rs +++ b/contracts/builder_unlock/src/contract.rs @@ -4,22 +4,16 @@ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_ow #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, coins, ensure, to_json_binary, Addr, BankMsg, Binary, Deps, DepsMut, Env, MessageInfo, - Order, Response, StdError, StdResult, Uint128, + attr, coins, ensure, BankMsg, DepsMut, Env, MessageInfo, Response, StdError, Uint128, }; use cw2::set_contract_version; -use cw_storage_plus::Bound; use cw_utils::{may_pay, must_pay}; -use astroport_governance::builder_unlock::msg::{ - AllocationResponse, ExecuteMsg, InstantiateMsg, QueryMsg, SimulateWithdrawResponse, - StateResponse, -}; -use astroport_governance::builder_unlock::{AllocationParams, AllocationStatus, Config, Schedule}; -use astroport_governance::{DEFAULT_LIMIT, MAX_LIMIT}; +use astroport_governance::builder_unlock::{Config, CreateAllocationParams, Schedule}; +use astroport_governance::builder_unlock::{ExecuteMsg, InstantiateMsg}; -use crate::contract::helpers::{compute_unlocked_amount, compute_withdraw_amount}; -use crate::state::{CONFIG, OWNERSHIP_PROPOSAL, PARAMS, STATE, STATUS}; +use crate::error::ContractError; +use crate::state::{Allocation, CONFIG, OWNERSHIP_PROPOSAL, PARAMS, STATE}; // Version and name used for contract migration. const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -29,10 +23,10 @@ const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, _info: MessageInfo, msg: InstantiateMsg, -) -> StdResult { +) -> Result { validate_native_denom(&msg.astro_denom)?; CONFIG.save( @@ -46,7 +40,7 @@ pub fn instantiate( set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - STATE.save(deps.storage, &Default::default())?; + STATE.save(deps.storage, &Default::default(), env.block.time.seconds())?; Ok(Response::default()) } @@ -80,43 +74,45 @@ pub fn instantiate( /// /// * **ExecuteMsg::UpdateConfig** Update contract configuration. #[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> StdResult { +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { match msg { ExecuteMsg::CreateAllocations { allocations } => { - execute_create_allocations(deps, info, allocations) + execute_create_allocations(deps, env, info, allocations) } ExecuteMsg::Withdraw {} => execute_withdraw(deps, env, info), ExecuteMsg::ProposeNewReceiver { new_receiver } => { - execute_propose_new_receiver(deps, info, new_receiver) + execute_propose_new_receiver(deps, env, info, new_receiver) } - ExecuteMsg::DropNewReceiver {} => execute_drop_new_receiver(deps, info), + ExecuteMsg::DropNewReceiver {} => execute_drop_new_receiver(deps, env, info), ExecuteMsg::ClaimReceiver { prev_receiver } => { - execute_claim_receiver(deps, info, prev_receiver) + execute_claim_receiver(deps, env, info, prev_receiver) } ExecuteMsg::IncreaseAllocation { receiver, amount } => { let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(StdError::generic_err( - "Only the contract owner can increase allocations", - )); - } - let deposit_amount = may_pay(&info, &config.astro_denom) - .map_err(|err| StdError::generic_err(err.to_string()))?; - - execute_increase_allocation(deps, &config, receiver, amount, deposit_amount) + ensure!( + info.sender == config.owner, + StdError::generic_err("Only the contract owner can increase allocations") + ); + let deposit_amount = may_pay(&info, &config.astro_denom)?; + + execute_increase_allocation(deps, env, &config, receiver, amount, deposit_amount) } ExecuteMsg::DecreaseAllocation { receiver, amount } => { execute_decrease_allocation(deps, env, info, receiver, amount) } ExecuteMsg::TransferUnallocated { amount, recipient } => { - execute_transfer_unallocated(deps, info, amount, recipient) + execute_transfer_unallocated(deps, env, info, amount, recipient) } ExecuteMsg::ProposeNewOwner { new_owner, expires_in, } => { - let config: Config = CONFIG.load(deps.storage)?; - + let config = CONFIG.load(deps.storage)?; propose_new_owner( deps, info, @@ -126,21 +122,23 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S config.owner, OWNERSHIP_PROPOSAL, ) + .map_err(Into::into) } ExecuteMsg::DropOwnershipProposal {} => { - let config: Config = CONFIG.load(deps.storage)?; - + let config = CONFIG.load(deps.storage)?; drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) } ExecuteMsg::ClaimOwnership {} => { claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { - CONFIG.update::<_, StdError>(deps.storage, |mut v| { - v.owner = new_owner; - Ok(v) - })?; - - Ok(()) + CONFIG + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + }) + .map(|_| ()) }) + .map_err(Into::into) } ExecuteMsg::UpdateConfig { new_max_allocations_amount, @@ -151,36 +149,6 @@ pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> S } } -/// Expose available contract queries. -/// -/// ## Queries -/// * **QueryMsg::Config {}** Return the contract configuration. -/// -/// * **QueryMsg::State {}** Return the contract state (number of ASTRO that still need to be withdrawn). -/// -/// * **QueryMsg::Allocation {}** Return the allocation details for a specific account. -/// -/// * **QueryMsg::UnlockedTokens {}** Return the amount of unlocked ASTRO for a specific account. -/// -/// * **QueryMsg::SimulateWithdraw {}** Return the result of a withdrawal simulation. -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), - QueryMsg::State {} => to_json_binary(&query_state(deps)?), - QueryMsg::Allocation { account } => to_json_binary(&query_allocation(deps, account)?), - QueryMsg::UnlockedTokens { account } => { - to_json_binary(&query_tokens_unlocked(deps, env, account)?) - } - QueryMsg::SimulateWithdraw { account, timestamp } => { - to_json_binary(&query_simulate_withdraw(deps, env, account, timestamp)?) - } - QueryMsg::Allocations { start_after, limit } => { - to_json_binary(&query_allocations(deps, start_after, limit)?) - } - } -} - /// Admin function facilitating the creation of new allocations. /// /// * **creator** allocations creator (the contract admin). @@ -190,11 +158,12 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { /// * **deposit_amount** tokens sent along with the call (should equal the sum of allocation amounts) /// /// * **deposit_amount** new allocations being created. -fn execute_create_allocations( +pub fn execute_create_allocations( deps: DepsMut, + env: Env, info: MessageInfo, - allocations: Vec<(String, AllocationParams)>, -) -> StdResult { + allocations: Vec<(String, CreateAllocationParams)>, +) -> Result { let config = CONFIG.load(deps.storage)?; ensure!( @@ -202,82 +171,62 @@ fn execute_create_allocations( StdError::generic_err("Only the contract owner can create allocations",) ); - let deposit_amount = must_pay(&info, &config.astro_denom) - .map_err(|err| StdError::generic_err(err.to_string()))?; - - if deposit_amount - != allocations - .iter() - .map(|params| params.1.amount) - .sum::() - { - return Err(StdError::generic_err("ASTRO deposit amount mismatch")); - } + let deposit_amount = must_pay(&info, &config.astro_denom)?; + let expected_deposit: Uint128 = allocations.iter().map(|(_, params)| params.amount).sum(); + ensure!( + deposit_amount == expected_deposit, + ContractError::DepositAmountMismatch { + expected: expected_deposit, + got: deposit_amount, + } + ); let mut state = STATE.load(deps.storage)?; state.total_astro_deposited += deposit_amount; state.remaining_astro_tokens += deposit_amount; - if state.total_astro_deposited > config.max_allocations_amount { - return Err(StdError::generic_err(format!( - "The total allocation for all recipients cannot exceed total ASTRO amount allocated to unlock (currently {} ASTRO)", - config.max_allocations_amount, - ))); - } + ensure!( + state.total_astro_deposited <= config.max_allocations_amount, + ContractError::TotalAllocationExceedsAmount(config.max_allocations_amount) + ); + + let block_ts = env.block.time.seconds(); for (user_unchecked, params) in allocations { - params.validate(&user_unchecked)?; let user = deps.api.addr_validate(&user_unchecked)?; - - if PARAMS.has(deps.storage, &user) { - return Err(StdError::generic_err(format!( - "Allocation (params) already exists for {user}" - ))); - } - PARAMS.save(deps.storage, &user, ¶ms)?; - STATUS.save(deps.storage, &user, &AllocationStatus::new())?; + let allocation = Allocation::new_allocation(deps.storage, block_ts, &user, params)?; + allocation.save(deps.storage)?; } - STATE.save(deps.storage, &state)?; + STATE.save(deps.storage, &state, block_ts)?; + Ok(Response::default()) } /// Allow allocation recipients to withdraw unlocked ASTRO. -fn execute_withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult { - let params = PARAMS.load(deps.storage, &info.sender)?; - - if params.proposed_receiver.is_some() { - return Err(StdError::generic_err( - "You may not withdraw once you proposed new receiver!", - )); - } - - let mut status = STATUS.load(deps.storage, &info.sender)?; - - let SimulateWithdrawResponse { astro_to_withdraw } = - compute_withdraw_amount(env.block.time.seconds(), ¶ms, &status); +pub fn execute_withdraw( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let block_ts = env.block.time.seconds(); + let mut allocation = Allocation::must_load(deps.storage, block_ts, &info.sender)?; - if astro_to_withdraw.is_zero() { - return Err(StdError::generic_err("No unlocked ASTRO to be withdrawn")); - } + let astro_to_withdraw = allocation.withdraw_and_update()?; + allocation.save(deps.storage)?; let mut state = STATE.load(deps.storage)?; - - status.astro_withdrawn += astro_to_withdraw; state.remaining_astro_tokens -= astro_to_withdraw; - // SAVE :: state & allocation - STATE.save(deps.storage, &state)?; - - // Update status - STATUS.save(deps.storage, &info.sender, &status)?; - - let config = CONFIG.load(deps.storage)?; + STATE.save(deps.storage, &state, block_ts)?; let bank_msg = BankMsg::Send { to_address: info.sender.to_string(), - amount: coins(astro_to_withdraw.u128(), config.astro_denom), + amount: coins( + astro_to_withdraw.u128(), + CONFIG.load(deps.storage)?.astro_denom, + ), }; Ok(Response::new() @@ -288,31 +237,18 @@ fn execute_withdraw(deps: DepsMut, env: Env, info: MessageInfo) -> StdResult StdResult { - let mut alloc_params = PARAMS.load(deps.storage, &info.sender)?; +) -> Result { + let mut allocation = + Allocation::must_load(deps.storage, env.block.time.seconds(), &info.sender)?; let new_receiver = deps.api.addr_validate(&new_receiver)?; - match alloc_params.proposed_receiver { - Some(proposed_receiver) => { - return Err(StdError::generic_err(format!( - "Proposed receiver already set to {proposed_receiver}" - ))); - } - None => { - if PARAMS.has(deps.storage, &new_receiver) { - return Err(StdError::generic_err( - "Invalid new_receiver. Proposed receiver already has an ASTRO allocation", - )); - } - - alloc_params.proposed_receiver = Some(new_receiver.clone()); - PARAMS.save(deps.storage, &info.sender, &alloc_params)?; - } - } + allocation.propose_new_receiver(deps.storage, &new_receiver)?; + allocation.save(deps.storage)?; Ok(Response::new() .add_attribute("action", "ProposeNewReceiver") @@ -320,20 +256,52 @@ fn execute_propose_new_receiver( } /// Drop the new proposed receiver for a specific allocation. -fn execute_drop_new_receiver(deps: DepsMut, info: MessageInfo) -> StdResult { - let mut alloc_params = PARAMS.load(deps.storage, &info.sender)?; +pub fn execute_drop_new_receiver( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let mut allocation = + Allocation::must_load(deps.storage, env.block.time.seconds(), &info.sender)?; - match alloc_params.proposed_receiver { - Some(proposed_receiver) => { - alloc_params.proposed_receiver = None; - PARAMS.save(deps.storage, &info.sender, &alloc_params)?; + let proposed_receiver = allocation.drop_proposed_receiver()?; + allocation.save(deps.storage)?; - Ok(Response::new() - .add_attribute("action", "DropNewReceiver") - .add_attribute("dropped_proposed_receiver", proposed_receiver)) - } - None => Err(StdError::generic_err("Proposed receiver not set")), + Ok(Response::new() + .add_attribute("action", "DropNewReceiver") + .add_attribute("dropped_proposed_receiver", proposed_receiver)) +} + +/// Allows a newly proposed allocation receiver to claim the ownership of that allocation. +/// +/// * **prev_receiver** this is the previous receiver for the allocation. +pub fn execute_claim_receiver( + deps: DepsMut, + env: Env, + info: MessageInfo, + prev_receiver: String, +) -> Result { + let prev_receiver_addr = deps.api.addr_validate(&prev_receiver)?; + let allocation = + Allocation::must_load(deps.storage, env.block.time.seconds(), &prev_receiver_addr)?; + + if allocation.params.proposed_receiver == Some(info.sender.clone()) { + ensure!( + !PARAMS.has(deps.storage, &info.sender), + ContractError::ProposedReceiverAlreadyHasAllocation {} + ); + + let new_allocation = allocation.claim_allocation(deps.storage, &info.sender)?; + new_allocation.save(deps.storage)?; + } else { + return Err(ContractError::ProposedReceiverMismatch {}); } + + Ok(Response::new().add_attributes(vec![ + attr("action", "ClaimReceiver"), + attr("prev_receiver", prev_receiver), + attr("receiver", info.sender), + ])) } /// Decrease an address' ASTRO allocation. @@ -341,48 +309,33 @@ fn execute_drop_new_receiver(deps: DepsMut, info: MessageInfo) -> StdResult StdResult { +) -> Result { let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(StdError::generic_err( - "Only the contract owner can decrease allocations", - )); - } + + ensure!( + info.sender == config.owner, + ContractError::UnauthorizedDecreaseAllocation {} + ); let receiver = deps.api.addr_validate(&receiver)?; + let block_ts = env.block.time.seconds(); + let mut allocation = Allocation::must_load(deps.storage, block_ts, &receiver)?; - let mut state = STATE.load(deps.storage)?; - let mut params = PARAMS.load(deps.storage, &receiver)?; - let mut status = STATUS.load(deps.storage, &receiver)?; - - let unlocked_amount = compute_unlocked_amount( - env.block.time.seconds(), - params.amount, - ¶ms.unlock_schedule, - status.unlocked_amount_checkpoint, - ); - let locked_amount = params.amount - unlocked_amount; + allocation.decrease_allocation(amount)?; + allocation.save(deps.storage)?; - if locked_amount < amount { - return Err(StdError::generic_err(format!( - "Insufficient amount of lock to decrease allocation, user has locked {locked_amount} ASTRO." - ))); - } + let mut state = STATE.load(deps.storage)?; - params.amount = params.amount.checked_sub(amount)?; - status.unlocked_amount_checkpoint = unlocked_amount; - state.unallocated_tokens = state.unallocated_tokens.checked_add(amount)?; + state.unallocated_astro_tokens = state.unallocated_astro_tokens.checked_add(amount)?; state.remaining_astro_tokens = state.remaining_astro_tokens.checked_sub(amount)?; - STATUS.save(deps.storage, &receiver, &status)?; - PARAMS.save(deps.storage, &receiver, ¶ms)?; - STATE.save(deps.storage, &state)?; + STATE.save(deps.storage, &state, block_ts)?; Ok(Response::new().add_attributes(vec![ attr("action", "execute_decrease_allocation"), @@ -393,53 +346,45 @@ fn execute_decrease_allocation( /// Increase an address' ASTRO allocation. /// -/// * **receiver** address that will have its allocation incrased. +/// * **receiver** address that will have its allocation increased. /// /// * **amount** ASTRO amount to increase the allocation by. /// -/// * **deposit_amount** is amount of ASTRO to increase the allocation by using CW20 Receive. -fn execute_increase_allocation( +/// * **deposit_amount** is amount of ASTRO to increase the allocation +pub fn execute_increase_allocation( deps: DepsMut, + env: Env, config: &Config, receiver: String, amount: Uint128, deposit_amount: Uint128, -) -> StdResult { +) -> Result { let receiver = deps.api.addr_validate(&receiver)?; + let block_ts = env.block.time.seconds(); + let mut allocation = Allocation::must_load(deps.storage, block_ts, &receiver)?; - match PARAMS.may_load(deps.storage, &receiver)? { - Some(mut params) => { - let mut state = STATE.load(deps.storage)?; - - state.total_astro_deposited = - state.total_astro_deposited.checked_add(deposit_amount)?; - state.unallocated_tokens = state.unallocated_tokens.checked_add(deposit_amount)?; - - if state.total_astro_deposited > config.max_allocations_amount { - return Err(StdError::generic_err(format!( - "The total allocation for all recipients cannot exceed total ASTRO amount allocated to unlock (currently {} ASTRO)", - config.max_allocations_amount, - ))); - } - - if state.unallocated_tokens < amount { - return Err(StdError::generic_err(format!( - "Insufficient unallocated ASTRO to increase allocation. Contract has: {} unallocated ASTRO.", - state.unallocated_tokens - ))); - } - - params.amount = params.amount.checked_add(amount)?; - state.unallocated_tokens = state.unallocated_tokens.checked_sub(amount)?; - state.remaining_astro_tokens = state.remaining_astro_tokens.checked_add(amount)?; - - PARAMS.save(deps.storage, &receiver, ¶ms)?; - STATE.save(deps.storage, &state)?; - } - None => { - return Err(StdError::generic_err("Proposed receiver not set")); - } - } + allocation.increase_allocation(amount)?; + allocation.save(deps.storage)?; + + let mut state = STATE.load(deps.storage)?; + + state.total_astro_deposited += deposit_amount; + state.unallocated_astro_tokens += deposit_amount; + + ensure!( + state.total_astro_deposited <= config.max_allocations_amount, + ContractError::TotalAllocationExceedsAmount(config.max_allocations_amount) + ); + + ensure!( + state.unallocated_astro_tokens >= amount, + ContractError::UnallocatedTokensExceedsTotalDeposited(state.unallocated_astro_tokens) + ); + + state.unallocated_astro_tokens = state.unallocated_astro_tokens.checked_sub(amount)?; + state.remaining_astro_tokens += amount; + + STATE.save(deps.storage, &state, block_ts)?; Ok(Response::new() .add_attribute("action", "execute_increase_allocation") @@ -452,30 +397,28 @@ fn execute_increase_allocation( /// * **amount** amount ASTRO to transfer. /// /// * **recipient** transfer recipient. -fn execute_transfer_unallocated( +pub fn execute_transfer_unallocated( deps: DepsMut, + env: Env, info: MessageInfo, amount: Uint128, recipient: Option, -) -> StdResult { +) -> Result { let config = CONFIG.load(deps.storage)?; - if config.owner != info.sender { - return Err(StdError::generic_err( - "Only contract owner can transfer unallocated ASTRO.", - )); - } + ensure!( + config.owner == info.sender, + ContractError::UnallocatedTransferUnauthorized {} + ); let mut state = STATE.load(deps.storage)?; - if state.unallocated_tokens < amount { - return Err(StdError::generic_err(format!( - "Insufficient unallocated ASTRO to transfer. Contract has: {} unallocated ASTRO.", - state.unallocated_tokens - ))); - } + ensure!( + state.unallocated_astro_tokens >= amount, + ContractError::InsufficientUnallocatedTokens(state.unallocated_astro_tokens) + ); - state.unallocated_tokens = state.unallocated_tokens.checked_sub(amount)?; + state.unallocated_astro_tokens = state.unallocated_astro_tokens.checked_sub(amount)?; state.total_astro_deposited = state.total_astro_deposited.checked_sub(amount)?; let recipient = addr_opt_validate(deps.api, &recipient)?.unwrap_or_else(|| info.sender.clone()); @@ -484,7 +427,7 @@ fn execute_transfer_unallocated( amount: coins(amount.u128(), config.astro_denom), }; - STATE.save(deps.storage, &state)?; + STATE.save(deps.storage, &state, env.block.time.seconds())?; Ok(Response::new() .add_attribute("action", "execute_transfer_unallocated") @@ -492,78 +435,23 @@ fn execute_transfer_unallocated( .add_message(bank_msg)) } -/// Allows a newly proposed allocation receiver to claim the ownership of that allocation. -/// -/// * **prev_receiver** this is the previous receiver for the allocation. -fn execute_claim_receiver( - deps: DepsMut, - info: MessageInfo, - prev_receiver: String, -) -> StdResult { - let prev_receiver_addr = deps.api.addr_validate(&prev_receiver)?; - let mut alloc_params = PARAMS.load(deps.storage, &prev_receiver_addr)?; - - match alloc_params.proposed_receiver { - Some(proposed_receiver) => { - if proposed_receiver == info.sender { - if let Some(sender_params) = PARAMS.may_load(deps.storage, &info.sender)? { - return Err(StdError::generic_err(format!( - "The proposed receiver already has an ASTRO allocation of {} ASTRO, that ends at {}", - sender_params.amount, - sender_params.unlock_schedule.start_time + sender_params.unlock_schedule.duration + sender_params.unlock_schedule.cliff, - ))); - } - - // Transfers allocation parameters - // 1. Save the allocation for the new receiver - alloc_params.proposed_receiver = None; - PARAMS.save(deps.storage, &info.sender, &alloc_params)?; - // 2. Remove the allocation info from the previous owner - PARAMS.remove(deps.storage, &prev_receiver_addr); - // Transfers Allocation Status - let status = STATUS.load(deps.storage, &prev_receiver_addr)?; - - STATUS.save(deps.storage, &info.sender, &status)?; - STATUS.remove(deps.storage, &prev_receiver_addr) - } else { - return Err(StdError::generic_err(format!( - "Proposed receiver mismatch, actual proposed receiver : {proposed_receiver}" - ))); - } - } - None => { - return Err(StdError::generic_err("Proposed receiver not set")); - } - } - - Ok(Response::new().add_attributes(vec![ - attr("action", "ClaimReceiver"), - attr("prev_receiver", prev_receiver), - attr("receiver", info.sender), - ])) -} - /// Updates builder unlock contract parameters. -fn update_config( +pub fn update_config( deps: DepsMut, info: MessageInfo, new_max_allocations_amount: Uint128, -) -> StdResult { +) -> Result { let mut config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(StdError::generic_err( - "Only the contract owner can change config", - )); - } + ensure!(info.sender == config.owner, ContractError::Unauthorized {}); let state = STATE.load(deps.storage)?; if new_max_allocations_amount < state.total_astro_deposited { return Err(StdError::generic_err(format!( - "The new max allocations amount {} can not be less than currently deposited {}", - new_max_allocations_amount, state.total_astro_deposited, - ))); + "The new max allocations amount {new_max_allocations_amount} can not be less than currently deposited {}", + state.total_astro_deposited, + )).into()); } config.max_allocations_amount = new_max_allocations_amount; @@ -575,204 +463,24 @@ fn update_config( } /// Updates builder unlock schedules for specified accounts. -fn update_unlock_schedules( +pub fn update_unlock_schedules( deps: DepsMut, env: Env, info: MessageInfo, new_unlock_schedules: Vec<(String, Schedule)>, -) -> StdResult { +) -> Result { let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(StdError::generic_err( - "Only the contract owner can change config", - )); - } + ensure!(info.sender == config.owner, ContractError::Unauthorized {}); + + let block_ts = env.block.time.seconds(); for (account, new_schedule) in new_unlock_schedules { let account_addr = deps.api.addr_validate(&account)?; - let mut params = PARAMS.load(deps.storage, &account_addr)?; - - let mut status = STATUS.load(deps.storage, &account_addr)?; - - let unlocked_amount_checkpoint = compute_unlocked_amount( - env.block.time.seconds(), - params.amount, - ¶ms.unlock_schedule, - status.unlocked_amount_checkpoint, - ); - - if unlocked_amount_checkpoint > status.unlocked_amount_checkpoint { - status.unlocked_amount_checkpoint = unlocked_amount_checkpoint; - STATUS.save(deps.storage, &account_addr, &status)?; - } - - params.update_schedule(new_schedule, &account)?; - PARAMS.save(deps.storage, &account_addr, ¶ms)?; + let mut allocation = Allocation::must_load(deps.storage, block_ts, &account_addr)?; + allocation.update_unlock_schedule(&new_schedule)?; + allocation.save(deps.storage)?; } Ok(Response::new().add_attribute("action", "update_unlock_schedules")) } - -/// Return the global distribution state. -pub fn query_state(deps: Deps) -> StdResult { - let state = STATE.load(deps.storage)?; - Ok(StateResponse { - total_astro_deposited: state.total_astro_deposited, - remaining_astro_tokens: state.remaining_astro_tokens, - unallocated_astro_tokens: state.unallocated_tokens, - }) -} - -/// Return information about a specific allocation. -/// -/// * **account** account whose allocation we query. -fn query_allocation(deps: Deps, account: String) -> StdResult { - let account_checked = deps.api.addr_validate(&account)?; - - Ok(AllocationResponse { - params: PARAMS - .may_load(deps.storage, &account_checked)? - .unwrap_or_default(), - status: STATUS - .may_load(deps.storage, &account_checked)? - .unwrap_or_default(), - }) -} - -/// Return information about a specific allocation. -/// -/// * **start_after** account from which to start querying. -/// -/// * **limit** max amount of entries to return. -fn query_allocations( - deps: Deps, - start_after: Option, - limit: Option, -) -> StdResult> { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; - let default_start; - - let start = if let Some(start_after) = start_after { - default_start = deps.api.addr_validate(&start_after)?; - Some(Bound::exclusive(&default_start)) - } else { - None - }; - - PARAMS - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect() -} - -/// Return the total amount of unlocked tokens for a specific account. -/// -/// * **account** account whose unlocked token amount we query. -fn query_tokens_unlocked(deps: Deps, env: Env, account: String) -> StdResult { - let account_checked = deps.api.addr_validate(&account)?; - - let params = PARAMS.load(deps.storage, &account_checked)?; - let status = STATUS.load(deps.storage, &account_checked)?; - - Ok(compute_unlocked_amount( - env.block.time.seconds(), - params.amount, - ¶ms.unlock_schedule, - status.unlocked_amount_checkpoint, - )) -} - -/// Simulate a token withdrawal. -/// -/// * **account** account for which we simulate a withdrawal. -/// -/// * **timestamp** timestamp where we assume the account would withdraw. -fn query_simulate_withdraw( - deps: Deps, - env: Env, - account: String, - timestamp: Option, -) -> StdResult { - let account_checked = deps.api.addr_validate(&account)?; - - let params = PARAMS.load(deps.storage, &account_checked)?; - let status = STATUS.load(deps.storage, &account_checked)?; - - Ok(compute_withdraw_amount( - timestamp.unwrap_or_else(|| env.block.time.seconds()), - ¶ms, - &status, - )) -} - -//---------------------------------------------------------------------------------------- -// Helper Functions -//---------------------------------------------------------------------------------------- - -mod helpers { - use cosmwasm_std::Uint128; - - use astroport_governance::builder_unlock::msg::SimulateWithdrawResponse; - use astroport_governance::builder_unlock::{AllocationParams, AllocationStatus, Schedule}; - - /// Computes number of tokens that are now unlocked for a given allocation - pub fn compute_unlocked_amount( - timestamp: u64, - amount: Uint128, - schedule: &Schedule, - unlock_checkpoint: Uint128, - ) -> Uint128 { - // Tokens haven't begun unlocking - if timestamp < schedule.start_time + schedule.cliff { - unlock_checkpoint - } else if (timestamp < schedule.start_time + schedule.duration) && schedule.duration != 0 { - // If percent_at_cliff is set, then this amount should be unlocked at cliff. - // The rest of tokens are vested linearly between cliff and end_time - let unlocked_amount = if let Some(percent_at_cliff) = schedule.percent_at_cliff { - let amount_at_cliff = amount * percent_at_cliff; - - amount_at_cliff - + amount.saturating_sub(amount_at_cliff).multiply_ratio( - timestamp - schedule.start_time - schedule.cliff, - schedule.duration - schedule.cliff, - ) - } else { - // Tokens unlock linearly between start time and end time - amount.multiply_ratio(timestamp - schedule.start_time, schedule.duration) - }; - - if unlocked_amount > unlock_checkpoint { - unlocked_amount - } else { - unlock_checkpoint - } - } - // After end time, all tokens are fully unlocked - else { - amount - } - } - - /// Computes number of tokens that are withdrawable for a given allocation - pub fn compute_withdraw_amount( - timestamp: u64, - params: &AllocationParams, - status: &AllocationStatus, - ) -> SimulateWithdrawResponse { - // "Unlocked" amount - let astro_unlocked = compute_unlocked_amount( - timestamp, - params.amount, - ¶ms.unlock_schedule, - status.unlocked_amount_checkpoint, - ); - - // Withdrawal amount is unlocked amount minus the amount already withdrawn - let astro_withdrawable = astro_unlocked - status.astro_withdrawn; - - SimulateWithdrawResponse { - astro_to_withdraw: astro_withdrawable, - } - } -} diff --git a/contracts/builder_unlock/src/error.rs b/contracts/builder_unlock/src/error.rs new file mode 100644 index 00000000..0730d5d2 --- /dev/null +++ b/contracts/builder_unlock/src/error.rs @@ -0,0 +1,65 @@ +use cosmwasm_std::{Addr, OverflowError, StdError, Uint128}; +use cw_utils::PaymentError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("The total allocation for all recipients cannot exceed total ASTRO amount allocated to unlock (currently {0} ASTRO)")] + TotalAllocationExceedsAmount(Uint128), + + #[error("Insufficient unallocated ASTRO to increase allocation. Contract has: {0} unallocated ASTRO")] + UnallocatedTokensExceedsTotalDeposited(Uint128), + + #[error("Proposed receiver not set")] + ProposedReceiverNotSet {}, + + #[error("Only contract owner can transfer unallocated ASTRO")] + UnallocatedTransferUnauthorized {}, + + #[error("Insufficient unallocated ASTRO to transfer. Contract has: {0} unallocated ASTRO")] + InsufficientUnallocatedTokens(Uint128), + + #[error("ASTRO deposit amount mismatch. Expected: {expected}, got: {got}")] + DepositAmountMismatch { expected: Uint128, got: Uint128 }, + + #[error("Allocation (params) already exists for {user}")] + AllocationExists { user: String }, + + #[error("You may not withdraw once you proposed new receiver!")] + WithdrawErrorWhenProposedReceiver {}, + + #[error("No unlocked ASTRO to be withdrawn")] + NoUnlockedAstro {}, + + #[error("Proposed receiver already set to {proposed_receiver}")] + ProposedReceiverAlreadySet { proposed_receiver: Addr }, + + #[error("Invalid new_receiver. Proposed receiver already has an ASTRO allocation")] + ProposedReceiverAlreadyHasAllocation {}, + + #[error("Only the contract owner can decrease allocations")] + UnauthorizedDecreaseAllocation {}, + + #[error( + "Insufficient amount of lock to decrease allocation, user has locked {locked_amount} ASTRO" + )] + InsufficientLockedAmount { locked_amount: Uint128 }, + + #[error("Proposed receiver is either not set or doesn't match the message sender")] + ProposedReceiverMismatch {}, + + #[error("{address} doesn't have allocation")] + NoAllocation { address: String }, +} diff --git a/contracts/builder_unlock/src/lib.rs b/contracts/builder_unlock/src/lib.rs index 3407c199..326d4720 100644 --- a/contracts/builder_unlock/src/lib.rs +++ b/contracts/builder_unlock/src/lib.rs @@ -1,2 +1,4 @@ pub mod contract; +pub mod error; +pub mod query; pub mod state; diff --git a/contracts/builder_unlock/src/query.rs b/contracts/builder_unlock/src/query.rs new file mode 100644 index 00000000..0f945c6a --- /dev/null +++ b/contracts/builder_unlock/src/query.rs @@ -0,0 +1,147 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_json_binary, Addr, Binary, Deps, Env, Order, StdError, StdResult, Uint128}; +use cw_storage_plus::Bound; + +use astroport_governance::builder_unlock::{ + AllocationParams, AllocationResponse, QueryMsg, SimulateWithdrawResponse, State, +}; +use astroport_governance::{DEFAULT_LIMIT, MAX_LIMIT}; + +use crate::error::ContractError; +use crate::state::{Allocation, CONFIG, PARAMS, STATE, STATUS}; + +/// Expose available contract queries. +/// +/// ## Queries +/// * **QueryMsg::Config {}** Return the contract configuration. +/// +/// * **QueryMsg::State {}** Return the contract state (number of ASTRO that still need to be withdrawn). +/// +/// * **QueryMsg::Allocation {}** Return the allocation details for a specific account. +/// +/// * **QueryMsg::UnlockedTokens {}** Return the amount of unlocked ASTRO for a specific account. +/// +/// * **QueryMsg::SimulateWithdraw {}** Return the result of a withdrawal simulation. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::State { timestamp } => to_json_binary(&query_state(deps, timestamp)?), + QueryMsg::Allocation { account, timestamp } => { + to_json_binary(&query_allocation(deps, account, timestamp)?) + } + QueryMsg::UnlockedTokens { account } => to_json_binary( + &query_tokens_unlocked(deps, env, account) + .map_err(|err| StdError::generic_err(err.to_string()))?, + ), + QueryMsg::SimulateWithdraw { account, timestamp } => to_json_binary( + &query_simulate_withdraw(deps, env, account, timestamp) + .map_err(|err| StdError::generic_err(err.to_string()))?, + ), + QueryMsg::Allocations { start_after, limit } => { + to_json_binary(&query_allocations(deps, start_after, limit)?) + } + } +} + +/// Query either historical or current contract state. +pub fn query_state(deps: Deps, timestamp: Option) -> StdResult { + if let Some(timestamp) = timestamp { + // Loads state at specific timestamp. State changes reflected **after** block has been produced. + STATE.may_load_at_height(deps.storage, timestamp) + } else { + // Loads latest state. Can load allocation state at the current block timestamp. + STATE.may_load(deps.storage) + } + .map(|state| state.unwrap_or_default()) +} + +/// Return either historical or current information about a specific allocation. +/// +/// * **account** account whose allocation we query. +/// +/// * **timestamp** timestamp at which we query the allocation. Optional. +pub fn query_allocation( + deps: Deps, + account: String, + timestamp: Option, +) -> StdResult { + let receiver = deps.api.addr_validate(&account)?; + let params = PARAMS + .may_load(deps.storage, &receiver)? + .unwrap_or_default(); + + let status = if let Some(timestamp) = timestamp { + // Loads allocation state at specific timestamp. State changes reflected **after** block has been produced. + STATUS + .may_load_at_height(deps.storage, &receiver, timestamp)? + .unwrap_or_default() + } else { + // Loads latest allocation state. Can load allocation state at the current block timestamp. + STATUS + .may_load(deps.storage, &receiver)? + .unwrap_or_default() + }; + + Ok(AllocationResponse { params, status }) +} + +/// Return information about a specific allocation. +/// +/// * **start_after** account from which to start querying. +/// +/// * **limit** max amount of entries to return. +pub fn query_allocations( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let default_start; + + let start = if let Some(start_after) = start_after { + default_start = deps.api.addr_validate(&start_after)?; + Some(Bound::exclusive(&default_start)) + } else { + None + }; + + PARAMS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect() +} + +/// Return the total amount of unlocked tokens for a specific account. +/// +/// * **account** account whose unlocked token amount we query. +pub fn query_tokens_unlocked( + deps: Deps, + env: Env, + account: String, +) -> Result { + let receiver = deps.api.addr_validate(&account)?; + let block_ts = env.block.time.seconds(); + let allocation = Allocation::must_load(deps.storage, block_ts, &receiver)?; + + Ok(allocation.compute_unlocked_amount(block_ts)) +} + +/// Simulate a token withdrawal. +/// +/// * **account** account for which we simulate a withdrawal. +/// +/// * **timestamp** timestamp where we assume the account would withdraw. +pub fn query_simulate_withdraw( + deps: Deps, + env: Env, + account: String, + timestamp: Option, +) -> Result { + let receiver = deps.api.addr_validate(&account)?; + let allocation = Allocation::must_load(deps.storage, env.block.time.seconds(), &receiver)?; + let timestamp = timestamp.unwrap_or_else(|| env.block.time.seconds()); + + Ok(allocation.compute_withdraw_amount(timestamp)) +} diff --git a/contracts/builder_unlock/src/state.rs b/contracts/builder_unlock/src/state.rs index 14507d0d..9457e459 100644 --- a/contracts/builder_unlock/src/state.rs +++ b/contracts/builder_unlock/src/state.rs @@ -1,16 +1,251 @@ use astroport::common::OwnershipProposal; -use cosmwasm_std::Addr; -use cw_storage_plus::{Item, Map}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ensure, Addr, StdResult, Storage, Uint128}; +use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy}; -use astroport_governance::builder_unlock::{AllocationParams, AllocationStatus, Config, State}; +use astroport_governance::builder_unlock::{ + AllocationParams, AllocationStatus, Config, CreateAllocationParams, Schedule, + SimulateWithdrawResponse, State, +}; + +use crate::error::ContractError; /// Stores the contract configuration pub const CONFIG: Item = Item::new("config"); -/// Stores global unlcok state such as the total amount of ASTRO tokens still to be distributed -pub const STATE: Item = Item::new("state"); +/// Stores global unlock state such as the total amount of ASTRO tokens still to be distributed +pub const STATE: SnapshotItem = SnapshotItem::new( + "state", + "state__checkpoint", + "state__changelog", + Strategy::EveryBlock, +); /// Allocation parameters for each unlock recipient pub const PARAMS: Map<&Addr, AllocationParams> = Map::new("params"); /// The status of each unlock schedule -pub const STATUS: Map<&Addr, AllocationStatus> = Map::new("status"); +pub const STATUS: SnapshotMap<&Addr, AllocationStatus> = SnapshotMap::new( + "status", + "status__checkpoint", + "status__changelog", + Strategy::EveryBlock, +); /// Contains a proposal to change contract ownership pub const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); + +#[cw_serde] +pub struct Allocation { + /// The allocation parameters + pub params: AllocationParams, + /// The allocation status + pub status: AllocationStatus, + /// Allocation owner + pub user: Addr, + /// Current block timestamp + pub block_ts: u64, +} + +impl Allocation { + pub fn must_load( + storage: &dyn Storage, + block_ts: u64, + user: &Addr, + ) -> Result { + let params = PARAMS + .load(storage, user) + .map_err(|_| ContractError::NoAllocation { + address: user.to_string(), + })?; + let status = STATUS.may_load(storage, user)?.unwrap_or_default(); + + Ok(Self { + params, + status, + user: user.clone(), + block_ts, + }) + } + + pub fn save(self, storage: &mut dyn Storage) -> StdResult<()> { + PARAMS.save(storage, &self.user, &self.params)?; + STATUS.save(storage, &self.user, &self.status, self.block_ts) + } + + pub fn new_allocation( + storage: &mut dyn Storage, + block_ts: u64, + user: &Addr, + params: CreateAllocationParams, + ) -> Result { + ensure!( + !PARAMS.has(storage, user), + ContractError::AllocationExists { + user: user.to_string() + } + ); + + params.validate(user.as_str())?; + + Ok(Self { + params: AllocationParams { + unlock_schedule: params.unlock_schedule, + proposed_receiver: None, + }, + status: AllocationStatus { + amount: params.amount, + astro_withdrawn: Default::default(), + unlocked_amount_checkpoint: Default::default(), + }, + user: user.clone(), + block_ts, + }) + } + + pub fn withdraw_and_update(&mut self) -> Result { + ensure!( + self.params.proposed_receiver.is_none(), + ContractError::WithdrawErrorWhenProposedReceiver {} + ); + + let SimulateWithdrawResponse { astro_to_withdraw } = + self.compute_withdraw_amount(self.block_ts); + + ensure!( + !astro_to_withdraw.is_zero(), + ContractError::NoUnlockedAstro {} + ); + + self.status.astro_withdrawn += astro_to_withdraw; + + Ok(astro_to_withdraw) + } + + pub fn propose_new_receiver( + &mut self, + storage: &dyn Storage, + new_receiver: &Addr, + ) -> Result<(), ContractError> { + match &self.params.proposed_receiver { + Some(proposed_receiver) => Err(ContractError::ProposedReceiverAlreadySet { + proposed_receiver: proposed_receiver.clone(), + }), + None => { + ensure!( + !PARAMS.has(storage, new_receiver), + ContractError::ProposedReceiverAlreadyHasAllocation {} + ); + + self.params.proposed_receiver = Some(new_receiver.clone()); + + Ok(()) + } + } + } + + pub fn drop_proposed_receiver(&mut self) -> Result { + match self.params.proposed_receiver.clone() { + Some(proposed_receiver) => { + self.params.proposed_receiver = None; + Ok(proposed_receiver) + } + None => Err(ContractError::ProposedReceiverNotSet {}), + } + } + + /// Produces new allocation object for new receiver. Old allocation is removed from state. + pub fn claim_allocation( + self, + storage: &mut dyn Storage, + new_receiver: &Addr, + ) -> Result { + PARAMS.remove(storage, &self.user); + STATUS.remove(storage, &self.user, self.block_ts)?; + + Ok(Self { + user: new_receiver.clone(), + params: AllocationParams { + proposed_receiver: None, + ..self.params + }, + ..self + }) + } + + /// Computes number of tokens that are now unlocked for a given allocation + pub fn compute_unlocked_amount(&self, timestamp: u64) -> Uint128 { + let (schedule, unlock_checkpoint, total_amount) = ( + &self.params.unlock_schedule, + self.status.unlocked_amount_checkpoint, + self.status.amount, + ); + + // Tokens haven't begun unlocking + if timestamp < schedule.start_time + schedule.cliff { + unlock_checkpoint + } else if (timestamp < schedule.start_time + schedule.duration) && schedule.duration != 0 { + // If percent_at_cliff is set, then this amount should be unlocked at cliff. + // The rest of tokens are vested linearly between cliff and end_time + let unlocked_amount = if let Some(percent_at_cliff) = schedule.percent_at_cliff { + let amount_at_cliff = total_amount * percent_at_cliff; + + amount_at_cliff + + total_amount.saturating_sub(amount_at_cliff).multiply_ratio( + timestamp - schedule.start_time - schedule.cliff, + schedule.duration - schedule.cliff, + ) + } else { + // Tokens unlock linearly between start time and end time + total_amount.multiply_ratio(timestamp - schedule.start_time, schedule.duration) + }; + + if unlocked_amount > unlock_checkpoint { + unlocked_amount + } else { + unlock_checkpoint + } + } + // After end time, all tokens are fully unlocked + else { + total_amount + } + } + + /// Computes number of tokens that are withdrawable for a given allocation + pub fn compute_withdraw_amount(&self, timestamp: u64) -> SimulateWithdrawResponse { + let astro_unlocked = self.compute_unlocked_amount(timestamp); + + // Withdrawal amount is unlocked amount minus the amount already withdrawn + SimulateWithdrawResponse { + astro_to_withdraw: astro_unlocked - self.status.astro_withdrawn, + } + } + + pub fn decrease_allocation(&mut self, amount: Uint128) -> Result<(), ContractError> { + let unlocked_amount = self.compute_unlocked_amount(self.block_ts); + let locked_amount = self.status.amount - unlocked_amount; + + ensure!( + locked_amount >= amount, + ContractError::InsufficientLockedAmount { locked_amount } + ); + + self.status.amount = self.status.amount.checked_sub(amount)?; + self.status.unlocked_amount_checkpoint = unlocked_amount; + + Ok(()) + } + + pub fn increase_allocation(&mut self, amount: Uint128) -> Result<(), ContractError> { + self.status.amount += amount; + Ok(()) + } + + pub fn update_unlock_schedule(&mut self, new_schedule: &Schedule) -> StdResult<()> { + let unlocked_amount_checkpoint = self.compute_unlocked_amount(self.block_ts); + + if unlocked_amount_checkpoint > self.status.unlocked_amount_checkpoint { + self.status.unlocked_amount_checkpoint = unlocked_amount_checkpoint; + } + + self.params + .update_schedule(new_schedule.clone(), self.user.as_str()) + } +} diff --git a/contracts/builder_unlock/tests/builder_unlock_integration.rs b/contracts/builder_unlock/tests/builder_unlock_integration.rs index 017700b5..63c2ef53 100644 --- a/contracts/builder_unlock/tests/builder_unlock_integration.rs +++ b/contracts/builder_unlock/tests/builder_unlock_integration.rs @@ -2,12 +2,14 @@ use std::time::SystemTime; use cosmwasm_std::{coin, coins, Addr, Decimal, StdResult, Timestamp, Uint128}; use cw_multi_test::{App, BasicApp, ContractWrapper, Executor}; +use cw_utils::PaymentError; -use astroport_governance::builder_unlock::msg::{ - AllocationResponse, ConfigResponse, ExecuteMsg, InstantiateMsg, QueryMsg, - SimulateWithdrawResponse, StateResponse, +use astroport_governance::builder_unlock::{ + AllocationParams, AllocationResponse, Config, ExecuteMsg, InstantiateMsg, QueryMsg, + SimulateWithdrawResponse, }; -use astroport_governance::builder_unlock::{AllocationParams, Schedule}; +use astroport_governance::builder_unlock::{CreateAllocationParams, Schedule, State}; +use builder_unlock::error::ContractError; pub const ASTRO_DENOM: &str = "factory/assembly/ASTRO"; @@ -34,7 +36,7 @@ fn init_contracts(app: &mut App) -> (Addr, InstantiateMsg) { let unlock_contract = Box::new(ContractWrapper::new( builder_unlock::contract::execute, builder_unlock::contract::instantiate, - builder_unlock::contract::query, + builder_unlock::query::query, )); let unlock_code_id = app.store_code(unlock_contract); @@ -76,10 +78,11 @@ fn check_alloc_amount(app: &mut App, contract_addr: &Addr, account: &Addr, amoun contract_addr, &QueryMsg::Allocation { account: account.to_string(), + timestamp: None, }, ) .unwrap(); - assert_eq!(res.params.amount, amount); + assert_eq!(res.status.amount, amount); } fn check_unlock_amount(app: &mut App, contract_addr: &Addr, account: &Addr, amount: Uint128) { @@ -100,7 +103,7 @@ fn proper_initialization() { let mut app = mock_app(); let (unlock_instance, init_msg) = init_contracts(&mut app); - let resp: ConfigResponse = app + let resp: Config = app .wrap() .query_wasm_smart(&unlock_instance, &QueryMsg::Config {}) .unwrap(); @@ -110,9 +113,9 @@ fn proper_initialization() { assert_eq!(init_msg.astro_denom, resp.astro_denom); // Check state - let resp: StateResponse = app + let resp: State = app .wrap() - .query_wasm_smart(&unlock_instance, &QueryMsg::State {}) + .query_wasm_smart(&unlock_instance, &QueryMsg::State { timestamp: None }) .unwrap(); assert_eq!(Uint128::zero(), resp.total_astro_deposited); @@ -157,7 +160,7 @@ fn test_transfer_ownership() { ) .unwrap(); - let resp: ConfigResponse = app + let resp: Config = app .wrap() .query_wasm_smart(&unlock_instance, &QueryMsg::Config {}) .unwrap(); @@ -172,10 +175,10 @@ fn test_create_allocations() { let mut app = mock_app(); let (unlock_instance, _) = init_contracts(&mut app); - let mut allocations: Vec<(String, AllocationParams)> = vec![]; + let mut allocations: Vec<(String, CreateAllocationParams)> = vec![]; allocations.push(( "investor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -183,12 +186,11 @@ fn test_create_allocations() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "advisor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -196,12 +198,11 @@ fn test_create_allocations() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "team_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -209,7 +210,6 @@ fn test_create_allocations() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); @@ -245,8 +245,8 @@ fn test_create_allocations() { .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - format!("Generic error: Must send reserve token '{ASTRO_DENOM}'") + err.downcast::().unwrap(), + ContractError::PaymentError(PaymentError::MissingDenom(ASTRO_DENOM.to_string())) ); // ###### ERROR :: ASTRO deposit amount mismatch ###### @@ -261,8 +261,11 @@ fn test_create_allocations() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: ASTRO deposit amount mismatch" + err.downcast::().unwrap(), + ContractError::DepositAmountMismatch { + expected: 15000000000000u128.into(), + got: 15000000000001u128.into() + } ); // ###### SUCCESSFULLY CREATES ALLOCATIONS ###### @@ -277,9 +280,9 @@ fn test_create_allocations() { .unwrap(); // Check state - let resp: StateResponse = app + let resp: State = app .wrap() - .query_wasm_smart(&unlock_instance, &QueryMsg::State {}) + .query_wasm_smart(&unlock_instance, &QueryMsg::State { timestamp: None }) .unwrap(); assert_eq!( resp.total_astro_deposited, @@ -297,10 +300,11 @@ fn test_create_allocations() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); - assert_eq!(resp.params.amount, Uint128::from(5_000_000_000000u64)); + assert_eq!(resp.status.amount, Uint128::from(5_000_000_000000u64)); assert_eq!(resp.status.astro_withdrawn, Uint128::from(0u64)); assert_eq!( resp.params.unlock_schedule, @@ -319,10 +323,11 @@ fn test_create_allocations() { &unlock_instance, &QueryMsg::Allocation { account: "advisor_1".to_string(), + timestamp: None, }, ) .unwrap(); - assert_eq!(resp.params.amount, Uint128::from(5_000_000_000000u64)); + assert_eq!(resp.status.amount, Uint128::from(5_000_000_000000u64)); assert_eq!(resp.status.astro_withdrawn, Uint128::from(0u64)); assert_eq!( resp.params.unlock_schedule, @@ -341,10 +346,11 @@ fn test_create_allocations() { &unlock_instance, &QueryMsg::Allocation { account: "team_1".to_string(), + timestamp: None, }, ) .unwrap(); - assert_eq!(resp.params.amount, Uint128::from(5_000_000_000000u64)); + assert_eq!(resp.status.amount, Uint128::from(5_000_000_000000u64)); assert_eq!(resp.status.astro_withdrawn, Uint128::from(0u64)); assert_eq!( resp.params.unlock_schedule, @@ -368,8 +374,10 @@ fn test_create_allocations() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: Allocation (params) already exists for investor_1" + err.downcast::().unwrap(), + ContractError::AllocationExists { + user: "investor_1".to_string() + } ); } @@ -378,10 +386,10 @@ fn test_withdraw() { let mut app = mock_app(); let (unlock_instance, _) = init_contracts(&mut app); - let mut allocations: Vec<(String, AllocationParams)> = vec![]; + let mut allocations: Vec<(String, CreateAllocationParams)> = vec![]; allocations.push(( "investor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -389,12 +397,11 @@ fn test_withdraw() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "advisor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -402,12 +409,11 @@ fn test_withdraw() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "team_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -415,10 +421,14 @@ fn test_withdraw() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); + app.update_block(|b| { + b.height += 17280; + b.time = Timestamp::from_seconds(1642402274) + }); + // SUCCESSFULLY CREATES ALLOCATIONS app.execute_contract( Addr::unchecked(OWNER), @@ -440,16 +450,15 @@ fn test_withdraw() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "astroport_governance::builder_unlock::AllocationParams not found" + err.downcast::().unwrap(), + ContractError::NoAllocation { + address: OWNER.to_string() + } ); - // ###### SUCCESSFULLY WITHDRAWS ASTRO #1 ###### - app.update_block(|b| { - b.height += 17280; - b.time = Timestamp::from_seconds(1642402275) - }); + app.next_block(1); + // ###### SUCCESSFULLY WITHDRAWS ASTRO #1 ###### let astro_bal_before = app.wrap().query_balance("investor_1", ASTRO_DENOM).unwrap(); app.execute_contract( @@ -459,11 +468,24 @@ fn test_withdraw() { &[], ) .unwrap(); + // ###### ERROR :: No unlocked ASTRO to be withdrawn ###### + let err = app + .execute_contract( + Addr::unchecked("investor_1"), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} + ); // Check state - let state_resp: StateResponse = app + let state_resp: State = app .wrap() - .query_wasm_smart(&unlock_instance, &QueryMsg::State {}) + .query_wasm_smart(&unlock_instance, &QueryMsg::State { timestamp: None }) .unwrap(); assert_eq!( state_resp.total_astro_deposited, @@ -474,6 +496,8 @@ fn test_withdraw() { Uint128::from(14_999_999_841452u64) ); + app.next_block(1); + // Check allocation #1 let alloc_resp: AllocationResponse = app .wrap() @@ -481,10 +505,11 @@ fn test_withdraw() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); - assert_eq!(alloc_resp.params.amount, Uint128::from(5_000_000_000000u64)); + assert_eq!(alloc_resp.status.amount, Uint128::from(5_000_000_000000u64)); assert_eq!(alloc_resp.status.astro_withdrawn, Uint128::from(158548u64)); let astro_bal_after = app.wrap().query_balance("investor_1", ASTRO_DENOM).unwrap(); @@ -495,7 +520,7 @@ fn test_withdraw() { ); // Check the number of unlocked tokens - let mut unlock_resp: Uint128 = app + let unlock_resp: Uint128 = app .wrap() .query_wasm_smart( &unlock_instance, @@ -504,21 +529,7 @@ fn test_withdraw() { }, ) .unwrap(); - assert_eq!(unlock_resp, Uint128::from(158548u64)); - - // ###### ERROR :: No unlocked ASTRO to be withdrawn ###### - let err = app - .execute_contract( - Addr::unchecked("investor_1"), - unlock_instance.clone(), - &ExecuteMsg::Withdraw {}, - &[], - ) - .unwrap_err(); - assert_eq!( - err.root_cause().to_string(), - "Generic error: No unlocked ASTRO to be withdrawn" - ); + assert_eq!(unlock_resp.u128(), 317097); // ###### SUCCESSFULLY WITHDRAWS ASTRO #2 ###### app.update_block(|b| { @@ -527,7 +538,7 @@ fn test_withdraw() { }); // Check the number of unlocked tokens - unlock_resp = app + let unlock_resp: Uint128 = app .wrap() .query_wasm_smart( &unlock_instance, @@ -563,16 +574,27 @@ fn test_withdraw() { ) .unwrap(); + let unlock_resp: Uint128 = app + .wrap() + .query_wasm_smart( + &unlock_instance, + &QueryMsg::UnlockedTokens { + account: "investor_1".to_string(), + }, + ) + .unwrap(); + let resp: AllocationResponse = app .wrap() .query_wasm_smart( &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); - assert_eq!(resp.params.amount, Uint128::from(5_000_000_000000u64)); + assert_eq!(resp.status.amount, Uint128::from(5_000_000_000000u64)); assert_eq!(resp.status.astro_withdrawn, unlock_resp); // ###### ERROR :: No unlocked ASTRO to be withdrawn ###### @@ -585,8 +607,8 @@ fn test_withdraw() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: No unlocked ASTRO to be withdrawn" + err.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} ); // ###### SUCCESSFULLY WITHDRAWS ASTRO #3 ###### @@ -597,7 +619,7 @@ fn test_withdraw() { }); // Check the number of unlocked tokens - unlock_resp = app + let unlock_resp: Uint128 = app .wrap() .query_wasm_smart( &unlock_instance, @@ -631,7 +653,7 @@ fn test_withdraw() { }); // Check the number of unlocked tokens - unlock_resp = app + let unlock_resp: Uint128 = app .wrap() .query_wasm_smart( &unlock_instance, @@ -673,6 +695,7 @@ fn test_withdraw() { &unlock_instance, &QueryMsg::Allocation { account: "team_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -701,10 +724,10 @@ fn test_propose_new_receiver() { let mut app = mock_app(); let (unlock_instance, _) = init_contracts(&mut app); - let mut allocations: Vec<(String, AllocationParams)> = vec![]; + let mut allocations: Vec<(String, CreateAllocationParams)> = vec![]; allocations.push(( "investor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -712,12 +735,11 @@ fn test_propose_new_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "advisor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -725,12 +747,11 @@ fn test_propose_new_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "team_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -738,7 +759,6 @@ fn test_propose_new_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); @@ -765,8 +785,10 @@ fn test_propose_new_receiver() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "astroport_governance::builder_unlock::AllocationParams not found" + err.downcast::().unwrap(), + ContractError::NoAllocation { + address: OWNER.to_string() + } ); // ###### ERROR :: Invalid new_receiver ###### @@ -781,8 +803,8 @@ fn test_propose_new_receiver() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: Invalid new_receiver. Proposed receiver already has an ASTRO allocation" + err.downcast::().unwrap(), + ContractError::ProposedReceiverAlreadyHasAllocation {} ); // ###### SUCCESSFULLY PROPOSES NEW RECEIVER ###### @@ -802,6 +824,7 @@ fn test_propose_new_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -822,8 +845,10 @@ fn test_propose_new_receiver() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: Proposed receiver already set to investor_1_new" + err.downcast::().unwrap(), + ContractError::ProposedReceiverAlreadySet { + proposed_receiver: Addr::unchecked("investor_1_new") + } ); } @@ -832,10 +857,10 @@ fn test_drop_new_receiver() { let mut app = mock_app(); let (unlock_instance, _) = init_contracts(&mut app); - let mut allocations: Vec<(String, AllocationParams)> = vec![]; + let mut allocations: Vec<(String, CreateAllocationParams)> = vec![]; allocations.push(( "investor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -843,12 +868,11 @@ fn test_drop_new_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "advisor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -856,12 +880,11 @@ fn test_drop_new_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "team_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -869,7 +892,6 @@ fn test_drop_new_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); @@ -894,8 +916,10 @@ fn test_drop_new_receiver() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "astroport_governance::builder_unlock::AllocationParams not found" + err.downcast::().unwrap(), + ContractError::NoAllocation { + address: OWNER.to_string() + } ); // ###### ERROR ::"Proposed receiver not set" ###### @@ -908,8 +932,8 @@ fn test_drop_new_receiver() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: Proposed receiver not set" + err.downcast::().unwrap(), + ContractError::ProposedReceiverNotSet {} ); // ###### SUCCESSFULLY DROP NEW RECEIVER ###### @@ -930,6 +954,7 @@ fn test_drop_new_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -952,6 +977,7 @@ fn test_drop_new_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -963,10 +989,10 @@ fn test_claim_receiver() { let mut app = mock_app(); let (unlock_instance, _) = init_contracts(&mut app); - let mut allocations: Vec<(String, AllocationParams)> = vec![]; + let mut allocations: Vec<(String, CreateAllocationParams)> = vec![]; allocations.push(( "investor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -974,12 +1000,11 @@ fn test_claim_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "advisor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -987,12 +1012,11 @@ fn test_claim_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "team_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -1000,7 +1024,6 @@ fn test_claim_receiver() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); @@ -1025,8 +1048,10 @@ fn test_claim_receiver() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "astroport_governance::builder_unlock::AllocationParams not found" + err.downcast::().unwrap(), + ContractError::NoAllocation { + address: OWNER.to_string() + } ); // ###### ERROR ::"Proposed receiver not set" ###### @@ -1041,8 +1066,8 @@ fn test_claim_receiver() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: Proposed receiver not set" + err.downcast::().unwrap(), + ContractError::ProposedReceiverMismatch {} ); // ###### SUCCESSFULLY CLAIMED BY NEW RECEIVER ###### @@ -1063,6 +1088,7 @@ fn test_claim_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -1097,12 +1123,12 @@ fn test_claim_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); assert_eq!( AllocationParams { - amount: Uint128::zero(), unlock_schedule: Schedule { start_time: 0u64, cliff: 0u64, @@ -1113,7 +1139,6 @@ fn test_claim_receiver() { }, alloc_resp_after.params ); - assert_eq!(alloc_resp_before.status, alloc_resp_after.status); // Check allocation state of new beneficiary let alloc_resp_after: AllocationResponse = app @@ -1122,12 +1147,12 @@ fn test_claim_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1_new".to_string(), + timestamp: None, }, ) .unwrap(); assert_eq!( AllocationParams { - amount: alloc_resp_before.params.amount, unlock_schedule: Schedule { start_time: alloc_resp_before.params.unlock_schedule.start_time, cliff: alloc_resp_before.params.unlock_schedule.cliff, @@ -1179,9 +1204,9 @@ fn test_increase_and_decrease_allocation() { let (unlock_instance, _) = init_contracts(&mut app); // Create allocations - let allocations: Vec<(String, AllocationParams)> = vec![( + let allocations: Vec<(String, CreateAllocationParams)> = vec![( "investor".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1_571_797_419u64, @@ -1189,7 +1214,6 @@ fn test_increase_and_decrease_allocation() { duration: 1_534_700u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )]; @@ -1252,8 +1276,10 @@ fn test_increase_and_decrease_allocation() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: Insufficient amount of lock to decrease allocation, user has locked 4918550856845 ASTRO." + err.downcast::().unwrap(), + ContractError::InsufficientLockedAmount { + locked_amount: 4918550856845u128.into() + } ); app.execute_contract( @@ -1274,17 +1300,20 @@ fn test_increase_and_decrease_allocation() { &Addr::unchecked("investor"), Uint128::new(81_449_143_155u128), ); - let res: StateResponse = app + let res: State = app .wrap() - .query_wasm_smart(unlock_instance.clone(), &QueryMsg::State {}) + .query_wasm_smart( + unlock_instance.clone(), + &QueryMsg::State { timestamp: None }, + ) .unwrap(); assert_eq!( res, - StateResponse { + State { total_astro_deposited: Uint128::new(5_000_000_000_000u128), remaining_astro_tokens: Uint128::new(3_983_710_171_369u128), - unallocated_astro_tokens: Uint128::new(1_000_000_000_000u128) + unallocated_astro_tokens: Uint128::new(1_000_000_000_000u128), } ); @@ -1301,8 +1330,8 @@ fn test_increase_and_decrease_allocation() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: Insufficient unallocated ASTRO to increase allocation. Contract has: 1000000000000 unallocated ASTRO." + err.downcast::().unwrap(), + ContractError::UnallocatedTokensExceedsTotalDeposited(1_000_000_000_000u128.into()) ); let balance_before = app.wrap().query_balance(OWNER, ASTRO_DENOM).unwrap().amount; @@ -1366,16 +1395,19 @@ fn test_increase_and_decrease_allocation() { .unwrap(); assert_eq!(res.astro_to_withdraw, Uint128::zero()); // Check state - let res: StateResponse = app + let res: State = app .wrap() - .query_wasm_smart(unlock_instance.clone(), &QueryMsg::State {}) + .query_wasm_smart( + unlock_instance.clone(), + &QueryMsg::State { timestamp: None }, + ) .unwrap(); assert_eq!( res, - StateResponse { + State { total_astro_deposited: Uint128::new(4_500_000_001_000u128), remaining_astro_tokens: Uint128::new(4_418_550_857_845u128), - unallocated_astro_tokens: Uint128::zero() + unallocated_astro_tokens: Uint128::zero(), } ); } @@ -1385,10 +1417,10 @@ fn test_updates_schedules() { let mut app = mock_app(); let (unlock_instance, _) = init_contracts(&mut app); - let mut allocations: Vec<(String, AllocationParams)> = vec![]; + let mut allocations: Vec<(String, CreateAllocationParams)> = vec![]; allocations.push(( "investor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -1396,12 +1428,11 @@ fn test_updates_schedules() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "advisor_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -1409,12 +1440,11 @@ fn test_updates_schedules() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( "team_1".to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(5_000_000_000000u64), unlock_schedule: Schedule { start_time: 1642402274u64, @@ -1422,7 +1452,6 @@ fn test_updates_schedules() { duration: 31536000u64, percent_at_cliff: None, }, - proposed_receiver: None, }, )); @@ -1438,9 +1467,9 @@ fn test_updates_schedules() { .unwrap(); // Check state before update parameters - let resp: StateResponse = app + let resp: State = app .wrap() - .query_wasm_smart(&unlock_instance, &QueryMsg::State {}) + .query_wasm_smart(&unlock_instance, &QueryMsg::State { timestamp: None }) .unwrap(); assert_eq!( resp.total_astro_deposited, @@ -1519,8 +1548,8 @@ fn test_updates_schedules() { ) .unwrap_err(); assert_eq!( - "Generic error: Only the contract owner can change config", - err.root_cause().to_string() + err.downcast::().unwrap(), + ContractError::Unauthorized {} ); let err = app @@ -1634,7 +1663,6 @@ fn test_updates_schedules() { ( Addr::unchecked("advisor_1"), AllocationParams { - amount: Uint128::new(5000000000000), unlock_schedule: Schedule { start_time: 1642402284u64, cliff: 8776000u64, @@ -1647,7 +1675,6 @@ fn test_updates_schedules() { ( Addr::unchecked("investor_1"), AllocationParams { - amount: Uint128::new(5000000000000), unlock_schedule: Schedule { start_time: 1642402274, cliff: 0, @@ -1660,7 +1687,6 @@ fn test_updates_schedules() { ( Addr::unchecked("team_1"), AllocationParams { - amount: Uint128::new(5000000000000), unlock_schedule: Schedule { start_time: 1642402284u64, cliff: 8776000u64, @@ -1687,7 +1713,6 @@ fn test_updates_schedules() { let comparing_values: Vec<(Addr, AllocationParams)> = vec![( Addr::unchecked("team_1"), AllocationParams { - amount: Uint128::new(5000000000000), unlock_schedule: Schedule { start_time: 1642402284u64, cliff: 8776000u64, @@ -1710,9 +1735,15 @@ fn check_allocation( ) -> StdResult<()> { let resp: AllocationResponse = app .wrap() - .query_wasm_smart(unlock_instance, &QueryMsg::Allocation { account }) + .query_wasm_smart( + unlock_instance, + &QueryMsg::Allocation { + account, + timestamp: None, + }, + ) .unwrap(); - assert_eq!(resp.params.amount, total_amount); + assert_eq!(resp.status.amount, total_amount); assert_eq!(resp.status.astro_withdrawn, astro_withdrawn); assert_eq!(resp.params.unlock_schedule, unlock_schedule); @@ -1746,7 +1777,7 @@ fn test_create_allocations_with_custom_cliff() { let mut allocations = vec![]; allocations.push(( investor1.to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(500_000_000000u64), unlock_schedule: Schedule { start_time: now_ts, @@ -1754,12 +1785,11 @@ fn test_create_allocations_with_custom_cliff() { duration: 3 * day * 365, // 3 years percent_at_cliff: None, }, - proposed_receiver: None, }, )); allocations.push(( investor2.to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(100_000_000000u64), unlock_schedule: Schedule { start_time: now_ts - day * 30, // 1 month ago @@ -1767,12 +1797,11 @@ fn test_create_allocations_with_custom_cliff() { duration: 3 * day * 365, // 3 years percent_at_cliff: Some(Decimal::from_ratio(1u8, 6u8)), // one sixth }, - proposed_receiver: None, }, )); allocations.push(( investor3.to_string(), - AllocationParams { + CreateAllocationParams { amount: Uint128::from(400_000_000000u64), unlock_schedule: Schedule { start_time: now_ts - day * 365, // 1 year ago @@ -1780,7 +1809,6 @@ fn test_create_allocations_with_custom_cliff() { duration: 3 * day * 365, // 3 years percent_at_cliff: Some(Decimal::percent(20)), // 20% at cliff }, - proposed_receiver: None, }, )); @@ -1805,8 +1833,8 @@ fn test_create_allocations_with_custom_cliff() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: No unlocked ASTRO to be withdrawn" + err.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} ); // Investor2 needs to wait 5 months more @@ -1819,8 +1847,8 @@ fn test_create_allocations_with_custom_cliff() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: No unlocked ASTRO to be withdrawn" + err.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} ); // Investor3 has 20% of his allocation unlocked + linearly unlocked astro for the last 6 months @@ -1849,8 +1877,8 @@ fn test_create_allocations_with_custom_cliff() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: No unlocked ASTRO to be withdrawn" + err.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} ); // Investor2 receives his one sixth of the allocation @@ -1960,8 +1988,21 @@ fn test_create_allocations_with_custom_cliff() { ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: No unlocked ASTRO to be withdrawn" + err.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} ); } } + +pub trait AppExtension { + fn next_block(&mut self, time: u64); +} + +impl AppExtension for App { + fn next_block(&mut self, time: u64) { + self.update_block(|block| { + block.time = block.time.plus_seconds(time); + block.height += 1 + }); + } +} diff --git a/packages/astroport-governance/src/builder_unlock.rs b/packages/astroport-governance/src/builder_unlock.rs index 871dede4..29cb0188 100644 --- a/packages/astroport-governance/src/builder_unlock.rs +++ b/packages/astroport-governance/src/builder_unlock.rs @@ -1,5 +1,113 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Decimal, StdError, Uint128}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Decimal, StdError, StdResult, Uint128}; + +#[cw_serde] +pub struct InstantiateMsg { + /// Account that can create new allocations + pub owner: String, + /// ASTRO token denom + pub astro_denom: String, + /// Max ASTRO tokens to allocate + pub max_allocations_amount: Uint128, +} + +/// This enum describes all the execute functions available in the contract. +#[cw_serde] +pub enum ExecuteMsg { + /// CreateAllocations creates new ASTRO allocations + CreateAllocations { + allocations: Vec<(String, CreateAllocationParams)>, + }, + /// Withdraw claims withdrawable ASTRO + Withdraw {}, + /// ProposeNewReceiver allows a user to change the receiver address for their ASTRO allocation + ProposeNewReceiver { new_receiver: String }, + /// DropNewReceiver allows a user to remove the previously proposed new receiver for their ASTRO allocation + DropNewReceiver {}, + /// ClaimReceiver allows newly proposed receivers to claim ASTRO allocations ownership + ClaimReceiver { prev_receiver: String }, + /// Increase the ASTRO allocation of a receiver + IncreaseAllocation { receiver: String, amount: Uint128 }, + /// Decrease the ASTRO allocation of a receiver + DecreaseAllocation { receiver: String, amount: Uint128 }, + /// Transfer unallocated tokens (only accessible to the owner) + TransferUnallocated { + amount: Uint128, + recipient: Option, + }, + /// Propose a new owner for the contract + ProposeNewOwner { new_owner: String, expires_in: u64 }, + /// Remove the ownership transfer proposal + DropOwnershipProposal {}, + /// Claim contract ownership + ClaimOwnership {}, + /// Update parameters in the contract configuration + UpdateConfig { new_max_allocations_amount: Uint128 }, + /// Update a schedule of allocation for specified accounts + UpdateUnlockSchedules { + new_unlock_schedules: Vec<(String, Schedule)>, + }, +} + +/// This enum describes all the queries available in the contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Config returns the configuration for this contract + #[returns(Config)] + Config {}, + /// State returns the state of this contract + #[returns(State)] + State { + // Timestamp at which we query. If none uses current block timestamp + timestamp: Option, + }, + /// Allocation returns the parameters and current status of an allocation + #[returns(AllocationResponse)] + Allocation { + /// Account whose allocation status we query + account: String, + // Timestamp at which we query. If none uses current block timestamp + timestamp: Option, + }, + /// Allocations returns a vector that contains builder unlock allocations by specified + /// parameters + #[returns(Vec<(String, AllocationParams)>)] + Allocations { + start_after: Option, + limit: Option, + }, + #[returns(Uint128)] + /// UnlockedTokens returns the unlocked tokens from an allocation + UnlockedTokens { + /// Account whose amount of unlocked ASTRO we query for + account: String, + }, + /// SimulateWithdraw simulates how many ASTRO will be released if a withdrawal is attempted + #[returns(SimulateWithdrawResponse)] + SimulateWithdraw { + /// Account for which we simulate a withdrawal + account: String, + /// Timestamp used to simulate how much ASTRO the account can withdraw + timestamp: Option, + }, +} + +/// This structure stores the parameters used to return the response when querying for an allocation data. +#[cw_serde] +pub struct AllocationResponse { + /// The allocation parameters + pub params: AllocationParams, + /// The allocation status + pub status: AllocationStatus, +} + +/// This structure stores the parameters used to return a response when simulating a withdrawal. +#[cw_serde] +pub struct SimulateWithdrawResponse { + /// Amount of ASTRO to receive + pub astro_to_withdraw: Uint128, +} /// This structure stores general parameters for the builder unlock contract. #[cw_serde] @@ -21,7 +129,7 @@ pub struct State { /// Currently available ASTRO tokens that still need to be unlocked and/or withdrawn pub remaining_astro_tokens: Uint128, /// Amount of ASTRO tokens deposited into the contract but not assigned to an allocation - pub unallocated_tokens: Uint128, + pub unallocated_astro_tokens: Uint128, } /// This structure stores the parameters describing a typical unlock schedule. @@ -40,22 +148,19 @@ pub struct Schedule { /// This structure stores the parameters used to describe an ASTRO allocation. #[cw_serde] -#[derive(Default)] -pub struct AllocationParams { +pub struct CreateAllocationParams { /// Total amount of ASTRO tokens allocated to a specific account pub amount: Uint128, /// Parameters controlling the unlocking process pub unlock_schedule: Schedule, - /// Proposed new receiver who will get the ASTRO allocation - pub proposed_receiver: Option, } -impl AllocationParams { - pub fn validate(&self, account: &str) -> Result<(), StdError> { +impl CreateAllocationParams { + pub fn validate(&self, account: &str) -> StdResult<()> { if self.unlock_schedule.cliff >= self.unlock_schedule.duration { return Err(StdError::generic_err(format!( - "The new cliff value must be less than the duration: {} < {}. Account: {}", - self.unlock_schedule.cliff, self.unlock_schedule.duration, account + "The new cliff value must be less than the duration: {} < {}. Account: {account}", + self.unlock_schedule.cliff, self.unlock_schedule.duration ))); }; @@ -65,20 +170,21 @@ impl AllocationParams { ))); } - if self.proposed_receiver.is_some() { - return Err(StdError::generic_err(format!( - "Proposed receiver must be unset. Account: {account}" - ))); - } - Ok(()) } +} + +#[cw_serde] +#[derive(Default)] +pub struct AllocationParams { + /// Parameters controlling the unlocking process + pub unlock_schedule: Schedule, + /// Proposed new receiver who will get the ASTRO allocation + pub proposed_receiver: Option, +} - pub fn update_schedule( - &mut self, - new_schedule: Schedule, - account: &String, - ) -> Result<(), StdError> { +impl AllocationParams { + pub fn update_schedule(&mut self, new_schedule: Schedule, account: &str) -> StdResult<()> { if new_schedule.cliff < self.unlock_schedule.cliff { return Err(StdError::generic_err(format!( "The new cliff value should be greater than or equal to the old one: {} >= {}. Account error: {}", @@ -109,143 +215,10 @@ impl AllocationParams { #[cw_serde] #[derive(Default)] pub struct AllocationStatus { + /// Total amount of ASTRO tokens allocated to a specific account + pub amount: Uint128, /// Amount of ASTRO already withdrawn pub astro_withdrawn: Uint128, /// Already unlocked amount after decreasing pub unlocked_amount_checkpoint: Uint128, } - -impl AllocationStatus { - pub const fn new() -> Self { - Self { - astro_withdrawn: Uint128::zero(), - unlocked_amount_checkpoint: Uint128::zero(), - } - } -} - -pub mod msg { - use cosmwasm_schema::{cw_serde, QueryResponses}; - use cosmwasm_std::Uint128; - - use crate::builder_unlock::Schedule; - - use super::{AllocationParams, AllocationStatus, Config}; - - /// This structure holds the initial parameters used to instantiate the contract. - #[cw_serde] - pub struct InstantiateMsg { - /// Account that can create new allocations - pub owner: String, - /// ASTRO token denom - pub astro_denom: String, - /// Max ASTRO tokens to allocate - pub max_allocations_amount: Uint128, - } - - /// This enum describes all the execute functions available in the contract. - #[cw_serde] - pub enum ExecuteMsg { - /// CreateAllocations creates new ASTRO allocations - CreateAllocations { - allocations: Vec<(String, AllocationParams)>, - }, - /// Withdraw claims withdrawable ASTRO - Withdraw {}, - /// ProposeNewReceiver allows a user to change the receiver address for their ASTRO allocation - ProposeNewReceiver { new_receiver: String }, - /// DropNewReceiver allows a user to remove the previously proposed new receiver for their ASTRO allocation - DropNewReceiver {}, - /// ClaimReceiver allows newly proposed receivers to claim ASTRO allocations ownership - ClaimReceiver { prev_receiver: String }, - /// Increase the ASTRO allocation of a receiver - IncreaseAllocation { receiver: String, amount: Uint128 }, - /// Decrease the ASTRO allocation of a receiver - DecreaseAllocation { receiver: String, amount: Uint128 }, - /// Transfer unallocated tokens (only accessible to the owner) - TransferUnallocated { - amount: Uint128, - recipient: Option, - }, - /// Propose a new owner for the contract - ProposeNewOwner { new_owner: String, expires_in: u64 }, - /// Remove the ownership transfer proposal - DropOwnershipProposal {}, - /// Claim contract ownership - ClaimOwnership {}, - /// Update parameters in the contract configuration - UpdateConfig { new_max_allocations_amount: Uint128 }, - /// Update a schedule of allocation for specified accounts - UpdateUnlockSchedules { - new_unlock_schedules: Vec<(String, Schedule)>, - }, - } - - /// Thie enum describes all the queries available in the contract. - #[cw_serde] - #[derive(QueryResponses)] - pub enum QueryMsg { - /// Config returns the configuration for this contract - #[returns(Config)] - Config {}, - /// State returns the state of this contract - #[returns(StateResponse)] - State {}, - /// Allocation returns the parameters and current status of an allocation - #[returns(AllocationResponse)] - Allocation { - /// Account whose allocation status we query - account: String, - }, - /// Allocations returns a vector that contains builder unlock allocations by specified - /// parameters - #[returns(Vec<(String, AllocationParams)>)] - Allocations { - start_after: Option, - limit: Option, - }, - #[returns(Uint128)] - /// UnlockedTokens returns the unlocked tokens from an allocation - UnlockedTokens { - /// Account whose amount of unlocked ASTRO we query for - account: String, - }, - /// SimulateWithdraw simulates how many ASTRO will be released if a withdrawal is attempted - #[returns(SimulateWithdrawResponse)] - SimulateWithdraw { - /// Account for which we simulate a withdrawal - account: String, - /// Timestamp used to simulate how much ASTRO the account can withdraw - timestamp: Option, - }, - } - - pub type ConfigResponse = Config; - - /// This structure stores the parameters used to return the response when querying for an allocation data. - #[cw_serde] - pub struct AllocationResponse { - /// The allocation parameters - pub params: AllocationParams, - /// The allocation status - pub status: AllocationStatus, - } - - /// This structure stores the parameters used to return a response when simulating a withdrawal. - #[cw_serde] - pub struct SimulateWithdrawResponse { - /// Amount of ASTRO to receive - pub astro_to_withdraw: Uint128, - } - - /// This structure stores parameters used to return the response when querying for the contract state. - #[cw_serde] - pub struct StateResponse { - /// ASTRO tokens deposited into the contract and that are meant to unlock - pub total_astro_deposited: Uint128, - /// Currently available ASTRO tokens that weren't yet withdrawn from the contract - pub remaining_astro_tokens: Uint128, - /// Currently available ASTRO tokens to withdraw or increase allocations by the owner - pub unallocated_astro_tokens: Uint128, - } -} From 9c510f3b3e4a38aacc80041d81b7b309c985aa9c Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 29 Feb 2024 17:04:44 +0500 Subject: [PATCH 36/47] prohibit xastro denom update --- contracts/assembly/src/contract.rs | 4 ---- contracts/assembly/tests/assembly_integration.rs | 3 --- packages/astroport-governance/src/assembly.rs | 2 -- 3 files changed, 9 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 6af4d61a..60985097 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -441,10 +441,6 @@ pub fn update_config( return Err(ContractError::Unauthorized {}); } - if let Some(xastro_denom) = updated_config.xastro_denom { - config.xastro_denom = xastro_denom; - } - if let Some(ibc_controller) = updated_config.ibc_controller { config.ibc_controller = Some(deps.api.addr_validate(&ibc_controller)?) } diff --git a/contracts/assembly/tests/assembly_integration.rs b/contracts/assembly/tests/assembly_integration.rs index 4223d845..68eb95cd 100644 --- a/contracts/assembly/tests/assembly_integration.rs +++ b/contracts/assembly/tests/assembly_integration.rs @@ -605,7 +605,6 @@ fn test_update_config() { owner.clone(), assembly.clone(), &ExecuteMsg::UpdateConfig(Box::new(UpdateConfig { - xastro_denom: None, ibc_controller: None, builder_unlock_addr: None, proposal_voting_period: None, @@ -626,7 +625,6 @@ fn test_update_config() { ); let updated_config = UpdateConfig { - xastro_denom: Some("test".to_string()), ibc_controller: Some("ibc_controller".to_string()), builder_unlock_addr: Some("builder_unlock".to_string()), proposal_voting_period: Some(*VOTING_PERIOD_INTERVAL.end()), @@ -655,7 +653,6 @@ fn test_update_config() { .query_wasm_smart(assembly, &QueryMsg::Config {}) .unwrap(); - assert_eq!(config.xastro_denom, "test"); assert_eq!( config.ibc_controller, Some(Addr::unchecked("ibc_controller")) diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index cc205aca..81751c15 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -239,8 +239,6 @@ impl Config { /// This structure stores the params used when updating the main Assembly contract params. #[cw_serde] pub struct UpdateConfig { - /// xASTRO token denom - pub xastro_denom: Option, /// Astroport IBC controller contract pub ibc_controller: Option, /// Builder unlock contract address From 2bb6f614517ac5e53a9a25f0b3f2b496ada3ce40 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 1 Mar 2024 15:21:16 +0500 Subject: [PATCH 37/47] assembly: emit attrs on update config --- contracts/assembly/src/contract.rs | 51 +++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 60985097..3aca37ca 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -441,47 +441,77 @@ pub fn update_config( return Err(ContractError::Unauthorized {}); } + let mut attrs = vec![attr("action", "update_config")]; + if let Some(ibc_controller) = updated_config.ibc_controller { - config.ibc_controller = Some(deps.api.addr_validate(&ibc_controller)?) + config.ibc_controller = Some(deps.api.addr_validate(&ibc_controller)?); + attrs.push(attr("new_ibc_controller", ibc_controller)); } if let Some(builder_unlock_addr) = updated_config.builder_unlock_addr { config.builder_unlock_addr = deps.api.addr_validate(&builder_unlock_addr)?; + attrs.push(attr("new_builder_unlock_addr", builder_unlock_addr)); } if let Some(proposal_voting_period) = updated_config.proposal_voting_period { config.proposal_voting_period = proposal_voting_period; + attrs.push(attr( + "new_proposal_voting_period", + proposal_voting_period.to_string(), + )); } if let Some(proposal_effective_delay) = updated_config.proposal_effective_delay { config.proposal_effective_delay = proposal_effective_delay; + attrs.push(attr( + "new_proposal_effective_delay", + proposal_effective_delay.to_string(), + )); } if let Some(proposal_expiration_period) = updated_config.proposal_expiration_period { config.proposal_expiration_period = proposal_expiration_period; + attrs.push(attr( + "new_proposal_expiration_period", + proposal_expiration_period.to_string(), + )); } if let Some(proposal_required_deposit) = updated_config.proposal_required_deposit { config.proposal_required_deposit = Uint128::from(proposal_required_deposit); + attrs.push(attr( + "new_proposal_required_deposit", + proposal_required_deposit.to_string(), + )); } if let Some(proposal_required_quorum) = updated_config.proposal_required_quorum { config.proposal_required_quorum = Decimal::from_str(&proposal_required_quorum)?; + attrs.push(attr( + "new_proposal_required_quorum", + proposal_required_quorum, + )); } if let Some(proposal_required_threshold) = updated_config.proposal_required_threshold { config.proposal_required_threshold = Decimal::from_str(&proposal_required_threshold)?; + attrs.push(attr( + "new_proposal_required_threshold", + proposal_required_threshold, + )); } if let Some(whitelist_add) = updated_config.whitelist_add { validate_links(&whitelist_add)?; - config.whitelisted_links.append( - &mut whitelist_add - .into_iter() - .filter(|link| !config.whitelisted_links.contains(link)) - .collect(), - ); + let mut new_links = whitelist_add + .into_iter() + .filter(|link| !config.whitelisted_links.contains(link)) + .collect::>(); + + attrs.push(attr("new_whitelisted_links", new_links.join(", "))); + + config.whitelisted_links.append(&mut new_links); } if let Some(whitelist_remove) = updated_config.whitelist_remove { @@ -489,6 +519,11 @@ pub fn update_config( .whitelisted_links .retain(|link| !whitelist_remove.contains(link)); + attrs.push(attr( + "removed_whitelisted_links", + whitelist_remove.join(", "), + )); + if config.whitelisted_links.is_empty() { return Err(ContractError::WhitelistEmpty {}); } @@ -499,7 +534,7 @@ pub fn update_config( CONFIG.save(deps.storage, &config)?; - Ok(Response::new().add_attribute("action", "update_config")) + Ok(Response::new().add_attributes(attrs)) } /// Updates proposal status InProgress -> Executed or Failed. Intended to be called in the end of From aa801cbf7abc7b0085d5b281c8c5b953297f3e8d Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:10:03 +0500 Subject: [PATCH 38/47] assembly: remove unused errors --- contracts/assembly/src/error.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index c5c08547..f2086182 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -44,27 +44,15 @@ pub enum ContractError { #[error("Proposal not passed!")] ProposalNotPassed {}, - #[error("Proposal not completed!")] - ProposalNotCompleted {}, - #[error("Proposal delay not ended!")] ProposalDelayNotEnded {}, - #[error("Proposal not in delay period!")] - ProposalNotInDelayPeriod {}, - - #[error("Contract can't be migrated!")] - MigrationError {}, - #[error("Whitelist cannot be empty!")] WhitelistEmpty {}, #[error("Messages check passed. Nothing was committed to the blockchain")] MessagesCheckPassed {}, - #[error("IBC controller does not have channel {0}")] - InvalidChannel(String), - #[error("IBC controller is not set")] MissingIBCController {}, @@ -80,18 +68,6 @@ pub enum ContractError { #[error("Sender is not an IBC controller installed in the assembly")] InvalidIBCController {}, - #[error("Sender is not the Generator controller installed in the assembly")] - InvalidGeneratorController {}, - - #[error("Sender is not the Hub installed in the assembly")] - InvalidHub {}, - - #[error("The proposal has no messages to execute")] - InvalidProposalMessages {}, - - #[error("Voting power exceeds maximum Outpost power")] - InvalidVotingPower {}, - #[error("{0}")] PaymentError(#[from] PaymentError), } From b388b378f66bf3a1f2081624c48832de4e6f583b Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:20:52 +0500 Subject: [PATCH 39/47] assembly: fix delay interval validation --- packages/astroport-governance/src/assembly.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index 81751c15..84f8e961 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -14,7 +14,7 @@ pub const MINIMUM_PROPOSAL_REQUIRED_QUORUM_PERCENTAGE: &str = "0.01"; /// Voting period must be between 1 and 7 days (Neutron: 2.6s per block) pub const VOTING_PERIOD_INTERVAL: RangeInclusive = 33230..=7 * 33230; /// From 0.5 to 2 days in blocks -pub const DELAY_INTERVAL: RangeInclusive = 16615..=33230; +pub const DELAY_INTERVAL: RangeInclusive = 16615..=66460; /// From 1 to 14 days in blocks pub const EXPIRATION_PERIOD_INTERVAL: RangeInclusive = 33230..=14 * 33230; // from 10k to 60k $xASTRO From 449887c9f6b5d271af0e4b911eb5326903c0415e Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:34:22 +0500 Subject: [PATCH 40/47] set expired status --- contracts/assembly/src/contract.rs | 54 +++++++++---------- contracts/assembly/src/error.rs | 3 -- .../assembly/tests/assembly_integration.rs | 10 ++-- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 3aca37ca..2c1fe3e4 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -2,6 +2,8 @@ use std::str::FromStr; use astroport::asset::addr_opt_validate; use astroport::staking; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; use cosmwasm_std::{ attr, coins, wasm_execute, Api, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, Response, StdError, SubMsg, Uint128, Uint64, WasmMsg, @@ -19,8 +21,6 @@ use astroport_governance::utils::check_contract_supports_channel; use crate::error::ContractError; use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_VOTERS}; use crate::utils::{calc_total_voting_power_at, calc_voting_power}; -#[cfg(not(feature = "library"))] -use cosmwasm_std::entry_point; // Contract name and version used for migration. pub const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); @@ -351,39 +351,39 @@ pub fn execute_proposal( return Err(ContractError::ProposalDelayNotEnded {}); } - if env.block.height > proposal.expiration_block { - return Err(ContractError::ExecuteProposalExpired {}); - } - let mut response = Response::new().add_attributes([ attr("action", "execute_proposal"), attr("proposal_id", proposal_id.to_string()), ]); - if let Some(channel) = &proposal.ibc_channel { - if !proposal.messages.is_empty() { - let config = CONFIG.load(deps.storage)?; - - proposal.status = ProposalStatus::InProgress; - response.messages.push(SubMsg::new(wasm_execute( - config - .ibc_controller - .ok_or(ContractError::MissingIBCController {})?, - &ControllerExecuteMsg::IbcExecuteProposal { - channel_id: channel.to_string(), - proposal_id, - messages: proposal.messages.clone(), - }, - vec![], - )?)) + if env.block.height > proposal.expiration_block { + proposal.status = ProposalStatus::Expired; + } else { + if let Some(channel) = &proposal.ibc_channel { + if !proposal.messages.is_empty() { + let config = CONFIG.load(deps.storage)?; + + proposal.status = ProposalStatus::InProgress; + response.messages.push(SubMsg::new(wasm_execute( + config + .ibc_controller + .ok_or(ContractError::MissingIBCController {})?, + &ControllerExecuteMsg::IbcExecuteProposal { + channel_id: channel.to_string(), + proposal_id, + messages: proposal.messages.clone(), + }, + vec![], + )?)) + } else { + proposal.status = ProposalStatus::Executed; + } } else { proposal.status = ProposalStatus::Executed; + response + .messages + .extend(proposal.messages.iter().cloned().map(SubMsg::new)) } - } else { - proposal.status = ProposalStatus::Executed; - response - .messages - .extend(proposal.messages.iter().cloned().map(SubMsg::new)) } PROPOSALS.save(deps.storage, proposal_id, &proposal)?; diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index f2086182..d794ac6e 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -35,9 +35,6 @@ pub enum ContractError { #[error("Voting period not ended yet!")] VotingPeriodNotEnded {}, - #[error("Proposal expired!")] - ExecuteProposalExpired {}, - #[error("Insufficient token deposit!")] InsufficientDeposit {}, diff --git a/contracts/assembly/tests/assembly_integration.rs b/contracts/assembly/tests/assembly_integration.rs index 68eb95cd..d1282282 100644 --- a/contracts/assembly/tests/assembly_integration.rs +++ b/contracts/assembly/tests/assembly_integration.rs @@ -455,12 +455,10 @@ fn test_expired_proposal() { PROPOSAL_REQUIRED_DEPOSIT ); - // Try to execute proposal. It should be rejected. - let err = helper.execute_proposal(1).unwrap_err(); - assert_eq!( - err.downcast::().unwrap(), - ContractError::ExecuteProposalExpired {} - ); + // Check expired proposal + helper.execute_proposal(1).unwrap(); + let proposal = helper.proposal(1); + assert_eq!(proposal.status, ProposalStatus::Expired); // Ensure proposal message was not executed assert_eq!( From a98996d139381277db5fb9d4d28d388ea04ce186 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Fri, 1 Mar 2024 17:37:44 +0500 Subject: [PATCH 41/47] clippy fix --- contracts/assembly/src/contract.rs | 44 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 2c1fe3e4..0432cb53 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -358,32 +358,30 @@ pub fn execute_proposal( if env.block.height > proposal.expiration_block { proposal.status = ProposalStatus::Expired; - } else { - if let Some(channel) = &proposal.ibc_channel { - if !proposal.messages.is_empty() { - let config = CONFIG.load(deps.storage)?; - - proposal.status = ProposalStatus::InProgress; - response.messages.push(SubMsg::new(wasm_execute( - config - .ibc_controller - .ok_or(ContractError::MissingIBCController {})?, - &ControllerExecuteMsg::IbcExecuteProposal { - channel_id: channel.to_string(), - proposal_id, - messages: proposal.messages.clone(), - }, - vec![], - )?)) - } else { - proposal.status = ProposalStatus::Executed; - } + } else if let Some(channel) = &proposal.ibc_channel { + if !proposal.messages.is_empty() { + let config = CONFIG.load(deps.storage)?; + + proposal.status = ProposalStatus::InProgress; + response.messages.push(SubMsg::new(wasm_execute( + config + .ibc_controller + .ok_or(ContractError::MissingIBCController {})?, + &ControllerExecuteMsg::IbcExecuteProposal { + channel_id: channel.to_string(), + proposal_id, + messages: proposal.messages.clone(), + }, + vec![], + )?)) } else { proposal.status = ProposalStatus::Executed; - response - .messages - .extend(proposal.messages.iter().cloned().map(SubMsg::new)) } + } else { + proposal.status = ProposalStatus::Executed; + response + .messages + .extend(proposal.messages.iter().cloned().map(SubMsg::new)) } PROPOSALS.save(deps.storage, proposal_id, &proposal)?; From d9934c34a48245de2deb56090300ee09cb128d0a Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:46:08 +0400 Subject: [PATCH 42/47] emit proposal status attribute --- contracts/assembly/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 0432cb53..c16e2229 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -386,7 +386,7 @@ pub fn execute_proposal( PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - Ok(response) + Ok(response.add_attribute("proposal_status", proposal.status.to_string())) } /// Checks that proposal messages are correct. From 99600ba36669dab00c5b7be23b7c08d58e72e2d3 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 14 Mar 2024 12:33:04 +0400 Subject: [PATCH 43/47] feat(assembly): allow txs from multisig; obtain IBC port --- contracts/assembly/src/contract.rs | 23 ++++++- contracts/assembly/src/ibc.rs | 62 +++++++++++++++++++ contracts/assembly/src/lib.rs | 3 + .../assembly/tests/assembly_integration.rs | 60 +++++++++++++++++- contracts/assembly/tests/common/helper.rs | 25 +++++++- packages/astroport-governance/src/assembly.rs | 1 + 6 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 contracts/assembly/src/ibc.rs diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index c16e2229..4cecd2cf 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -6,7 +6,7 @@ use astroport::staking; use cosmwasm_std::entry_point; use cosmwasm_std::{ attr, coins, wasm_execute, Api, BankMsg, CosmosMsg, Decimal, DepsMut, Env, MessageInfo, - Response, StdError, SubMsg, Uint128, Uint64, WasmMsg, + QuerierWrapper, Response, StdError, SubMsg, Uint128, Uint64, WasmMsg, }; use cw2::set_contract_version; use cw_utils::must_pay; @@ -132,6 +132,9 @@ pub fn execute( proposal_id, status, } => update_ibc_proposal_status(deps, info, proposal_id, status), + ExecuteMsg::ExecuteFromMultisig(proposal_messages) => { + exec_from_multisig(deps.querier, info, env, proposal_messages) + } } } @@ -571,3 +574,21 @@ fn update_ibc_proposal_status( Err(ContractError::InvalidIBCController {}) } } + +pub fn exec_from_multisig( + querier: QuerierWrapper, + info: MessageInfo, + env: Env, + messages: Vec, +) -> Result { + match querier + .query_wasm_contract_info(env.contract.address)? + .admin + { + None => Err(ContractError::Unauthorized {}), + Some(admin) if admin != info.sender => Err(ContractError::Unauthorized {}), + _ => Ok(()), + }?; + + Ok(Response::new().add_messages(messages)) +} diff --git a/contracts/assembly/src/ibc.rs b/contracts/assembly/src/ibc.rs new file mode 100644 index 00000000..1b36688a --- /dev/null +++ b/contracts/assembly/src/ibc.rs @@ -0,0 +1,62 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + DepsMut, Env, Ibc3ChannelOpenResponse, IbcBasicResponse, IbcChannelCloseMsg, + IbcChannelConnectMsg, IbcChannelOpenMsg, IbcPacketAckMsg, IbcPacketReceiveMsg, + IbcPacketTimeoutMsg, IbcReceiveResponse, StdResult, +}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_open( + _deps: DepsMut, + _env: Env, + _msg: IbcChannelOpenMsg, +) -> StdResult> { + unimplemented!() +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_connect( + _deps: DepsMut, + _env: Env, + _msg: IbcChannelConnectMsg, +) -> StdResult { + unimplemented!() +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_channel_close( + _deps: DepsMut, + _env: Env, + _channel: IbcChannelCloseMsg, +) -> StdResult { + // Allow to close old Satellite channel + Ok(IbcBasicResponse::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_receive( + _deps: DepsMut, + _env: Env, + _msg: IbcPacketReceiveMsg, +) -> StdResult { + unimplemented!() +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_ack( + _deps: DepsMut, + _env: Env, + _msg: IbcPacketAckMsg, +) -> StdResult { + unimplemented!() +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn ibc_packet_timeout( + _deps: DepsMut, + _env: Env, + _msg: IbcPacketTimeoutMsg, +) -> StdResult { + unimplemented!() +} diff --git a/contracts/assembly/src/lib.rs b/contracts/assembly/src/lib.rs index 00d9dc3a..6163c541 100644 --- a/contracts/assembly/src/lib.rs +++ b/contracts/assembly/src/lib.rs @@ -2,6 +2,9 @@ pub mod contract; pub mod error; pub mod state; +/// Exclusively to bypass wasmd migration limitation. Assembly doesn't have IBC features. +/// https://github.com/CosmWasm/wasmd/blob/7165e41cbf14d60a9fef4fb1e04c2c2e5e4e0cf4/x/wasm/keeper/keeper.go#L446 +pub mod ibc; pub mod queries; pub mod utils; diff --git a/contracts/assembly/tests/assembly_integration.rs b/contracts/assembly/tests/assembly_integration.rs index d1282282..33c2adea 100644 --- a/contracts/assembly/tests/assembly_integration.rs +++ b/contracts/assembly/tests/assembly_integration.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use std::str::FromStr; -use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Decimal, Uint128, WasmMsg}; +use cosmwasm_std::{ + coin, coins, wasm_execute, Addr, BankMsg, CosmosMsg, Decimal, Empty, Uint128, WasmMsg, +}; use cw_multi_test::Executor; use astro_assembly::error::ContractError; @@ -12,8 +14,8 @@ use astroport_governance::assembly::{ }; use crate::common::helper::{ - default_init_msg, Helper, PROPOSAL_DELAY, PROPOSAL_EXPIRATION, PROPOSAL_REQUIRED_DEPOSIT, - PROPOSAL_VOTING_PERIOD, + default_init_msg, noop_contract, Helper, PROPOSAL_DELAY, PROPOSAL_EXPIRATION, + PROPOSAL_REQUIRED_DEPOSIT, PROPOSAL_VOTING_PERIOD, }; mod common; @@ -886,3 +888,55 @@ fn test_manipulate_governance_proposal() { ContractError::NoVotingPower {} ); } + +#[test] +fn test_execute_multisig() { + let owner = Addr::unchecked("owner"); + let mut helper = Helper::new(&owner).unwrap(); + let assembly = helper.assembly.clone(); + + helper + .app + .execute( + assembly.clone(), + WasmMsg::UpdateAdmin { + contract_addr: assembly.to_string(), + admin: owner.to_string(), + } + .into(), + ) + .unwrap(); + + let noop_code = helper.app.store_code(noop_contract()); + let noop_addr = helper + .app + .instantiate_contract(noop_code, owner.clone(), &Empty {}, &[], "none", None) + .unwrap(); + + let messages: Vec<_> = (0..5) + .into_iter() + .map(|_| wasm_execute(&noop_addr, &Empty {}, vec![]).unwrap().into()) + .collect(); + + let random = Addr::unchecked("random"); + let err = helper + .app + .execute_contract( + random.clone(), + assembly.clone(), + &ExecuteMsg::ExecuteFromMultisig(messages.clone()), + &[], + ) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + helper + .app + .execute_contract( + owner.clone(), + assembly.clone(), + &ExecuteMsg::ExecuteFromMultisig(messages), + &[], + ) + .unwrap(); +} diff --git a/contracts/assembly/tests/common/helper.rs b/contracts/assembly/tests/common/helper.rs index 713faaec..89b1b873 100644 --- a/contracts/assembly/tests/common/helper.rs +++ b/contracts/assembly/tests/common/helper.rs @@ -4,8 +4,8 @@ use anyhow::Result as AnyResult; use astroport::staking; use cosmwasm_std::testing::MockApi; use cosmwasm_std::{ - coin, coins, Addr, BankMsg, Coin, CosmosMsg, Decimal, DepsMut, Empty, Env, GovMsg, IbcMsg, - IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, WasmMsg, + coin, coins, Addr, BankMsg, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env, + GovMsg, IbcMsg, IbcQuery, MemoryStorage, MessageInfo, Response, StdResult, Uint128, WasmMsg, }; use cw_multi_test::{ App, AppResponse, BankKeeper, BasicAppBuilder, Contract, ContractWrapper, DistributionKeeper, @@ -62,6 +62,27 @@ fn builder_contract() -> Box> { )) } +pub fn noop_contract() -> Box> { + fn noop_execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: Empty, + ) -> StdResult { + Ok(Response::new()) + } + + fn noop_query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult { + Ok(Default::default()) + } + + Box::new(ContractWrapper::new_with_empty( + noop_execute, + noop_execute, + noop_query, + )) +} + pub const PROPOSAL_REQUIRED_DEPOSIT: Uint128 = Uint128::new(*DEPOSIT_INTERVAL.start()); pub const PROPOSAL_VOTING_PERIOD: u64 = *VOTING_PERIOD_INTERVAL.start(); pub const PROPOSAL_DELAY: u64 = *DELAY_INTERVAL.start(); diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index 84f8e961..0d475eb4 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -109,6 +109,7 @@ pub enum ExecuteMsg { proposal_id: u64, status: ProposalStatus, }, + ExecuteFromMultisig(Vec), } /// Thie enum describes all the queries available in the contract. From 75adf9ca8bb72531d541baeaa392591aa45f0ef5 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Thu, 21 Mar 2024 16:55:56 +0100 Subject: [PATCH 44/47] disable exec from multisig if assembly is admin of itself --- contracts/assembly/src/contract.rs | 7 +++++-- .../assembly/tests/assembly_integration.rs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/contracts/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 4cecd2cf..f93ed619 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -582,11 +582,14 @@ pub fn exec_from_multisig( messages: Vec, ) -> Result { match querier - .query_wasm_contract_info(env.contract.address)? + .query_wasm_contract_info(&env.contract.address)? .admin { None => Err(ContractError::Unauthorized {}), - Some(admin) if admin != info.sender => Err(ContractError::Unauthorized {}), + // Don't allow to execute this endpoint if the contract is admin of itself + Some(admin) if admin != info.sender || admin == env.contract.address => { + Err(ContractError::Unauthorized {}) + } _ => Ok(()), }?; diff --git a/contracts/assembly/tests/assembly_integration.rs b/contracts/assembly/tests/assembly_integration.rs index 33c2adea..324638ca 100644 --- a/contracts/assembly/tests/assembly_integration.rs +++ b/contracts/assembly/tests/assembly_integration.rs @@ -591,6 +591,27 @@ fn test_check_messages() { err.root_cause().to_string(), "Generic error: Can't check messages with a MsgGrant message" ); + + // Check execute from multisig message + let err = helper + .app + .execute_contract( + Addr::unchecked("permissionless"), + assembly.clone(), + &ExecuteMsg::CheckMessages(vec![wasm_execute( + &assembly, + &ExecuteMsg::ExecuteFromMultisig(vec![]), + vec![], + ) + .unwrap() + .into()]), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); } #[test] From e1c4475708c5d92acece729ae939d8caac4295d6 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 2 Apr 2024 10:49:44 +0200 Subject: [PATCH 45/47] feat: astroport-governance v3 * bump astroport to v4 * bump astroport-governance to v3 * remove unused scripts; bust Rust in CI --- .github/workflows/check_artifacts.yml | 2 +- .github/workflows/code_coverage.yml | 2 +- .github/workflows/tests_and_checks.yml | 2 +- Cargo.lock | 68 +- Cargo.toml | 9 + contracts/assembly/Cargo.toml | 24 +- contracts/builder_unlock/Cargo.toml | 18 +- packages/astroport-governance/Cargo.toml | 12 +- scripts/README.md | 29 - scripts/build_app.sh | 7 - scripts/build_release.sh | 2 +- scripts/chain_configs/localterra.json | 108 - scripts/chain_configs/phoenix-1.json | 420 --- scripts/chain_configs/pisco-1.json | 120 - scripts/coverage.sh | 7 + scripts/deploy.ts | 274 -- scripts/helpers.ts | 289 -- scripts/package-lock.json | 3724 ---------------------- scripts/package.json | 22 - scripts/tsconfig.json | 69 - scripts/types.d/chain_configs.ts | 3 - scripts/types.d/deploy_interfaces.ts | 121 - 22 files changed, 69 insertions(+), 5263 deletions(-) delete mode 100644 scripts/README.md delete mode 100755 scripts/build_app.sh delete mode 100644 scripts/chain_configs/localterra.json delete mode 100644 scripts/chain_configs/phoenix-1.json delete mode 100644 scripts/chain_configs/pisco-1.json create mode 100755 scripts/coverage.sh delete mode 100644 scripts/deploy.ts delete mode 100644 scripts/helpers.ts delete mode 100644 scripts/package-lock.json delete mode 100644 scripts/package.json delete mode 100644 scripts/tsconfig.json delete mode 100644 scripts/types.d/chain_configs.ts delete mode 100644 scripts/types.d/deploy_interfaces.ts diff --git a/.github/workflows/check_artifacts.yml b/.github/workflows/check_artifacts.yml index 3681d5f6..f311595e 100644 --- a/.github/workflows/check_artifacts.yml +++ b/.github/workflows/check_artifacts.yml @@ -47,7 +47,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.68.0 + toolchain: 1.75.0 override: true - name: Fetch cargo deps diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index 9fea23e5..f8649eb0 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -47,7 +47,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.68.0 + toolchain: 1.75.0 override: true - name: Run cargo-tarpaulin diff --git a/.github/workflows/tests_and_checks.yml b/.github/workflows/tests_and_checks.yml index a3249686..3eedb028 100644 --- a/.github/workflows/tests_and_checks.yml +++ b/.github/workflows/tests_and_checks.yml @@ -49,7 +49,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.68.0 + toolchain: 1.75.0 override: true components: rustfmt, clippy diff --git a/Cargo.lock b/Cargo.lock index 274eb5e5..db92af5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,8 +25,8 @@ version = "2.0.0" dependencies = [ "anyhow", "astro-satellite", - "astroport 3.8.0", - "astroport-governance 2.0.0", + "astroport 4.0.0", + "astroport-governance 3.0.0", "astroport-staking", "astroport-tokenfactory-tracker", "builder-unlock", @@ -102,56 +102,28 @@ dependencies = [ [[package]] name = "astroport" -version = "3.8.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" +version = "4.0.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#350dd47ee30af5749251a822fd2f7e08942cf854" dependencies = [ - "astroport-circular-buffer 0.1.0 (git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration)", - "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" -version = "3.10.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#abc23e17bbde99b5d10f0fc0e80517b6e17a4f30" -dependencies = [ - "astroport-circular-buffer 0.1.0 (git+https://github.com/astroport-fi/hidden_astroport_core)", + "astroport-circular-buffer", "cosmwasm-schema", "cosmwasm-std", "cw-asset", - "cw-storage-plus 0.15.1", + "cw-storage-plus 1.2.0", "cw-utils 1.0.3", - "cw20 0.15.1", - "cw3", - "itertools 0.10.5", + "cw20 1.1.2", + "itertools 0.12.0", "uint", ] [[package]] name = "astroport-circular-buffer" -version = "0.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "thiserror", -] - -[[package]] -name = "astroport-circular-buffer" -version = "0.1.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#abc23e17bbde99b5d10f0fc0e80517b6e17a4f30" +version = "0.2.0" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#350dd47ee30af5749251a822fd2f7e08942cf854" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", + "cw-storage-plus 1.2.0", "thiserror", ] @@ -182,9 +154,9 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "2.0.0" +version = "3.0.0" dependencies = [ - "astroport 3.10.0", + "astroport 4.0.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.2.0", @@ -212,9 +184,9 @@ dependencies = [ [[package]] name = "astroport-staking" version = "2.0.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#350dd47ee30af5749251a822fd2f7e08942cf854" dependencies = [ - "astroport 3.8.0", + "astroport 4.0.0", "cosmwasm-std", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -226,9 +198,9 @@ dependencies = [ [[package]] name = "astroport-tokenfactory-tracker" version = "1.0.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core?branch=feat/neutron-migration#84fb6364a0581d2f22ce8538fd308b395494812e" +source = "git+https://github.com/astroport-fi/hidden_astroport_core#350dd47ee30af5749251a822fd2f7e08942cf854" dependencies = [ - "astroport 3.8.0", + "astroport 4.0.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.2.0", @@ -294,12 +266,12 @@ checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" name = "builder-unlock" version = "3.0.0" dependencies = [ - "astroport 3.10.0", - "astroport-governance 2.0.0", + "astroport 4.0.0", + "astroport-governance 3.0.0", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", - "cw-storage-plus 0.15.1", + "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 8c6f283e..020c8f06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,15 @@ members = [ # "contracts/voting_escrow_lite", ] +[workspace.dependencies] +cosmwasm-std = "1.5" +cw-storage-plus = "1.2" +cw2 = "1" +thiserror = "1.0" +itertools = "0.12" +cosmwasm-schema = "1.5" +cw-utils = "1" + [profile.release] opt-level = "z" debug = false diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 4965ad88..46aa32aa 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -3,6 +3,8 @@ name = "astro-assembly" version = "2.0.0" authors = ["Astroport"] edition = "2021" +description = "Astroport DAO Contract" +license = "GPL-3.0-only" repository = "https://github.com/astroport-fi/astroport-governance" homepage = "https://astroport.fi" @@ -15,22 +17,22 @@ testnet = [] library = [] [dependencies] -cw2 = "1" -cosmwasm-std = { version = "1.5", features = ["ibc3", "cosmwasm_1_1"] } -cw-storage-plus = "1.2.0" -astroport-governance = { path = "../../packages/astroport-governance", version = "2" } -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } +cw2.workspace = true +cosmwasm-std = { workspace = true, features = ["ibc3", "cosmwasm_1_1"] } +cw-storage-plus.workspace = true +thiserror.workspace = true +cosmwasm-schema.workspace = true +cw-utils.workspace = true +astroport-governance = { path = "../../packages/astroport-governance", version = "3" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "4" } astro-satellite = { git = "https://github.com/astroport-fi/astroport_ibc", tag = "v1.2.1", features = ["library"] } ibc-controller-package = "1.0.0" -thiserror = "1" -cosmwasm-schema = "1.5" -cw-utils = "1" [dev-dependencies] cw-multi-test = { git = "https://github.com/astroport-fi/cw-multi-test", branch = "feat/bank_with_send_hooks", features = ["cosmwasm_1_1"] } osmosis-std = "0.21" -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } -astroport-tokenfactory-tracker = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } -builder-unlock = { path = "../builder_unlock" } +astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "2" } +astroport-tokenfactory-tracker = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "1" } +builder-unlock = { path = "../builder_unlock", version = "3" } anyhow = "1" test-case = "3.3.1" \ No newline at end of file diff --git a/contracts/builder_unlock/Cargo.toml b/contracts/builder_unlock/Cargo.toml index 0a96a5e3..3e8f1206 100644 --- a/contracts/builder_unlock/Cargo.toml +++ b/contracts/builder_unlock/Cargo.toml @@ -3,6 +3,8 @@ name = "builder-unlock" version = "3.0.0" authors = ["Astroport"] edition = "2021" +description = "Astroport Builders Unlock Contract" +license = "GPL-3.0-only" repository = "https://github.com/astroport-fi/astroport-governance" homepage = "https://astroport.fi" @@ -14,14 +16,14 @@ crate-type = ["cdylib", "rlib"] library = [] [dependencies] -cw2 = "1.1" -cw-utils = "1" -cosmwasm-std = "1.5" -cw-storage-plus = "0.15" -astroport-governance = { path = "../../packages/astroport-governance", version = "2" } -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "3" } -cosmwasm-schema = "1.5" -thiserror = "1" +cw2.workspace = true +cw-utils.workspace = true +cosmwasm-std.workspace = true +cw-storage-plus.workspace = true +cosmwasm-schema.workspace = true +thiserror.workspace = true +astroport-governance = { path = "../../packages/astroport-governance", version = "3" } +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "4" } [dev-dependencies] cw-multi-test = "0.20" \ No newline at end of file diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 564acec0..6926a999 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "astroport-governance" -version = "2.0.0" +version = "3.0.0" authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" @@ -15,8 +15,8 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cw20 = "1.1" -cosmwasm-std = { version = "1.5", features = ["ibc3"] } -cw-storage-plus = "1.2.0" -cosmwasm-schema = "1.5" -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "3" } -thiserror = "1" +cosmwasm-std = { workspace = true, features = ["ibc3"] } +cw-storage-plus.workspace = true +cosmwasm-schema.workspace = true +thiserror.workspace = true +astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "4" } diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 70f6f846..00000000 --- a/scripts/README.md +++ /dev/null @@ -1,29 +0,0 @@ -## Scripts - -### Build local env - -```shell -npm install -npm start -``` - -### Deploy on `testnet` - -Set multisig address in corresponding config or create new one in chain_configs - -Build contract: -```shell -npm run build-artifacts -``` - -Create `.env`: -```shell -WALLET="mnemonic" -LCD_CLIENT_URL=https://pisco-lcd.terra.dev -CHAIN_ID=pisco-1 -``` - -Deploy contracts: -```shell -npm run build-app -``` diff --git a/scripts/build_app.sh b/scripts/build_app.sh deleted file mode 100755 index b5260c26..00000000 --- a/scripts/build_app.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -e - -projectPath=$(cd "$(dirname "${0}")" && cd ../ && pwd) - -cd "$projectPath/scripts" && node --loader ts-node/esm deploy.ts diff --git a/scripts/build_release.sh b/scripts/build_release.sh index d6e1eda1..9724903b 100755 --- a/scripts/build_release.sh +++ b/scripts/build_release.sh @@ -8,4 +8,4 @@ projectPath=$(cd "$(dirname "${0}")" && cd ../ && pwd) docker run --rm -v "$projectPath":/code \ --mount type=volume,source="$(basename "$projectPath")_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/workspace-optimizer:0.12.9 \ No newline at end of file + cosmwasm/workspace-optimizer:0.15.1 \ No newline at end of file diff --git a/scripts/chain_configs/localterra.json b/scripts/chain_configs/localterra.json deleted file mode 100644 index 350926b5..00000000 --- a/scripts/chain_configs/localterra.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "generalInfo": { - "multisig": "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v", - "astro_token": "", - "xastro_token": "", - "factory_addr": "", - "generator_addr": "" - }, - "teamUnlock": { - "admin": null, - "initMsg": { - "owner": null, - "astro_token": null, - "max_allocations_amount": "1000000000" - }, - "label": "Astroport Builder Unlocking Contract", - "change_owner": false, - "propose_new_owner": { - "owner": "", - "expires_in": 604800 - }, - "allocations": [ - [ - "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v", - { - "amount": "1000000000", - "unlock_schedule": { - "start_time": 0, - "cliff": 86400, - "duration": 94608000 - }, - "proposed_receiver": null - } - ] - ] - }, - "assembly": { - "admin": null, - "initMsg": { - "xastro_token_addr": null, - "builder_unlock_addr": null, - "proposal_voting_period": 57600, - "proposal_effective_delay": 6171, - "proposal_expiration_period": 12342, - "proposal_required_deposit": "30000000000", - "proposal_required_quorum": "0.1", - "proposal_required_threshold": "0.50", - "whitelisted_links": [ - "https://forum.astroport.fi/", - "http://forum.astroport.fi/", - "https://astroport.fi/", - "http://astroport.fi/" - ] - }, - "label": "Astroport Assembly Contract" - }, - "votingEscrow": { - "admin": null, - "initMsg": { - "owner": null, - "guardian_addr": null, - "deposit_token_addr": null, - "marketing": { - "project": "Astroport", - "description": "Astroport is a neutral marketplace where anyone, from anywhere in the galaxy, can dock to trade their wares.", - "marketing": null, - "logo": { - "url": "https://astroport.fi/vxastro_logo.png" - } - }, - "logo_urls_whitelist": [ - "https://astroport.fi/" - ] - }, - "label": "Astroport Voting Escrow Contract" - }, - "feeDistributor": { - "admin": null, - "initMsg": { - "owner": null, - "astro_token": null, - "voting_escrow_addr": null, - "is_claim_disabled": false, - "claim_many_limit": 12 - }, - "label": "Astroport Escrow Fee Distributor Contract" - }, - "generatorController": { - "admin": null, - "initMsg": { - "owner": null, - "escrow_addr": null, - "generator_addr": null, - "factory_addr": null, - "pools_limit": 12 - }, - "label": "Astroport Generator Controller Contract" - }, - "votingEscrowDelegation": { - "admin": null, - "initMsg": { - "owner": null, - "voting_escrow_addr": null, - "nft_code_id": null - }, - "label": "Astroport Voting Escrow Delegation Contract" - } -} diff --git a/scripts/chain_configs/phoenix-1.json b/scripts/chain_configs/phoenix-1.json deleted file mode 100644 index dde64897..00000000 --- a/scripts/chain_configs/phoenix-1.json +++ /dev/null @@ -1,420 +0,0 @@ -{ - "generalInfo": { - "multisig": "terra174gu7kg8ekk5gsxdma5jlfcedm653tyg6ayppw", - "astro_token": "terra1nsuqsk6kh58ulczatwev87ttq2z6r3pusulg9r24mfj2fvtzd4uq3exn26", - "xastro_token": "terra1x62mjnme4y0rdnag3r8rfgjuutsqlkkyuh4ndgex0wl3wue25uksau39q8", - "factory_addr": "terra14x9fr055x5hvr48hzy2t4q7kvjvfttsvxusa4xsdcy702mnzsvuqprer8r", - "generator_addr": "terra1ksvlfex49desf4c452j6dewdjs6c48nafemetuwjyj6yexd7x3wqvwa7j9" - }, - "teamUnlock": { - "admin": null, - "initMsg": { - "owner": null, - "astro_token": null, - "max_allocations_amount": "300000000000100" - }, - "label": "Astroport Builder Unlocking Contract", - "change_owner": false, - "propose_new_owner": { - "owner": "terra174gu7kg8ekk5gsxdma5jlfcedm653tyg6ayppw", - "expires_in": 604800 - }, - "allocations": [ - [ - "terra14zees4lwrdds0em258axe7d3lqqj9n4v7saq7e", - { - "amount": "1000000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 86400, - "duration": 94608000 - }, - "proposed_receiver": null - } - ], - [ - "terra18wqkwdcz04upyg0eew3vyhepq9rgfl35aq6jw6", - { - "amount": "1000000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 86400, - "duration": 94608000 - }, - "proposed_receiver": null - } - ], - [ - "terra1nj7umezl9xdqrsd5n0hzcct0kwadkuc726xpdt", - { - "amount": "112_383_407_330000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1nupt9dl6sqhc6eve8dwqsrww2panvju4wxrulp", - { - "amount": "10_456_400_835900", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1kyndl58gmxnz859j9wm5k85lwzqyhc9jqw3fk6", - { - "amount": "10_343_900_835900", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1qyxz6nl2pqq8agnmtjdkp3xa90fdt53nndf4en", - { - "amount": "10_343_900_835900", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1ltryq9mrvk0esdhsrs7dgcehcv0uw5chd2smgn", - { - "amount": "7_428_306_375800", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1svlx2775tg2dlfwkpcvu49q4y4xgefp3ftyk0z", - { - "amount": "116_666_670000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1zaqeperrwghqlsa9yykzsjaets54mtq0u6kl60", - { - "amount": "15_000_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra10yfjdgrj40yckeh5gju86fzyyrw46va48ajxg4", - { - "amount": "547_445_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1frrme65c3rxngyry6j44ahwusha6mkkxefu0tr", - { - "amount": "182_481_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra19xmydpl3zdnw2ef2mnresrsurn3g23e07a8xya", - { - "amount": "200_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1jy3vlu9x2fc2slundxz0kvj7n5y9hjlj6h0hkw", - { - "amount": "100_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1cgdmn0n2x4jj4awnwehstfsy42stcrfqvxcf66", - { - "amount": "1_950_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1pq02fnrm68x6kcv2lhgvyetjelps550w3pq6m2", - { - "amount": "6_000_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1kkwklh7kyr20ktq29uctagkxc7rc27ymp2gf3h", - { - "amount": "1_500_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra14rg786jljcyt08mpfjqfe0tyqtkc5ku07u5cpl", - { - "amount": "1_550_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1gn53cj0v8kvwxqg867e3mu9f3q9yzskmfgnvla", - { - "amount": "2_000_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra17j6tjl2zxd0lugwz3vvsvjcl0z34kh9hqaa63l", - { - "amount": "4_750_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1630fz3hu9np4fwdqt42eduu69hdzv8yfd3mcdp", - { - "amount": "3_500_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1lv845g7szf9m3082qn3eehv9ewkjjr2kdyz0t6", - { - "amount": "600_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1a7rqwyn3zgymqjwhde27d3208muhy8zvgyng6l", - { - "amount": "300_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1jjq6vyq5am5q7tzchc9252y0aczvjtj5ju5hu2", - { - "amount": "670_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1h5cankw4vjf2q5cuepxww4cmefww0ds0qqgem7", - { - "amount": "15_672_765_538700", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1t2fj6czytujh22dwe8zx4sduqkrcpda758mn0q", - { - "amount": "14_821_821_827800", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1l750ue570u3xwm8008ncs5cw22pwrsz0yawztp", - { - "amount": "32_080_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ], - [ - "terra1q4pxqn3ytlt4wqkdpkt76mx6v4v8h2zakye4jn", - { - "amount": "43_300_000_000000", - "unlock_schedule": { - "start_time": 1654646400, - "cliff": 16329600, - "duration": 63072000 - }, - "proposed_receiver": null - } - ] - ] - }, - "assembly": { - "admin": null, - "initMsg": { - "xastro_token_addr": null, - "builder_unlock_addr": null, - "proposal_voting_period": 57600, - "proposal_effective_delay": 6171, - "proposal_expiration_period": 12342, - "proposal_required_deposit": "30000000000", - "proposal_required_quorum": "0.1", - "proposal_required_threshold": "0.50", - "whitelisted_links": [ - "https://forum.astroport.fi/", - "http://forum.astroport.fi/", - "https://astroport.fi/", - "http://astroport.fi/" - ] - }, - "label": "Astroport Assembly Contract" - }, - "votingEscrow": { - "admin": null, - "initMsg": { - "owner": null, - "guardian_addr": "terra1vp629527wwvm9kxqsgn4fx2plgs4j5un0ea5yu", - "deposit_token_addr": null, - "marketing": { - "project": "Astroport", - "description": "Astroport is a neutral marketplace where anyone, from anywhere in the galaxy, can dock to trade their wares.", - "marketing": null, - "logo": { - "url": "https://astroport.fi/vxastro_logo.png" - } - }, - "logo_urls_whitelist": [ - "https://astroport.fi/" - ] - }, - "label": "Astroport Voting Escrow Contract" - }, - "feeDistributor": { - "admin": null, - "initMsg": { - "owner": null, - "astro_token": null, - "voting_escrow_addr": null, - "is_claim_disabled": false, - "claim_many_limit": 12 - }, - "label": "Astroport Escrow Fee Distributor Contract" - }, - "generatorController": { - "admin": null, - "initMsg": { - "owner": null, - "escrow_addr": null, - "generator_addr": null, - "factory_addr": null, - "pools_limit": 12 - }, - "label": "Astroport Generator Controller Contract" - }, - "votingEscrowDelegation": { - "admin": null, - "initMsg": { - "owner": null, - "voting_escrow_addr": null, - "nft_code_id": null - }, - "label": "Astroport Voting Escrow Delegation Contract" - } -} diff --git a/scripts/chain_configs/pisco-1.json b/scripts/chain_configs/pisco-1.json deleted file mode 100644 index 48de9a0b..00000000 --- a/scripts/chain_configs/pisco-1.json +++ /dev/null @@ -1,120 +0,0 @@ -{ - "generalInfo": { - "multisig": "terra174gu7kg8ekk5gsxdma5jlfcedm653tyg6ayppw", - "astro_token": "terra167dsqkh2alurx997wmycw9ydkyu54gyswe3ygmrs4lwume3vmwks8ruqnv", - "xastro_token": "terra1ctzthkc0nzseppqtqlwq9mjwy9gq8ht2534rtcj3yplerm06snmqfc5ucr", - "factory_addr": "terra1z3y69xas85r7egusa0c7m5sam0yk97gsztqmh8f2cc6rr4s4anysudp7k0", - "generator_addr": "terra1gc4d4v82vjgkz0ag28lrmlxx3tf6sq69tmaujjpe7jwmnqakkx0qm28j2l" - }, - "teamUnlock": { - "admin": null, - "initMsg": { - "owner": null, - "astro_token": null, - "max_allocations_amount": "300000000000000" - }, - "label": "Astroport Builder Unlocking Contract", - "change_owner": false, - "propose_new_owner": { - "owner": "terra174gu7kg8ekk5gsxdma5jlfcedm653tyg6ayppw", - "expires_in": 604800 - }, - "allocations": [ - [ - "terra14zees4lwrdds0em258axe7d3lqqj9n4v7saq7e", - { - "amount": "1000000000", - "unlock_schedule": { - "start_time": 0, - "cliff": 86400, - "duration": 94608000 - }, - "proposed_receiver": null - } - ], - [ - "terra18wqkwdcz04upyg0eew3vyhepq9rgfl35aq6jw6", - { - "amount": "1000000000", - "unlock_schedule": { - "start_time": 0, - "cliff": 86400, - "duration": 94608000 - }, - "proposed_receiver": null - } - ] - ] - }, - "assembly": { - "admin": null, - "initMsg": { - "xastro_token_addr": null, - "builder_unlock_addr": null, - "proposal_voting_period": 57600, - "proposal_effective_delay": 6171, - "proposal_expiration_period": 12342, - "proposal_required_deposit": "30000000000", - "proposal_required_quorum": "0.1", - "proposal_required_threshold": "0.50", - "whitelisted_links": [ - "https://forum.astroport.fi/", - "http://forum.astroport.fi/", - "https://astroport.fi/", - "http://astroport.fi/" - ] - }, - "label": "Astroport Assembly Contract" - }, - "votingEscrow": { - "admin": null, - "initMsg": { - "owner": null, - "guardian_addr": "terra1vp629527wwvm9kxqsgn4fx2plgs4j5un0ea5yu", - "deposit_token_addr": null, - "marketing": { - "project": "Astroport", - "description": "Astroport is a neutral marketplace where anyone, from anywhere in the galaxy, can dock to trade their wares.", - "marketing": null, - "logo": { - "url": "https://astroport.fi/vxastro_logo.png" - } - }, - "logo_urls_whitelist": [ - "https://astroport.fi/" - ] - }, - "label": "Astroport Voting Escrow Contract" - }, - "feeDistributor": { - "admin": null, - "initMsg": { - "owner": null, - "astro_token": null, - "voting_escrow_addr": null, - "is_claim_disabled": false, - "claim_many_limit": 12 - }, - "label": "Astroport Escrow Fee Distributor Contract" - }, - "generatorController": { - "admin": null, - "initMsg": { - "owner": null, - "escrow_addr": null, - "generator_addr": null, - "factory_addr": null, - "pools_limit": 12 - }, - "label": "Astroport Generator Controller Contract" - }, - "votingEscrowDelegation": { - "admin": null, - "initMsg": { - "owner": null, - "voting_escrow_addr": null, - "nft_code_id": null - }, - "label": "Astroport Voting Escrow Delegation Contract" - } -} diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100755 index 00000000..9b031d04 --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,7 @@ +#!/usr/src/env bash + +# Usage: ./scripts/coverage.sh +# Example: ./scripts/coverage.sh astroport-pair + +cargo tarpaulin --target-dir target/tarpaulin_build --skip-clean --exclude-files *tests*.rs --exclude-files target*.rs \ + -p "$1" --out Html diff --git a/scripts/deploy.ts b/scripts/deploy.ts deleted file mode 100644 index 6d94f957..00000000 --- a/scripts/deploy.ts +++ /dev/null @@ -1,274 +0,0 @@ -import 'dotenv/config' -import { - newClient, - writeArtifact, - readArtifact, - deployContract, executeContract, uploadContract, delay, -} from './helpers.js' -import { join } from 'path' -import { LCDClient, LocalTerra, Wallet } from '@terra-money/terra.js'; -import { chainConfigs } from "./types.d/chain_configs.js"; - -const ARTIFACTS_PATH = '../artifacts' -const SECONDS_IN_DAY: number = 60 * 60 * 24 // min, hour, da - -async function main() { - const { terra, wallet } = newClient() - console.log(`chainID: ${terra.config.chainID} wallet: ${wallet.key.accAddress}`) - - let property: keyof GeneralInfo; - for (property in chainConfigs.generalInfo) { - if (!chainConfigs.generalInfo[property]) { - throw new Error(`Set required param: ${property}`) - } - } - - await deployTeamUnlock(terra, wallet) - await deployAssembly(terra, wallet) - await deployVotingEscrow(terra, wallet) - - let network = readArtifact(terra.config.chainID) - checkParams(network, ["votingEscrowAddress", "assemblyAddress"]) - - await deployFeeDistributor(terra, wallet) - await deployGeneratorController(terra, wallet) - await deployVotingEscrowDelegation(terra, wallet) -} - -async function deployVotingEscrowDelegation(terra: LCDClient, wallet: any) { - let network = readArtifact(terra.config.chainID) - - if (!network.nftCodeID) { - console.log('Register Astroport NFT Contract...') - network.nftCodeID = await uploadContract(terra, wallet, join(ARTIFACTS_PATH, 'astroport_nft.wasm')!) - writeArtifact(network, terra.config.chainID) - } - - if (!network.votingEscrowDelegationAddress) { - chainConfigs.votingEscrowDelegation.admin ||= chainConfigs.generalInfo.multisig - chainConfigs.votingEscrowDelegation.initMsg.nft_code_id ||= network.nftCodeID - chainConfigs.votingEscrowDelegation.initMsg.owner ||= network.assemblyAddress - chainConfigs.votingEscrowDelegation.initMsg.voting_escrow_addr ||= network.votingEscrowAddress - - console.log('Deploying voting escrow delegation...') - network.votingEscrowDelegationAddress = await deployContract( - terra, - wallet, - chainConfigs.votingEscrowDelegation.admin, - join(ARTIFACTS_PATH, 'voting_escrow_delegation.wasm'), - chainConfigs.votingEscrowDelegation.initMsg, - chainConfigs.votingEscrowDelegation.label - ) - - console.log("Voting Escrow Delegation: ", network.votingEscrowDelegationAddress) - writeArtifact(network, terra.config.chainID) - } -} - -async function deployGeneratorController(terra: LCDClient, wallet: any) { - let network = readArtifact(terra.config.chainID) - - if (!network.generatorControllerAddress) { - chainConfigs.generatorController.initMsg.owner ||= network.assemblyAddress - chainConfigs.generatorController.initMsg.escrow_addr ||= network.votingEscrowAddress - chainConfigs.generatorController.initMsg.generator_addr ||= chainConfigs.generalInfo.generator_addr - chainConfigs.generatorController.initMsg.factory_addr ||= chainConfigs.generalInfo.factory_addr - chainConfigs.generatorController.admin ||= chainConfigs.generalInfo.multisig - - console.log('Deploying generator controller...') - network.generatorControllerAddress = await deployContract( - terra, - wallet, - chainConfigs.generatorController.admin, - join(ARTIFACTS_PATH, 'generator_controller.wasm'), - chainConfigs.generatorController.initMsg, - chainConfigs.generatorController.label - ) - - console.log("Generator controller: ", network.generatorControllerAddress) - writeArtifact(network, terra.config.chainID) - } -} - -async function deployFeeDistributor(terra: LCDClient, wallet: any) { - let network = readArtifact(terra.config.chainID) - - if (!network.feeDistributorAddress) { - chainConfigs.feeDistributor.admin ||= chainConfigs.generalInfo.multisig - chainConfigs.feeDistributor.initMsg.owner ||= network.assemblyAddress - chainConfigs.feeDistributor.initMsg.astro_token ||= chainConfigs.generalInfo.astro_token - chainConfigs.feeDistributor.initMsg.voting_escrow_addr ||= network.votingEscrowAddress - - console.log('Deploying fee distributor...') - network.feeDistributorAddress = await deployContract( - terra, - wallet, - chainConfigs.feeDistributor.admin, - join(ARTIFACTS_PATH, 'astroport_escrow_fee_distributor.wasm'), - chainConfigs.feeDistributor.initMsg, - chainConfigs.feeDistributor.label, - ) - - console.log("Fee distributor: ", network.feeDistributorAddress) - writeArtifact(network, terra.config.chainID) - } -} - -async function deployVotingEscrow(terra: LCDClient, wallet: any) { - let network = readArtifact(terra.config.chainID) - - if (!network.votingEscrowAddress) { - checkParams(network, ["assemblyAddress"]) - chainConfigs.votingEscrow.admin ||= chainConfigs.generalInfo.multisig - chainConfigs.votingEscrow.initMsg.owner ||= network.assemblyAddress - chainConfigs.votingEscrow.initMsg.deposit_token_addr ||= chainConfigs.generalInfo.xastro_token - chainConfigs.votingEscrow.initMsg.marketing.marketing ||= chainConfigs.generalInfo.multisig - - console.log('Deploying votingEscrow...') - network.votingEscrowAddress = await deployContract( - terra, - wallet, - chainConfigs.votingEscrow.admin, - join(ARTIFACTS_PATH, 'voting_escrow.wasm'), - chainConfigs.votingEscrow.initMsg, - chainConfigs.votingEscrow.label - ) - - console.log("votingEscrow", network.votingEscrowAddress) - writeArtifact(network, terra.config.chainID) - } -} - -async function deployTeamUnlock(terra: LCDClient, wallet: any) { - let network = readArtifact(terra.config.chainID) - - if (!network.builderUnlockAddress) { - chainConfigs.teamUnlock.admin ||= chainConfigs.generalInfo.multisig - chainConfigs.teamUnlock.initMsg.owner ||= wallet.key.accAddress - chainConfigs.teamUnlock.initMsg.astro_token ||= chainConfigs.generalInfo.astro_token - - console.log("Builder Unlock Contract deploying...") - network.builderUnlockAddress = await deployContract( - terra, - wallet, - chainConfigs.teamUnlock.admin, - join(ARTIFACTS_PATH, 'builder_unlock.wasm'), - chainConfigs.teamUnlock.initMsg, - chainConfigs.teamUnlock.label - ) - console.log(`Builder unlock contract address: ${network.builderUnlockAddress}`) - - checkAllocationAmount(chainConfigs.teamUnlock.allocations); - await create_allocations(terra, wallet, network, chainConfigs.teamUnlock.allocations); - - // Set new owner for builder unlock - if (chainConfigs.teamUnlock.change_owner) { - console.log('Propose owner for builder unlock. Ownership has to be claimed within %s days', - Number(chainConfigs.teamUnlock.propose_new_owner.expires_in) / SECONDS_IN_DAY) - await executeContract(terra, wallet, network.builderUnlockAddress, { - "propose_new_owner": chainConfigs.teamUnlock.propose_new_owner - }) - } - writeArtifact(network, terra.config.chainID) - } -} - -function checkAllocationAmount(allocations: Allocations[]) { - let sum = 0; - - for (let builder of allocations) { - sum += parseInt(builder[1].amount); - } - - if (sum != parseInt(chainConfigs.teamUnlock.initMsg.max_allocations_amount)) { - throw new Error(`Sum of allocations is ${sum}, but should be ${chainConfigs.teamUnlock.initMsg.max_allocations_amount}`); - } -} - -async function create_allocations(terra: LocalTerra | LCDClient, wallet: Wallet, network: any, allocations: Allocations[]) { - if (allocations.length > 0) { - let from = 0; - let step = 5; - let till = allocations.length > step ? step : allocations.length; - - do { - if (!network[`allocations_created_${from}_${till}`]) { - let astro_to_transfer = 0; - let allocations_to_create = []; - - for (let i = from; i < till; i++) { - astro_to_transfer += Number(allocations[i][1].amount); - allocations_to_create.push(allocations[i]); - } - - console.log(`from ${from} to ${till}: ${astro_to_transfer / 1000000} ASTRO to transfer.`); - - // Create allocations : TX - let tx = await executeContract(terra, wallet, chainConfigs.generalInfo.astro_token, - { - send: { - contract: network.builderUnlockAddress, - amount: String(astro_to_transfer), - msg: Buffer.from( - JSON.stringify({ - create_allocations: { - allocations: allocations_to_create, - }, - }) - ).toString("base64") - }, - } - ); - - console.log( - `Creating ASTRO Unlocking schedules ::: ${from} - ${till}, ASTRO sent : ${astro_to_transfer / 1000000 - }, \n Tx hash --> ${tx.txhash} \n` - ); - - network[`allocations_created_${from}_${till}`] = true; - writeArtifact(network, terra.config.chainID); - await delay(1000); - } - - from = till; - step = allocations.length > (till + step) ? step : allocations.length - till - till += step; - } while (from < allocations.length); - } else { - console.log("Builder Unlock has no allocation points to install") - } -} - -async function deployAssembly(terra: LCDClient, wallet: any) { - let network = readArtifact(terra.config.chainID) - - if (!network.assemblyAddress) { - checkParams(network, ["builderUnlockAddress"]) - chainConfigs.assembly.initMsg.xastro_token_addr ||= chainConfigs.generalInfo.xastro_token - chainConfigs.assembly.initMsg.builder_unlock_addr ||= network.builderUnlockAddress - chainConfigs.assembly.admin ||= chainConfigs.generalInfo.multisig - - console.log('Deploying Assembly Contract...') - network.assemblyAddress = await deployContract( - terra, - wallet, - chainConfigs.assembly.admin, - join(ARTIFACTS_PATH, 'astro_assembly.wasm'), - chainConfigs.assembly.initMsg, - chainConfigs.assembly.label - ) - - console.log("assemblyAddress", network.assemblyAddress) - writeArtifact(network, terra.config.chainID) - } -} - -function checkParams(network: any, required_params: any) { - for (const k in required_params) { - if (!network[required_params[k]]) { - throw "Set required param: " + required_params[k] - } - } -} - -await main() diff --git a/scripts/helpers.ts b/scripts/helpers.ts deleted file mode 100644 index f6c3365e..00000000 --- a/scripts/helpers.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { - isTxError, - LCDClient, - LocalTerra, - MnemonicKey, - Msg, - MsgExecuteContract, - MsgInstantiateContract, - MsgMigrateContract, - MsgStoreCode, - Wallet, - PublicKey, - Tx, -} from "@terra-money/terra.js"; -import { readFileSync, writeFileSync } from "fs"; -import path from "path"; -import { CustomError } from 'ts-custom-error' - -export const ARTIFACTS_PATH = "../artifacts"; - -export function readArtifact(name: string = 'artifact', from: string = ARTIFACTS_PATH,) { - try { - const data = readFileSync(path.join(from, `${name}.json`), 'utf8') - return JSON.parse(data) - } catch (e) { - return {} - } -} - -export interface Client { - wallet: Wallet; - terra: LCDClient | LocalTerra; - MULTI_SIG_TO_USE: String; -} - -// Creates `Client` instance with `terra` and `wallet` to be used for interacting with terra -export function newClient(): Client { - const client = {}; - if (process.env.WALLET) { - client.terra = new LCDClient({ - URL: String(process.env.LCD_CLIENT_URL), - chainID: String(process.env.CHAIN_ID), - }); - - client.wallet = recover(client.terra, process.env.WALLET); - } else { - client.terra = new LocalTerra(); - client.wallet = (client.terra as LocalTerra).wallets.test1; - } - return client; -} - -export function writeArtifact(data: object, name: string = "artifact") { - writeFileSync( - path.join(ARTIFACTS_PATH, `${name}.json`), - JSON.stringify(data, null, 2) - ); -} - -// Tequila lcd is load balanced, so txs can't be sent too fast, otherwise account sequence queries -// may resolve an older state depending on which lcd you end up with. Generally 1000 ms is enough -// for all nodes to sync up. -let TIMEOUT = 1000; - -export function setTimeoutDuration(t: number) { - TIMEOUT = t; -} - -export function getTimeoutDuration() { - return TIMEOUT; -} - -export class TransactionError extends CustomError { - public constructor( - public code: string | number, - public codespace: string | undefined, - public rawLog: string, - ) { - super("transaction failed") - } -} - -export async function sleep(timeout: number) { - await new Promise(resolve => setTimeout(resolve, timeout)) -} - -export async function createTransaction(wallet: Wallet, msg: Msg) { - return await wallet.createAndSignTx({ msgs: [msg]}) -} - -export async function broadcastTransaction(terra: LCDClient, signedTx: Tx) { - const result = await terra.tx.broadcast(signedTx) - await sleep(TIMEOUT) - return result -} - -export async function performTransaction(terra: LCDClient, wallet: Wallet, msg: Msg) { - const signedTx = await createTransaction(wallet, msg) - const result = await broadcastTransaction(terra, signedTx) - if (isTxError(result)) { - throw new TransactionError(result.code, result.codespace, result.raw_log) - } - return result -} - -export async function uploadContract( - terra: LocalTerra | LCDClient, - wallet: Wallet, - filepath: string -) { - const contract = readFileSync(filepath, "base64"); - const uploadMsg = new MsgStoreCode(wallet.key.accAddress, contract); - let result = await performTransaction(terra, wallet, uploadMsg); - return Number(result.logs[0].eventsByType.store_code.code_id[0]); // code_id -} - -export async function instantiateContract(terra: LCDClient, wallet: Wallet, admin_address: string | undefined, codeId: number, msg: object, label?: string) { -const instantiateMsg = new MsgInstantiateContract(wallet.key.accAddress, admin_address, codeId, msg, undefined, label); -let result = await performTransaction(terra, wallet, instantiateMsg) -return result.logs[0].events.filter(el => el.type == 'instantiate').map(x => x.attributes.filter(element => element.key == '_contract_address' ).map(x => x.value))[0][0]; -} - -export async function executeContract( - terra: LocalTerra | LCDClient, - wallet: Wallet, - contractAddress: string, - msg: object, - coins?: any -) { - const executeMsg = new MsgExecuteContract( - wallet.key.accAddress, - contractAddress, - msg, - coins - ); - return await performTransaction(terra, wallet, executeMsg); -} - -// Returns a TX object -export async function executeContractJsonForMultiSig( - terra: LocalTerra | LCDClient, - multisigAddress: string, - sequence_number: number, - pub_key: PublicKey | null, - contract_address: string, - execute_msg: any, - memo?: string -) { - const tx = await terra.tx.create( - [ - { - address: multisigAddress, - sequenceNumber: sequence_number, - publicKey: pub_key, - }, - ], - { - msgs: [ - new MsgExecuteContract(multisigAddress, contract_address, execute_msg), - ], - memo: memo, - } - ); - return tx; -} - -export async function queryContract( - terra: LocalTerra | LCDClient, - contractAddress: string, - query: object -): Promise { - return await terra.wasm.contractQuery(contractAddress, query); -} - -export async function deployContract( - terra: LocalTerra | LCDClient, - wallet: Wallet, - admin_address: string | undefined, - filepath: string, - initMsg: object, - label?: string -) { - const codeId = await uploadContract(terra, wallet, filepath); - return await instantiateContract(terra, wallet, admin_address, codeId, initMsg, label); -} - -export async function migrate( - terra: LocalTerra | LCDClient, - wallet: Wallet, - contractAddress: string, - newCodeId: number -) { - const migrateMsg = new MsgMigrateContract( - wallet.key.accAddress, - contractAddress, - newCodeId, - {} - ); - return await performTransaction(terra, wallet, migrateMsg); -} - -export function recover(terra: LocalTerra | LCDClient, mnemonic: string) { - const mk = new MnemonicKey({ mnemonic: mnemonic }); - return terra.wallet(mk); -} - -export function initialize(terra: LCDClient) { - const mk = new MnemonicKey(); - - console.log(`Account Address: ${mk.accAddress}`); - console.log(`MnemonicKey: ${mk.mnemonic}`); - - return terra.wallet(mk); -} - -export async function transferCW20Tokens( - terra: LCDClient, - wallet: Wallet, - tokenAddress: string, - recipient: string, - amount: number -) { - let transfer_msg = { - transfer: { recipient: recipient, amount: amount.toString() }, - }; - let resp = await executeContract(terra, wallet, tokenAddress, transfer_msg); -} - -export async function getCW20Balance( - terra: LocalTerra | LCDClient, - token_addr: string, - user_address: string -) { - let curBalance = await terra.wasm.contractQuery<{ balance: string }>( - token_addr, - { balance: { address: user_address } } - ); - return curBalance.balance; -} - -export function toEncodedBinary(object: any) { - return Buffer.from(JSON.stringify(object)).toString("base64"); -} - -// Returns the `pool_address` and `lp_token_address` for a terraswap pool that's created -export function extract_terraswap_pool_info(response: any) { - let pool_address = ""; - let lp_token_address = ""; - if (response.height > 0) { - let events_array = JSON.parse(response["raw_log"])[0]["events"]; - let attributes = events_array[1]["attributes"]; - for (let i = 0; i < attributes.length; i++) { - // console.log(attributes[i]); - if (attributes[i]["key"] == "liquidity_token_addr") { - lp_token_address = attributes[i]["value"]; - } - if (attributes[i]["key"] == "pair_contract_addr") { - pool_address = attributes[i]["value"]; - } - } - } - - return { pool_address: pool_address, lp_token_address: lp_token_address }; -} - -// Returns the `pool_address` and `lp_token_address` of the Astroport pool that's created -export function extract_astroport_pool_info(response: any) { - let pool_address = ""; - let lp_token_address = ""; - if (response.height > 0) { - let events_array = JSON.parse(response["raw_log"])[0]["events"]; - let attributes = events_array[1]["attributes"]; - for (let i = 0; i < attributes.length; i++) { - // console.log(attributes[i]); - if (attributes[i]["key"] == "liquidity_token_addr") { - lp_token_address = attributes[i]["value"]; - } - if (attributes[i]["key"] == "pair_contract_addr") { - pool_address = attributes[i]["value"]; - } - } - } - - return { pool_address: pool_address, lp_token_address: lp_token_address }; -} - -export function delay(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} diff --git a/scripts/package-lock.json b/scripts/package-lock.json deleted file mode 100644 index 7d123df8..00000000 --- a/scripts/package-lock.json +++ /dev/null @@ -1,3724 +0,0 @@ -{ - "name": "scripts", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "scripts", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "@terra-money/terra.js": "^3.1.5", - "dotenv": "^8.2.0", - "ts-custom-error": "^3.2.0" - }, - "devDependencies": { - "eslint": "^7.24.0", - "ts-node": "^10.8.0", - "typescript": "^4.3.5" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", - "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", - "dev": true - }, - "node_modules/@improbable-eng/grpc-web": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.14.1.tgz", - "integrity": "sha512-XaIYuunepPxoiGVLLHmlnVminUGzBTnXr8Wv7khzmLWbNw4TCwJKX09GSMJlKhu/TRk6gms0ySFxewaETSBqgw==", - "dependencies": { - "browser-headers": "^0.4.1" - }, - "peerDependencies": { - "google-protobuf": "^3.14.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", - "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", - "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "node_modules/@terra-money/legacy.proto": { - "name": "@terra-money/terra.proto", - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz", - "integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==", - "dependencies": { - "google-protobuf": "^3.17.3", - "long": "^4.0.0", - "protobufjs": "~6.11.2" - } - }, - "node_modules/@terra-money/terra.js": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-3.1.7.tgz", - "integrity": "sha512-z7NwVI1gh0846pgQJaPHya6SRKLd/dHWR5UwWG6T2Pf24I2EjCo8YY5Fay30pCvHTYA2NBFgnWfXEZ/31TfB1Q==", - "dependencies": { - "@terra-money/legacy.proto": "npm:@terra-money/terra.proto@^0.1.7", - "@terra-money/terra.proto": "^2.1.0", - "axios": "^0.27.2", - "bech32": "^2.0.0", - "bip32": "^2.0.6", - "bip39": "^3.0.3", - "bufferutil": "^4.0.3", - "decimal.js": "^10.2.1", - "jscrypto": "^1.0.1", - "readable-stream": "^3.6.0", - "secp256k1": "^4.0.2", - "tmp": "^0.2.1", - "utf-8-validate": "^5.0.5", - "ws": "^7.5.9" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@terra-money/terra.proto": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-2.1.0.tgz", - "integrity": "sha512-rhaMslv3Rkr+QsTQEZs64FKA4QlfO0DfQHaR6yct/EovenMkibDEQ63dEL6yJA6LCaEQGYhyVB9JO9pTUA8ybw==", - "dependencies": { - "@improbable-eng/grpc-web": "^0.14.1", - "google-protobuf": "^3.17.3", - "long": "^4.0.0", - "protobufjs": "~6.11.2" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", - "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", - "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", - "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", - "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true - }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, - "node_modules/@types/node": { - "version": "10.12.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", - "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" - }, - "node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/base-x": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", - "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bip32": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz", - "integrity": "sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA==", - "dependencies": { - "@types/node": "10.12.18", - "bs58check": "^2.1.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "tiny-secp256k1": "^1.1.3", - "typeforce": "^1.11.5", - "wif": "^2.0.6" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/bip39": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", - "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", - "dependencies": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" - } - }, - "node_modules/bip39/node_modules/@types/node": { - "version": "11.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" - }, - "node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "node_modules/browser-headers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" - }, - "node_modules/bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", - "dependencies": { - "base-x": "^3.0.2" - } - }, - "node_modules/bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", - "dependencies": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/bufferutil": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", - "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", - "hasInstallScript": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "engines": { - "node": ">=10" - } - }, - "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "dependencies": { - "ansi-colors": "^4.1.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "dependencies": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "dependencies": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", - "dev": true - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "node_modules/glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", - "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/google-protobuf": { - "version": "3.20.1", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.20.1.tgz", - "integrity": "sha512-XMf1+O32FjYIV3CYu6Tuh5PNbfNEU5Xu22X+Xkdb/DUexFlCzhvv7d5Iirm4AOwn8lv4al1YvIhzGrg2j9Zfzw==" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jscrypto": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/jscrypto/-/jscrypto-1.0.2.tgz", - "integrity": "sha512-r+oNJLGTv1nkNMBBq3c70xYrFDgJOYVgs2OHijz5Ht+0KJ0yObD0oYxC9mN72KLzVfXw+osspg6t27IZvuTUxw==", - "bin": { - "jscrypto": "bin/cli.js" - } - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, - "node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node_modules/node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" - }, - "node_modules/node-gyp-build": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/protobufjs/node_modules/@types/node": { - "version": "17.0.38", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.38.tgz", - "integrity": "sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==" - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/secp256k1": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", - "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", - "hasInstallScript": true, - "dependencies": { - "elliptic": "^6.5.2", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "node_modules/tiny-secp256k1": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz", - "integrity": "sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA==", - "hasInstallScript": true, - "dependencies": { - "bindings": "^1.3.0", - "bn.js": "^4.11.8", - "create-hmac": "^1.1.7", - "elliptic": "^6.4.0", - "nan": "^2.13.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/ts-custom-error": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz", - "integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/ts-node": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", - "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typeforce": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" - }, - "node_modules/typescript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", - "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "hasInstallScript": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "node_modules/v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wif": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", - "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", - "dependencies": { - "bs58check": "<3.0.0" - } - }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } - } - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", - "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", - "dev": true - }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - } - }, - "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - } - }, - "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" - } - }, - "@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", - "dev": true - }, - "@improbable-eng/grpc-web": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.14.1.tgz", - "integrity": "sha512-XaIYuunepPxoiGVLLHmlnVminUGzBTnXr8Wv7khzmLWbNw4TCwJKX09GSMJlKhu/TRk6gms0ySFxewaETSBqgw==", - "requires": { - "browser-headers": "^0.4.1" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz", - "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz", - "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "@terra-money/legacy.proto": { - "version": "npm:@terra-money/terra.proto@0.1.7", - "resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-0.1.7.tgz", - "integrity": "sha512-NXD7f6pQCulvo6+mv6MAPzhOkUzRjgYVuHZE/apih+lVnPG5hDBU0rRYnOGGofwvKT5/jQoOENnFn/gioWWnyQ==", - "requires": { - "google-protobuf": "^3.17.3", - "long": "^4.0.0", - "protobufjs": "~6.11.2" - } - }, - "@terra-money/terra.js": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@terra-money/terra.js/-/terra.js-3.1.7.tgz", - "integrity": "sha512-z7NwVI1gh0846pgQJaPHya6SRKLd/dHWR5UwWG6T2Pf24I2EjCo8YY5Fay30pCvHTYA2NBFgnWfXEZ/31TfB1Q==", - "requires": { - "@terra-money/legacy.proto": "npm:@terra-money/terra.proto@^0.1.7", - "@terra-money/terra.proto": "^2.1.0", - "axios": "^0.27.2", - "bech32": "^2.0.0", - "bip32": "^2.0.6", - "bip39": "^3.0.3", - "bufferutil": "^4.0.3", - "decimal.js": "^10.2.1", - "jscrypto": "^1.0.1", - "readable-stream": "^3.6.0", - "secp256k1": "^4.0.2", - "tmp": "^0.2.1", - "utf-8-validate": "^5.0.5", - "ws": "^7.5.9" - } - }, - "@terra-money/terra.proto": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@terra-money/terra.proto/-/terra.proto-2.1.0.tgz", - "integrity": "sha512-rhaMslv3Rkr+QsTQEZs64FKA4QlfO0DfQHaR6yct/EovenMkibDEQ63dEL6yJA6LCaEQGYhyVB9JO9pTUA8ybw==", - "requires": { - "@improbable-eng/grpc-web": "^0.14.1", - "google-protobuf": "^3.17.3", - "long": "^4.0.0", - "protobufjs": "~6.11.2" - } - }, - "@tsconfig/node10": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", - "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", - "dev": true - }, - "@tsconfig/node12": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", - "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", - "dev": true - }, - "@tsconfig/node14": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", - "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", - "dev": true - }, - "@tsconfig/node16": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", - "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", - "dev": true - }, - "@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, - "@types/node": { - "version": "10.12.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", - "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base-x": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz", - "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "bech32": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", - "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==" - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bip32": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/bip32/-/bip32-2.0.6.tgz", - "integrity": "sha512-HpV5OMLLGTjSVblmrtYRfFFKuQB+GArM0+XP8HGWfJ5vxYBqo+DesvJwOdC2WJ3bCkZShGf0QIfoIpeomVzVdA==", - "requires": { - "@types/node": "10.12.18", - "bs58check": "^2.1.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "tiny-secp256k1": "^1.1.3", - "typeforce": "^1.11.5", - "wif": "^2.0.6" - } - }, - "bip39": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.4.tgz", - "integrity": "sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==", - "requires": { - "@types/node": "11.11.6", - "create-hash": "^1.1.0", - "pbkdf2": "^3.0.9", - "randombytes": "^2.0.1" - }, - "dependencies": { - "@types/node": { - "version": "11.11.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz", - "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==" - } - } - }, - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browser-headers": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" - }, - "bs58": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", - "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=", - "requires": { - "base-x": "^3.0.2" - } - }, - "bs58check": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-2.1.2.tgz", - "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", - "requires": { - "bs58": "^4.0.0", - "create-hash": "^1.1.0", - "safe-buffer": "^5.1.2" - } - }, - "bufferutil": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.7.tgz", - "integrity": "sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==", - "requires": { - "node-gyp-build": "^4.3.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==" - }, - "deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", - "dev": true, - "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - } - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - }, - "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", - "dev": true, - "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, - "requires": { - "flat-cache": "^3.0.4" - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", - "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" - } - }, - "flatted": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", - "dev": true - }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "google-protobuf": { - "version": "3.20.1", - "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.20.1.tgz", - "integrity": "sha512-XMf1+O32FjYIV3CYu6Tuh5PNbfNEU5Xu22X+Xkdb/DUexFlCzhvv7d5Iirm4AOwn8lv4al1YvIhzGrg2j9Zfzw==" - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jscrypto": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/jscrypto/-/jscrypto-1.0.2.tgz", - "integrity": "sha512-r+oNJLGTv1nkNMBBq3c70xYrFDgJOYVgs2OHijz5Ht+0KJ0yObD0oYxC9mN72KLzVfXw+osspg6t27IZvuTUxw==" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "requires": { - "yallist": "^4.0.0" - } - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "requires": { - "mime-db": "1.52.0" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "node-addon-api": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz", - "integrity": "sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==" - }, - "node-gyp-build": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", - "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "17.0.38", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.38.tgz", - "integrity": "sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==" - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "secp256k1": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", - "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", - "requires": { - "elliptic": "^6.5.2", - "node-addon-api": "^2.0.0", - "node-gyp-build": "^4.2.0" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", - "dev": true, - "requires": { - "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "tiny-secp256k1": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.6.tgz", - "integrity": "sha512-FmqJZGduTyvsr2cF3375fqGHUovSwDi/QytexX1Se4BPuPZpTE5Ftp5fg+EFSuEf3lhZqgCRjEG3ydUQ/aNiwA==", - "requires": { - "bindings": "^1.3.0", - "bn.js": "^4.11.8", - "create-hmac": "^1.1.7", - "elliptic": "^6.4.0", - "nan": "^2.13.2" - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "requires": { - "rimraf": "^3.0.0" - } - }, - "ts-custom-error": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.2.0.tgz", - "integrity": "sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==" - }, - "ts-node": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.8.0.tgz", - "integrity": "sha512-/fNd5Qh+zTt8Vt1KbYZjRHCE9sI5i7nqfD/dzBBRDeVXZXS6kToW6R7tTU6Nd4XavFs0mAVCg29Q//ML7WsZYA==", - "dev": true, - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "dependencies": { - "acorn": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.5.0.tgz", - "integrity": "sha512-yXbYeFy+jUuYd3/CDcg2NkIYE991XYX/bje7LmjJigUciaeO1JR4XxXgCIV1/Zc/dRuFEyw1L0pbA+qynJkW5Q==", - "dev": true - } - } - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - }, - "typeforce": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", - "integrity": "sha512-7uc1O8h1M1g0rArakJdf0uLRSSgFcYexrVoKo+bzJd32gd4gDy2L/Z+8/FjPnU9ydY3pEnVPtr9FyscYY60K1g==" - }, - "typescript": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz", - "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "utf-8-validate": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", - "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", - "requires": { - "node-gyp-build": "^4.3.0" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true - }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "wif": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/wif/-/wif-2.0.6.tgz", - "integrity": "sha1-CNP1IFbGZnkplyb63g1DKudLRwQ=", - "requires": { - "bs58check": "<3.0.0" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "requires": {} - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true - } - } -} diff --git a/scripts/package.json b/scripts/package.json deleted file mode 100644 index 4f1837f8..00000000 --- a/scripts/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "scripts", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "type": "module", - "scripts": { - "start": "npm run build-app", - "build-app": "bash build_app.sh", - "build-artifacts": "bash build_release.sh" - }, - "dependencies": { - "@terra-money/terra.js": "^3.1.5", - "dotenv": "^8.2.0", - "ts-custom-error": "^3.2.0" - }, - "devDependencies": { - "eslint": "^7.24.0", - "ts-node": "^10.8.0", - "typescript": "^4.3.5" - } -} \ No newline at end of file diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json deleted file mode 100644 index cf11cd58..00000000 --- a/scripts/tsconfig.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "ts-node": { - "files": true - }, - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */, - "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ - "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ - /* Module Resolution Options */ - "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, - // "baseUrl": ".", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "resolveJsonModule": true, - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - /* Advanced Options */ - "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ - } -} \ No newline at end of file diff --git a/scripts/types.d/chain_configs.ts b/scripts/types.d/chain_configs.ts deleted file mode 100644 index 5ab81f33..00000000 --- a/scripts/types.d/chain_configs.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { readArtifact } from "../helpers.js"; - -export const chainConfigs: Config = readArtifact(`${process.env.CHAIN_ID || "localterra"}`, 'chain_configs'); \ No newline at end of file diff --git a/scripts/types.d/deploy_interfaces.ts b/scripts/types.d/deploy_interfaces.ts deleted file mode 100644 index 0f631a87..00000000 --- a/scripts/types.d/deploy_interfaces.ts +++ /dev/null @@ -1,121 +0,0 @@ -interface GeneralInfo { - multisig: string, - astro_token: string, - xastro_token: string, - factory_addr: string, - generator_addr: string, -} - -type Marketing = { - project: string, - description: string, - marketing: string, - logo: { - url: string - } -} - -type Allocation = { - amount: string, - unlock_schedule: { - start_time: number, - cliff: number, - duration: number, - }, - proposed_receiver: string, -} - -type Allocations = [ - string, - Allocation -] - -interface TeamUnlock { - admin: string, - initMsg: { - owner: string, - astro_token: string, - max_allocations_amount: string - }, - label: string, - change_owner: boolean, - propose_new_owner: { - owner: string, - expires_in: number - }, - allocations: Allocations[] -} - -interface Assembly { - admin: string, - initMsg: { - xastro_token_addr: string, - builder_unlock_addr: string, - proposal_voting_period: number, - proposal_effective_delay: number, - proposal_expiration_period: number, - proposal_required_deposit: string, - proposal_required_quorum: string, - proposal_required_threshold: string, - whitelisted_links: string[], - vxastro_token_addr?: string, - voting_escrow_delegator_addr?: string - }, - label: string -} - -interface VotingEscrow { - admin: string, - initMsg: { - owner: string, - guardian_addr?: string, - deposit_token_addr: string, - marketing: Marketing, - logo_urls_whitelist: string[] - }, - label: string, -} - -interface FeeDistributor { - admin: string, - initMsg: { - owner: string, - astro_token: string, - voting_escrow_addr: string, - claim_many_limit?: number, - is_claim_disabled?: boolean - }, - label: string, -} - -interface GeneratorController { - admin: string, - initMsg: { - owner: string, - escrow_addr: string, - generator_addr: string, - factory_addr: string, - pools_limit: number, - }, - label: string -} - -interface VotingEscrowDelegation { - admin: string, - initMsg: { - owner: string, - voting_escrow_addr: string, - nft_code_id: number - }, - label: string -} - -interface Config { - teamUnlock: TeamUnlock, - assembly: Assembly, - votingEscrow: VotingEscrow, - feeDistributor: FeeDistributor, - generatorController: GeneratorController, - votingEscrowDelegation: VotingEscrowDelegation, - generalInfo: GeneralInfo -} From 996ac43e7bf551e238aca10338a3886eea79afc3 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:18:48 +0400 Subject: [PATCH 46/47] update astroport deps --- .github/workflows/check_artifacts.yml | 50 +---------- .github/workflows/code_coverage.yml | 4 - Cargo.lock | 85 ++++++++++++------- contracts/assembly/Cargo.toml | 4 +- .../generator_controller_lite/Cargo.toml | 12 +-- contracts/hub/Cargo.toml | 2 +- contracts/outpost/Cargo.toml | 2 +- contracts/voting_escrow_lite/Cargo.toml | 2 +- packages/astroport-governance/Cargo.toml | 5 +- packages/astroport-governance/src/lib.rs | 2 - packages/astroport-tests-lite/Cargo.toml | 14 +-- 11 files changed, 79 insertions(+), 103 deletions(-) diff --git a/.github/workflows/check_artifacts.yml b/.github/workflows/check_artifacts.yml index f311595e..79109e39 100644 --- a/.github/workflows/check_artifacts.yml +++ b/.github/workflows/check_artifacts.yml @@ -8,59 +8,17 @@ on: env: CARGO_TERM_COLOR: always - CARGO_NET_GIT_FETCH_WITH_CLI: true jobs: - fetch_deps: - name: Fetch cargo dependencies + check-artifacts-size: runs-on: ubuntu-latest - + name: Check Artifacts Size steps: - name: Cancel Previous Runs uses: styfle/cancel-workflow-action@0.11.0 with: access_token: ${{ github.token }} - - uses: actions/checkout@v3 - - uses: webfactory/ssh-agent@v0.7.0 - with: - ssh-private-key: | - ${{ secrets.CORE_PRIVATE_KEY }} - - - uses: actions/cache@v3 - if: always() - with: - path: | - ~/.cargo/bin - ~/.cargo/git/checkouts - ~/.cargo/git/db - ~/.cargo/registry/cache - ~/.cargo/registry/index - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- - - - run: | - git config url."ssh://git@github.com/astroport-fi/hidden_astroport_core.git".insteadOf "https://github.com/astroport-fi/hidden_astroport_core" - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: 1.75.0 - override: true - - - name: Fetch cargo deps - uses: actions-rs/cargo@v1 - with: - command: fetch - args: --locked - - check-artifacts-size: - runs-on: ubuntu-latest - name: Check Artifacts Size - needs: fetch_deps - steps: - name: Checkout sources uses: actions/checkout@v3 @@ -73,8 +31,6 @@ jobs: ~/.cargo/registry/cache ~/.cargo/registry/index key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - # docker can't pull private sources, so we fail if cache is missing - fail-on-cache-miss: true - name: Build Artifacts run: | @@ -82,7 +38,7 @@ jobs: -v "$GITHUB_WORKSPACE":/code \ -v ~/.cargo/registry:/usr/local/cargo/registry \ -v ~/.cargo/git:/usr/local/cargo/git \ - cosmwasm/workspace-optimizer:0.12.13 + cosmwasm/workspace-optimizer:0.15.1 - name: Save artifacts cache uses: actions/cache/save@v3 diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index 20afda72..b4bff4c9 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -8,10 +8,6 @@ on: branches: - main -env: - CARGO_TERM_COLOR: always - CARGO_NET_GIT_FETCH_WITH_CLI: true - jobs: code-coverage: name: Code coverage diff --git a/Cargo.lock b/Cargo.lock index db92af5f..17499a13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,7 +25,7 @@ version = "2.0.0" dependencies = [ "anyhow", "astro-satellite", - "astroport 4.0.0", + "astroport 4.0.0 (git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4)", "astroport-governance 3.0.0", "astroport-staking", "astroport-tokenfactory-tracker", @@ -44,26 +44,25 @@ dependencies = [ [[package]] name = "astro-satellite" -version = "1.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1#61f3cf90ac7e48de93224e906171ebe206d7f860" +version = "1.2.0" +source = "git+https://github.com/astroport-fi/astroport_ibc#0b7f69eeb853ad4dfc871a4d10f34cbd251517eb" dependencies = [ "astro-satellite-package", - "astroport-ibc 1.2.1 (git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1)", + "astroport-ibc 1.3.0", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw2 0.15.1", - "ibc-controller-package 0.1.0", - "itertools 0.10.5", + "cw2 1.1.2", + "ibc-controller-package 1.1.1", + "itertools 0.12.0", "thiserror", ] [[package]] name = "astro-satellite-package" -version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1#61f3cf90ac7e48de93224e906171ebe206d7f860" +version = "1.2.0" +source = "git+https://github.com/astroport-fi/astroport_ibc#0b7f69eeb853ad4dfc871a4d10f34cbd251517eb" dependencies = [ "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", "cosmwasm-schema", @@ -103,9 +102,25 @@ dependencies = [ [[package]] name = "astroport" version = "4.0.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#350dd47ee30af5749251a822fd2f7e08942cf854" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" +dependencies = [ + "astroport-circular-buffer 0.2.0 (git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4)", + "cosmwasm-schema", + "cosmwasm-std", + "cw-asset", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "itertools 0.12.0", + "uint", +] + +[[package]] +name = "astroport" +version = "4.0.0" +source = "git+https://github.com/astroport-fi/astroport-core#350dd47ee30af5749251a822fd2f7e08942cf854" dependencies = [ - "astroport-circular-buffer", + "astroport-circular-buffer 0.2.0 (git+https://github.com/astroport-fi/astroport-core)", "cosmwasm-schema", "cosmwasm-std", "cw-asset", @@ -119,7 +134,18 @@ dependencies = [ [[package]] name = "astroport-circular-buffer" version = "0.2.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#350dd47ee30af5749251a822fd2f7e08942cf854" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "thiserror", +] + +[[package]] +name = "astroport-circular-buffer" +version = "0.2.0" +source = "git+https://github.com/astroport-fi/astroport-core#350dd47ee30af5749251a822fd2f7e08942cf854" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -156,7 +182,6 @@ dependencies = [ name = "astroport-governance" version = "3.0.0" dependencies = [ - "astroport 4.0.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.2.0", @@ -175,8 +200,8 @@ dependencies = [ [[package]] name = "astroport-ibc" -version = "1.2.1" -source = "git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1#61f3cf90ac7e48de93224e906171ebe206d7f860" +version = "1.3.0" +source = "git+https://github.com/astroport-fi/astroport_ibc#0b7f69eeb853ad4dfc871a4d10f34cbd251517eb" dependencies = [ "cosmwasm-schema", ] @@ -184,9 +209,9 @@ dependencies = [ [[package]] name = "astroport-staking" version = "2.0.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#350dd47ee30af5749251a822fd2f7e08942cf854" +source = "git+https://github.com/astroport-fi/astroport-core#350dd47ee30af5749251a822fd2f7e08942cf854" dependencies = [ - "astroport 4.0.0", + "astroport 4.0.0 (git+https://github.com/astroport-fi/astroport-core)", "cosmwasm-std", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -198,9 +223,9 @@ dependencies = [ [[package]] name = "astroport-tokenfactory-tracker" version = "1.0.0" -source = "git+https://github.com/astroport-fi/hidden_astroport_core#350dd47ee30af5749251a822fd2f7e08942cf854" +source = "git+https://github.com/astroport-fi/astroport-core#350dd47ee30af5749251a822fd2f7e08942cf854" dependencies = [ - "astroport 4.0.0", + "astroport 4.0.0 (git+https://github.com/astroport-fi/astroport-core)", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.2.0", @@ -266,7 +291,7 @@ checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" name = "builder-unlock" version = "3.0.0" dependencies = [ - "astroport 4.0.0", + "astroport 4.0.0 (git+https://github.com/astroport-fi/astroport-core)", "astroport-governance 3.0.0", "cosmwasm-schema", "cosmwasm-std", @@ -799,23 +824,23 @@ dependencies = [ [[package]] name = "ibc-controller-package" -version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1#61f3cf90ac7e48de93224e906171ebe206d7f860" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfcf94f5691716bfecb45e6bb6a82a5c11a392d501c2a695589c5087671f7c33" dependencies = [ - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", - "astroport-ibc 1.2.1 (git+https://github.com/astroport-fi/astroport_ibc?tag=v1.2.1)", + "astroport-governance 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "astroport-ibc 1.2.1", "cosmwasm-schema", "cosmwasm-std", ] [[package]] name = "ibc-controller-package" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcf94f5691716bfecb45e6bb6a82a5c11a392d501c2a695589c5087671f7c33" +version = "1.1.1" +source = "git+https://github.com/astroport-fi/astroport_ibc#0b7f69eeb853ad4dfc871a4d10f34cbd251517eb" dependencies = [ - "astroport-governance 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "astroport-ibc 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-ibc 1.3.0", "cosmwasm-schema", "cosmwasm-std", ] diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index b6e2a11f..5b530b74 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -24,8 +24,8 @@ thiserror.workspace = true cosmwasm-schema.workspace = true cw-utils.workspace = true astroport-governance = { path = "../../packages/astroport-governance", version = "3" } -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", version = "4" } -astro-satellite = { git = "https://github.com/astroport-fi/astroport_ibc", tag = "v1.2.1", features = ["library"] } +astroport = { git = "https://github.com/astroport-fi/astroport-core", version = "4", branch = "feat/astroport_v4" } +astro-satellite = { git = "https://github.com/astroport-fi/astroport_ibc", features = ["library"], version = "1.2.0" } ibc-controller-package = "1.0.0" [dev-dependencies] diff --git a/contracts/generator_controller_lite/Cargo.toml b/contracts/generator_controller_lite/Cargo.toml index 0fec7417..f10c47de 100644 --- a/contracts/generator_controller_lite/Cargo.toml +++ b/contracts/generator_controller_lite/Cargo.toml @@ -36,12 +36,12 @@ cosmwasm-schema = "1.1" cw-multi-test = "0.16" astroport-tests-lite = { path = "../../packages/astroport-tests-lite" } -astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-generator = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-pair = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-factory = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-token = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-staking = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-whitelist = { git = "https://github.com/astroport-fi/astroport-core" } cw20 = "0.15" voting-escrow = { path = "../voting_escrow" } anyhow = "1" diff --git a/contracts/hub/Cargo.toml b/contracts/hub/Cargo.toml index c010a372..6041e3b1 100644 --- a/contracts/hub/Cargo.toml +++ b/contracts/hub/Cargo.toml @@ -32,7 +32,7 @@ cw-storage-plus = "0.15" schemars = "0.8.12" serde = { version = "1.0.164", default-features = false, features = ["derive"] } thiserror = "1.0.40" -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport = { git = "https://github.com/astroport-fi/astroport-core" } astroport-governance = { path = "../../packages/astroport-governance" } serde-json-wasm = "0.5.1" diff --git a/contracts/outpost/Cargo.toml b/contracts/outpost/Cargo.toml index 9bfe2838..722742f6 100644 --- a/contracts/outpost/Cargo.toml +++ b/contracts/outpost/Cargo.toml @@ -34,7 +34,7 @@ schemars = "0.8.12" semver = "1.0.17" serde = { version = "1.0.164", default-features = false, features = ["derive"] } thiserror = "1.0.40" -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport = { git = "https://github.com/astroport-fi/astroport-core" } astroport-governance = { path = "../../packages/astroport-governance" } serde-json-wasm = "0.5.1" base64 = { version = "0.13.0" } diff --git a/contracts/voting_escrow_lite/Cargo.toml b/contracts/voting_escrow_lite/Cargo.toml index 8e2e7447..fc5b22ff 100644 --- a/contracts/voting_escrow_lite/Cargo.toml +++ b/contracts/voting_escrow_lite/Cargo.toml @@ -37,6 +37,6 @@ cosmwasm-schema = "1.5" [dev-dependencies] cw-multi-test = "0.20" astroport-generator-controller = { path = "../../contracts/generator_controller_lite", package = "generator-controller-lite" } -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core", branch = "feat/neutron-migration" } +astroport = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/neutron-migration" } anyhow = "1" proptest = "1.0" diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 24716ce9..ea51744b 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -5,6 +5,8 @@ authors = ["Astroport"] edition = "2021" repository = "https://github.com/astroport-fi/astroport-governance" homepage = "https://astroport.fi" +description = "Astroport Governance common types, queriers and other utils" +license = "Apache-2.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -18,5 +20,4 @@ cw20 = "1.1" cosmwasm-std = { workspace = true, features = ["ibc3"] } cw-storage-plus.workspace = true cosmwasm-schema.workspace = true -thiserror.workspace = true -astroport = { git = "https://github.com/astroport-fi/astroport-core", version = "4" } +thiserror.workspace = true \ No newline at end of file diff --git a/packages/astroport-governance/src/lib.rs b/packages/astroport-governance/src/lib.rs index b973465c..874adb49 100644 --- a/packages/astroport-governance/src/lib.rs +++ b/packages/astroport-governance/src/lib.rs @@ -12,8 +12,6 @@ pub mod voting_escrow; pub mod voting_escrow_delegation; pub mod voting_escrow_lite; -pub use astroport; - // Default pagination constants pub const DEFAULT_LIMIT: u32 = 10; pub const MAX_LIMIT: u32 = 30; diff --git a/packages/astroport-tests-lite/Cargo.toml b/packages/astroport-tests-lite/Cargo.toml index 6c27a081..359169cc 100644 --- a/packages/astroport-tests-lite/Cargo.toml +++ b/packages/astroport-tests-lite/Cargo.toml @@ -18,17 +18,17 @@ cosmwasm-std = "1.1" cosmwasm-schema = "1.1" cw-multi-test = "0.16" -astroport = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport = { git = "https://github.com/astroport-fi/astroport-core" } astroport-escrow-fee-distributor = { path = "../../contracts/escrow_fee_distributor" } astroport-governance = { path = "../astroport-governance" } voting-escrow-lite = { package = "astroport-voting-escrow-lite", path = "../../contracts/voting_escrow_lite" } generator-controller-lite = { path = "../../contracts/generator_controller_lite" } astro-assembly = { path = "../../contracts/assembly" } -astroport-generator = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-pair = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-factory = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-token = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-staking = { git = "https://github.com/astroport-fi/hidden_astroport_core" } -astroport-whitelist = { git = "https://github.com/astroport-fi/hidden_astroport_core" } +astroport-generator = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-pair = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-factory = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-token = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-staking = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-whitelist = { git = "https://github.com/astroport-fi/astroport-core" } anyhow = "1" From 67cba9037fb9854caa6752a591fd186752b95ee5 Mon Sep 17 00:00:00 2001 From: Timofey <5527315+epanchee@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:45:20 +0400 Subject: [PATCH 47/47] temporary set astroport-core branch in deps --- Cargo.lock | 197 ++++++++++++---------------- contracts/assembly/Cargo.toml | 4 +- contracts/builder_unlock/Cargo.toml | 2 +- 3 files changed, 88 insertions(+), 115 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 17499a13..9e4107c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ahash" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom", "once_cell", @@ -15,9 +15,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "astro-assembly" @@ -25,14 +25,14 @@ version = "2.0.0" dependencies = [ "anyhow", "astro-satellite", - "astroport 4.0.0 (git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4)", + "astroport 4.0.0", "astroport-governance 3.0.0", "astroport-staking", "astroport-tokenfactory-tracker", "builder-unlock", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.20.0 (git+https://github.com/astroport-fi/cw-multi-test?branch=feat/bank_with_send_hooks)", + "cw-multi-test 0.20.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -55,7 +55,7 @@ dependencies = [ "cw-storage-plus 0.15.1", "cw2 1.1.2", "ibc-controller-package 1.1.1", - "itertools 0.12.0", + "itertools 0.12.1", "thiserror", ] @@ -104,30 +104,14 @@ name = "astroport" version = "4.0.0" source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" dependencies = [ - "astroport-circular-buffer 0.2.0 (git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4)", - "cosmwasm-schema", - "cosmwasm-std", - "cw-asset", - "cw-storage-plus 1.2.0", - "cw-utils 1.0.3", - "cw20 1.1.2", - "itertools 0.12.0", - "uint", -] - -[[package]] -name = "astroport" -version = "4.0.0" -source = "git+https://github.com/astroport-fi/astroport-core#350dd47ee30af5749251a822fd2f7e08942cf854" -dependencies = [ - "astroport-circular-buffer 0.2.0 (git+https://github.com/astroport-fi/astroport-core)", + "astroport-circular-buffer", "cosmwasm-schema", "cosmwasm-std", "cw-asset", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw20 1.1.2", - "itertools 0.12.0", + "itertools 0.12.1", "uint", ] @@ -142,17 +126,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "astroport-circular-buffer" -version = "0.2.0" -source = "git+https://github.com/astroport-fi/astroport-core#350dd47ee30af5749251a822fd2f7e08942cf854" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 1.2.0", - "thiserror", -] - [[package]] name = "astroport-governance" version = "1.2.0" @@ -169,7 +142,7 @@ dependencies = [ [[package]] name = "astroport-governance" version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance#f0ef7c6dde76fc77ce360262923366a5cde3c3f8" +source = "git+https://github.com/astroport-fi/astroport-governance#182dd5bc201dd634995b5e4dc9e2774495693703" dependencies = [ "astroport 2.10.0", "cosmwasm-schema", @@ -209,9 +182,9 @@ dependencies = [ [[package]] name = "astroport-staking" version = "2.0.0" -source = "git+https://github.com/astroport-fi/astroport-core#350dd47ee30af5749251a822fd2f7e08942cf854" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" dependencies = [ - "astroport 4.0.0 (git+https://github.com/astroport-fi/astroport-core)", + "astroport 4.0.0", "cosmwasm-std", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -223,9 +196,9 @@ dependencies = [ [[package]] name = "astroport-tokenfactory-tracker" version = "1.0.0" -source = "git+https://github.com/astroport-fi/astroport-core#350dd47ee30af5749251a822fd2f7e08942cf854" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" dependencies = [ - "astroport 4.0.0 (git+https://github.com/astroport-fi/astroport-core)", + "astroport 4.0.0", "cosmwasm-schema", "cosmwasm-std", "cw-storage-plus 1.2.0", @@ -235,9 +208,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "base16ct" @@ -283,19 +256,19 @@ dependencies = [ [[package]] name = "bnum" -version = "0.8.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" +checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" [[package]] name = "builder-unlock" version = "3.0.0" dependencies = [ - "astroport 4.0.0 (git+https://github.com/astroport-fi/astroport-core)", + "astroport 4.0.0", "astroport-governance 3.0.0", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-multi-test 0.20.1", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -310,9 +283,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cfg-if" @@ -322,9 +295,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.32" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "num-traits", ] @@ -337,9 +310,9 @@ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cosmwasm-crypto" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6aa9f904de106fa16443ad14ec2abe75e94ba003bb61c681c0e43d4c58d2a" +checksum = "9934c79e58d9676edfd592557dee765d2a6ef54c09d5aa2edb06156b00148966" dependencies = [ "digest 0.10.7", "ecdsa", @@ -351,18 +324,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40abec852f3d4abec6d44ead9a58b78325021a1ead1e7229c3471414e57b2e49" +checksum = "bc5e72e330bd3bdab11c52b5ecbdeb6a8697a004c57964caeb5d876f0b088b3c" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b166215fbfe93dc5575bae062aa57ae7bb41121cffe53bac33b033257949d2a9" +checksum = "ac3e3a2136e2a60e8b6582f5dffca5d1a683ed77bf38537d330bc1dfccd69010" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -373,9 +346,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf12f8e20bb29d1db66b7ca590bc2f670b548d21e9be92499bc0f9022a994a8" +checksum = "f5d803bea6bd9ed61bd1ee0b4a2eb09ee20dbb539cc6e0b8795614d20952ebb1" dependencies = [ "proc-macro2", "quote", @@ -384,9 +357,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad011ae7447188e26e4a7dbca2fcd0fc186aa21ae5c86df0503ea44c78f9e469" +checksum = "ef8666e572a3a2519010dde88c04d16e9339ae751b56b2bb35081fe3f7d6be74" dependencies = [ "base64", "bech32", @@ -475,9 +448,9 @@ dependencies = [ [[package]] name = "cw-asset" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431e57314dceabd29a682c78bb3ff7c641f8bdc8b915400bb9956cb911e8e571" +checksum = "c999a12f8cd8736f6f86e9a4ede5905530cb23cfdef946b9da1c506ad1b70799" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -490,8 +463,7 @@ dependencies = [ [[package]] name = "cw-multi-test" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fff029689ae89127cf6d7655809a68d712f3edbdb9686c70b018ba438b26ca" +source = "git+https://github.com/astroport-fi/cw-multi-test?branch=feat/bank_with_send_hooks#80ebf1aff909d5438fff093b6243c5d7cbf924b3" dependencies = [ "anyhow", "bech32", @@ -499,7 +471,7 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "derivative", - "itertools 0.12.0", + "itertools 0.12.1", "prost 0.12.3", "schemars", "serde", @@ -509,8 +481,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.20.0" -source = "git+https://github.com/astroport-fi/cw-multi-test?branch=feat/bank_with_send_hooks#80ebf1aff909d5438fff093b6243c5d7cbf924b3" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc392a5cb7e778e3f90adbf7faa43c4db7f35b6623224b08886d796718edb875" dependencies = [ "anyhow", "bech32", @@ -518,7 +491,7 @@ dependencies = [ "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "derivative", - "itertools 0.12.0", + "itertools 0.12.1", "prost 0.12.3", "schemars", "serde", @@ -649,9 +622,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "zeroize", @@ -691,9 +664,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" @@ -726,9 +699,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "elliptic-curve" @@ -865,24 +838,24 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "k256" -version = "0.13.3" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" dependencies = [ "cfg-if", "ecdsa", @@ -894,15 +867,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] @@ -915,9 +888,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "osmosis-std" @@ -960,9 +933,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1010,7 +983,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.57", ] [[package]] @@ -1067,9 +1040,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schemars" @@ -1111,15 +1084,15 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -1135,22 +1108,22 @@ dependencies = [ [[package]] name = "serde-json-wasm" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" +checksum = "9e9213a07d53faa0b8dd81e767a54a8188a242fdb9be99ab75ec576a774bfdd7" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.57", ] [[package]] @@ -1166,9 +1139,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1244,9 +1217,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", @@ -1271,7 +1244,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.57", ] [[package]] @@ -1282,28 +1255,28 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.57", "test-case-core", ] [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.57", ] [[package]] diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 5b530b74..47e224d8 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -31,8 +31,8 @@ ibc-controller-package = "1.0.0" [dev-dependencies] cw-multi-test = { git = "https://github.com/astroport-fi/cw-multi-test", branch = "feat/bank_with_send_hooks", features = ["cosmwasm_1_1"] } osmosis-std = "0.21" -astroport-staking = { git = "https://github.com/astroport-fi/astroport-core", version = "2" } -astroport-tokenfactory-tracker = { git = "https://github.com/astroport-fi/astroport-core", version = "1" } +astroport-staking = { git = "https://github.com/astroport-fi/astroport-core", version = "2", branch = "feat/astroport_v4" } +astroport-tokenfactory-tracker = { git = "https://github.com/astroport-fi/astroport-core", version = "1", branch = "feat/astroport_v4" } builder-unlock = { path = "../builder_unlock", version = "3" } anyhow = "1" test-case = "3.3.1" \ No newline at end of file diff --git a/contracts/builder_unlock/Cargo.toml b/contracts/builder_unlock/Cargo.toml index 802f1247..d8e51fa3 100644 --- a/contracts/builder_unlock/Cargo.toml +++ b/contracts/builder_unlock/Cargo.toml @@ -23,7 +23,7 @@ cw-storage-plus.workspace = true cosmwasm-schema.workspace = true thiserror.workspace = true astroport-governance = { path = "../../packages/astroport-governance", version = "3" } -astroport = { git = "https://github.com/astroport-fi/astroport-core", version = "4" } +astroport = { git = "https://github.com/astroport-fi/astroport-core", version = "4", branch = "feat/astroport_v4" } [dev-dependencies] cw-multi-test = "0.20" \ No newline at end of file