diff --git a/.github/workflows/check_artifacts.yml b/.github/workflows/check_artifacts.yml new file mode 100644 index 00000000..79109e39 --- /dev/null +++ b/.github/workflows/check_artifacts.yml @@ -0,0 +1,72 @@ +name: Compiled binaries checks + +on: + pull_request: + push: + branches: + - main + +env: + CARGO_TERM_COLOR: always + +jobs: + 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 }} + + - 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') }} + + - 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.15.1 + + - 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,cosmwasm_1_1 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 bcfdf126..b4bff4c9 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -36,7 +36,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.64.0 + toolchain: 1.75.0 override: true - name: Run cargo-tarpaulin @@ -45,7 +45,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 47f49ba8..e5928dd0 100644 --- a/.github/workflows/tests_and_checks.yml +++ b/.github/workflows/tests_and_checks.yml @@ -16,7 +16,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 }} @@ -37,7 +37,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: 1.65.0 + toolchain: 1.75.0 override: true components: rustfmt, clippy diff --git a/Cargo.lock b/Cargo.lock index ff24d20f..9e4107c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ "getrandom", "once_cell", @@ -15,126 +15,124 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] -name = "ap-native-coin-registry" -version = "1.0.0" -source = "git+https://github.com/astroport-fi/astroport-core?branch=merge/release#ff923ddac6b99c10bed53c7af6c8c4a5d2f5995e" +name = "astro-assembly" +version = "2.0.0" dependencies = [ + "anyhow", + "astro-satellite", + "astroport 4.0.0", + "astroport-governance 3.0.0", + "astroport-staking", + "astroport-tokenfactory-tracker", + "builder-unlock", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", + "cw-multi-test 0.20.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "ibc-controller-package 1.0.0", + "osmosis-std", + "test-case", + "thiserror", ] [[package]] -name = "astro-assembly" -version = "1.5.0" +name = "astro-satellite" +version = "1.2.0" +source = "git+https://github.com/astroport-fi/astroport_ibc#0b7f69eeb853ad4dfc871a4d10f34cbd251517eb" dependencies = [ - "anyhow", - "astroport-governance 1.2.0", - "astroport-nft", - "astroport-staking", - "astroport-token", - "astroport-xastro-token", - "builder-unlock", + "astro-satellite-package", + "astroport-ibc 1.3.0", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cosmwasm-storage", "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "ibc-controller-package", + "cw2 1.1.2", + "ibc-controller-package 1.1.1", + "itertools 0.12.1", "thiserror", - "voting-escrow", - "voting-escrow-delegation", ] [[package]] -name = "astroport" -version = "2.4.0" -source = "git+https://github.com/astroport-fi/astroport-core?branch=merge/release#ff923ddac6b99c10bed53c7af6c8c4a5d2f5995e" +name = "astro-satellite-package" +version = "1.2.0" +source = "git+https://github.com/astroport-fi/astroport_ibc#0b7f69eeb853ad4dfc871a4d10f34cbd251517eb" dependencies = [ - "ap-native-coin-registry", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", "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" -source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#8d8e65566d17c2933b1fc9367af2d654ac79c856" +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-escrow-fee-distributor" -version = "1.0.2" +name = "astroport" +version = "2.10.0" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#11e7a81d4b18a40bed916177061a549633e02b1b" dependencies = [ - "astroport-governance 1.2.0", - "astroport-tests", - "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", "cw-storage-plus 0.15.1", - "cw2 0.15.1", + "cw-utils 1.0.3", "cw20 0.15.1", - "thiserror", + "cw3", + "itertools 0.10.5", + "uint", ] [[package]] -name = "astroport-factory" -version = "1.5.1" -source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#8d8e65566d17c2933b1fc9367af2d654ac79c856" +name = "astroport" +version = "4.0.0" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" dependencies = [ - "astroport 2.10.0", + "astroport-circular-buffer", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "itertools", - "protobuf", - "thiserror", + "cw-asset", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "itertools 0.12.1", + "uint", ] [[package]] -name = "astroport-generator" -version = "2.3.0" -source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#8d8e65566d17c2933b1fc9367af2d654ac79c856" +name = "astroport-circular-buffer" +version = "0.2.0" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" dependencies = [ - "astroport 2.10.0", - "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance?branch=main)", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw1-whitelist", - "cw2 0.15.1", - "cw20 0.15.1", - "protobuf", + "cw-storage-plus 1.2.0", "thiserror", ] [[package]] name = "astroport-governance" version = "1.2.0" +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", @@ -144,7 +142,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#7e8126d688a640799dd059f93616a66b6c1ee12f" +source = "git+https://github.com/astroport-fi/astroport-governance#182dd5bc201dd634995b5e4dc9e2774495693703" dependencies = [ "astroport 2.10.0", "cosmwasm-schema", @@ -155,149 +153,76 @@ dependencies = [ [[package]] name = "astroport-governance" -version = "1.2.0" -source = "git+https://github.com/astroport-fi/astroport-governance?branch=main#193a33d72f9ee608d7fcff6c1f0425b84e22777a" +version = "3.0.0" dependencies = [ - "astroport 2.4.0", "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw20 0.15.1", + "cw-storage-plus 1.2.0", + "cw20 1.1.2", + "thiserror", ] [[package]] name = "astroport-ibc" -version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=feat/update_deps_2023_05_22#5ce8b68dc89965790558523ab84c6b2f153c5181" -dependencies = [ - "cosmwasm-schema", -] - -[[package]] -name = "astroport-nft" -version = "1.0.0" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93c0ce66970f190873b30f862b0cd39fb0d8499678a1860446aa60d9618671f4" dependencies = [ - "astroport-governance 1.2.0", "cosmwasm-schema", - "cosmwasm-std", - "cw2 0.15.1", - "cw721", - "cw721-base", ] [[package]] -name = "astroport-pair" -version = "1.3.1" -source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#8d8e65566d17c2933b1fc9367af2d654ac79c856" +name = "astroport-ibc" +version = "1.3.0" +source = "git+https://github.com/astroport-fi/astroport_ibc#0b7f69eeb853ad4dfc871a4d10f34cbd251517eb" dependencies = [ - "astroport 2.10.0", "cosmwasm-schema", - "cosmwasm-std", - "cw-storage-plus 0.15.1", - "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/astroport-core?branch=feat/merge_hidden_2023_05_22#8d8e65566d17c2933b1fc9367af2d654ac79c856" +version = "2.0.0" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" dependencies = [ - "astroport 2.10.0", - "cosmwasm-schema", + "astroport 4.0.0", "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "protobuf", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "osmosis-std", "thiserror", ] [[package]] -name = "astroport-tests" +name = "astroport-tokenfactory-tracker" version = "1.0.0" +source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/astroport_v4#589efb629cc9189697a203882fe651dfd6d316c7" dependencies = [ - "anyhow", - "astroport 2.10.0", - "astroport-escrow-fee-distributor", - "astroport-factory", - "astroport-generator", - "astroport-governance 1.2.0", - "astroport-pair", - "astroport-staking", - "astroport-token", - "astroport-whitelist", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "cw2 0.15.1", - "cw20 0.15.1", - "generator-controller", - "voting-escrow", -] - -[[package]] -name = "astroport-token" -version = "1.1.1" -source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#8d8e65566d17c2933b1fc9367af2d654ac79c856" -dependencies = [ - "astroport 2.10.0", - "cosmwasm-schema", - "cosmwasm-std", - "cw2 0.15.1", - "cw20 0.15.1", - "cw20-base", - "snafu", -] - -[[package]] -name = "astroport-whitelist" -version = "1.0.1" -source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#8d8e65566d17c2933b1fc9367af2d654ac79c856" -dependencies = [ - "astroport 2.10.0", + "astroport 4.0.0", "cosmwasm-schema", "cosmwasm-std", - "cw1-whitelist", - "cw2 0.15.1", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", "thiserror", ] -[[package]] -name = "astroport-xastro-token" -version = "1.0.2" -source = "git+https://github.com/astroport-fi/astroport-core?branch=feat/merge_hidden_2023_05_22#8d8e65566d17c2933b1fc9367af2d654ac79c856" -dependencies = [ - "astroport 2.10.0", - "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" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "base16ct" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] name = "base64" -version = "0.13.1" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -306,25 +231,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[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" +name = "bech32" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" [[package]] name = "block-buffer" @@ -344,39 +254,38 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bnum" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" + [[package]] name = "builder-unlock" -version = "1.2.3" +version = "3.0.0" dependencies = [ - "astroport 2.10.0", - "astroport-governance 1.2.0", - "astroport-token", + "astroport 4.0.0", + "astroport-governance 3.0.0", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", + "cw-multi-test 0.20.1", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", "thiserror", ] [[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.79" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cfg-if" @@ -384,19 +293,29 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" +dependencies = [ + "num-traits", +] + [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "cosmwasm-crypto" -version = "1.2.5" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75836a10cb9654c54e77ee56da94d592923092a10b369cdb0dbd56acefc16340" +checksum = "9934c79e58d9676edfd592557dee765d2a6ef54c09d5aa2edb06156b00148966" dependencies = [ "digest 0.10.7", + "ecdsa", "ed25519-zebra", "k256", "rand_core 0.6.4", @@ -405,18 +324,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.2.5" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c9f7f0e51bfc7295f7b2664fe8513c966428642aa765dad8a74acdab5e0c773" +checksum = "bc5e72e330bd3bdab11c52b5ecbdeb6a8697a004c57964caeb5d876f0b088b3c" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.5" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f00b363610218eea83f24bbab09e1a7c3920b79f068334fdfcc62f6129ef9fc" +checksum = "ac3e3a2136e2a60e8b6582f5dffca5d1a683ed77bf38537d330bc1dfccd69010" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -427,9 +346,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.5" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae38f909b2822d32b275c9e2db9728497aa33ffe67dd463bc67c6a3b7092785c" +checksum = "f5d803bea6bd9ed61bd1ee0b4a2eb09ee20dbb539cc6e0b8795614d20952ebb1" dependencies = [ "proc-macro2", "quote", @@ -438,11 +357,13 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.5" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49b85345e811c8e80ec55d0d091e4fcb4f00f97ab058f9be5f614c444a730cb" +checksum = "ef8666e572a3a2519010dde88c04d16e9339ae751b56b2bb35081fe3f7d6be74" dependencies = [ "base64", + "bech32", + "bnum", "cosmwasm-crypto", "cosmwasm-derive", "derivative", @@ -451,16 +372,16 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", - "sha2 0.10.6", + "sha2 0.10.8", + "static_assertions", "thiserror", - "uint", ] [[package]] name = "cosmwasm-storage" -version = "1.2.5" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3737a3aac48f5ed883b5b73bfb731e77feebd8fc6b43419844ec2971072164d" +checksum = "66de2ab9db04757bcedef2b5984fbe536903ada4a8a9766717a4a71197ef34f6" dependencies = [ "cosmwasm-std", "serde", @@ -468,9 +389,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -483,9 +404,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.4.9" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -517,101 +438,115 @@ dependencies = [ ] [[package]] -name = "cw-multi-test" -version = "0.15.1" +name = "cw-address-like" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e81b4a7821d5eeba0d23f737c16027b39a600742ca8c32eb980895ffd270f4" +checksum = "451a4691083a88a3c0630a8a88799e9d4cd6679b7ce8ff22b8da2873ff31d380" dependencies = [ - "anyhow", "cosmwasm-std", - "cosmwasm-storage", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "derivative", - "itertools", - "prost", - "schemars", - "serde", - "thiserror", ] [[package]] -name = "cw-storage-plus" -version = "0.15.1" +name = "cw-asset" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" +checksum = "c999a12f8cd8736f6f86e9a4ede5905530cb23cfdef946b9da1c506ad1b70799" +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" +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.1", + "prost 0.12.3", "schemars", "serde", + "sha2 0.10.8", + "thiserror", ] [[package]] -name = "cw-storage-plus" -version = "1.0.1" +name = "cw-multi-test" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053a5083c258acd68386734f428a5a171b29f7d733151ae83090c6fcc9417ffa" +checksum = "cc392a5cb7e778e3f90adbf7faa43c4db7f35b6623224b08886d796718edb875" dependencies = [ + "anyhow", + "bech32", "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "derivative", + "itertools 0.12.1", + "prost 0.12.3", "schemars", "serde", + "sha2 0.10.8", + "thiserror", ] [[package]] -name = "cw-utils" +name = "cw-storage-plus" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ae0b69fa7679de78825b4edeeec045066aa2b2c4b6e063d80042e565bb4da5c" +checksum = "dc6cf70ef7686e2da9ad7b067c5942cd3e88dd9453f7af42f54557f8af300fb0" dependencies = [ - "cosmwasm-schema", "cosmwasm-std", - "cw2 0.15.1", "schemars", - "semver", "serde", - "thiserror", ] [[package]] -name = "cw-utils" -version = "1.0.1" +name = "cw-storage-plus" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +checksum = "d5ff29294ee99373e2cd5fd21786a3c0ced99a52fec2ca347d565489c61b723c" dependencies = [ - "cosmwasm-schema", "cosmwasm-std", - "cw2 1.0.1", "schemars", - "semver", "serde", - "thiserror", ] [[package]] -name = "cw1" +name = "cw-utils" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe0783ec4210ba4e0cdfed9874802f469c6db0880f742ad427cb950e940b21c" +checksum = "0ae0b69fa7679de78825b4edeeec045066aa2b2c4b6e063d80042e565bb4da5c" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw2 0.15.1", "schemars", + "semver", "serde", + "thiserror", ] [[package]] -name = "cw1-whitelist" -version = "0.15.1" +name = "cw-utils" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233dd13f61495f1336da57c8bdca0536fa9f8dd59c12d2bbfc59928ea580e478" +checksum = "1c4a657e5caacc3a0d00ee96ca8618745d050b8f757c709babafb81208d4239c" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw1", - "cw2 0.15.1", + "cw2 1.1.2", "schemars", + "semver", "serde", "thiserror", ] @@ -631,15 +566,17 @@ dependencies = [ [[package]] name = "cw2" -version = "1.0.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8" +checksum = "c6c120b24fbbf5c3bedebb97f2cc85fbfa1c3287e09223428e7e597b5293c1fa" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.0.1", + "cw-storage-plus 1.2.0", "schemars", + "semver", "serde", + "thiserror", ] [[package]] @@ -657,75 +594,27 @@ dependencies = [ [[package]] name = "cw20" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91666da6c7b40c8dd5ff94df655a28114efc10c79b70b4d06f13c31e37d60609" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-utils 1.0.1", - "schemars", - "serde", -] - -[[package]] -name = "cw20-base" -version = "0.15.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0909c56d0c14601fbdc69382189799482799dcad87587926aec1f3aa321abc41" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", + "cw-utils 1.0.3", "schemars", - "semver", "serde", - "thiserror", ] [[package]] name = "cw3" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe0b587008aa221cd2a2579a21990a28c4347dc53ad43167c68ad765f5b6efa" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-utils 1.0.1", - "cw20 1.0.1", - "schemars", - "serde", - "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" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62c3ee3b669fc2a8094301a73fd7be97a7454d4df2650c33599f737e8f254d24" +checksum = "2967fbd073d4b626dd9e7148e05a84a3bebd9794e71342e12351110ffbb12395" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.15.1", - "cw-utils 0.15.1", - "cw2 0.15.1", - "cw721", + "cw-utils 1.0.3", + "cw20 1.1.2", "schemars", "serde", "thiserror", @@ -733,9 +622,9 @@ dependencies = [ [[package]] name = "der" -version = "0.6.1" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "zeroize", @@ -768,32 +657,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "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.11" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "ecdsa" -version = "0.14.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", + "spki", ] [[package]] @@ -813,19 +699,18 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "elliptic-curve" -version = "0.12.3" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "der", "digest 0.10.7", "ff", "generic-array", @@ -838,81 +723,20 @@ dependencies = [ ] [[package]] -name = "errno" -version = "0.3.1" +name = "ff" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ - "errno-dragonfly", - "libc", - "windows-sys 0.48.0", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "forward_ref" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "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.2.0", - "astroport-pair", - "astroport-staking", - "astroport-tests", - "astroport-token", - "astroport-whitelist", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "itertools", - "proptest", - "thiserror", - "voting-escrow", -] +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "generic-array" @@ -922,13 +746,14 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -937,9 +762,9 @@ dependencies = [ [[package]] name = "group" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", "rand_core 0.6.4", @@ -955,12 +780,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" @@ -978,235 +797,220 @@ dependencies = [ [[package]] name = "ibc-controller-package" -version = "0.1.0" -source = "git+https://github.com/astroport-fi/astroport_ibc?branch=feat/update_deps_2023_05_22#5ce8b68dc89965790558523ab84c6b2f153c5181" +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?branch=feat/merge_hidden_2023_05_22)", - "astroport-ibc", + "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 = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +name = "ibc-controller-package" +version = "1.1.1" +source = "git+https://github.com/astroport-fi/astroport_ibc#0b7f69eeb853ad4dfc871a4d10f34cbd251517eb" dependencies = [ - "cfg-if", + "astroport-governance 1.2.0 (git+https://github.com/astroport-fi/astroport-governance)", + "astroport-ibc 1.3.0", + "cosmwasm-schema", + "cosmwasm-std", ] [[package]] -name = "integer-sqrt" -version = "0.1.5" +name = "itertools" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ - "num-traits", + "either", ] [[package]] -name = "io-lifetimes" -version = "1.0.10" +name = "itertools" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", + "either", ] [[package]] name = "itertools" -version = "0.10.5" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "k256" -version = "0.11.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sha2 0.10.6", + "once_cell", + "sha2 0.10.8", + "signature", ] [[package]] -name = "lazy_static" -version = "1.4.0" +name = "libc" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] -name = "libc" -version = "0.2.144" +name = "num-traits" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] [[package]] -name = "libm" -version = "0.2.7" +name = "once_cell" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "linux-raw-sys" -version = "0.3.8" +name = "opaque-debug" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] -name = "num-traits" -version = "0.2.15" +name = "osmosis-std" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "e87adf61f03306474ce79ab322d52dfff6b0bcf3aed1e12d8864ac0400dec1bf" dependencies = [ - "autocfg", - "libm", + "chrono", + "cosmwasm-std", + "osmosis-std-derive", + "prost 0.12.3", + "prost-types 0.12.3", + "schemars", + "serde", + "serde-cw-value", ] [[package]] -name = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "opaque-debug" -version = "0.3.0" +name = "osmosis-std-derive" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +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" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "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.58" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] -name = "proptest" -version = "1.1.0" +name = "prost" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29f1b898011ce9595050a68e60f90bad083ff2987a695a42357134c8381fba70" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ - "bit-set", - "bitflags", - "byteorder", - "lazy_static", - "num-traits", - "quick-error 2.0.1", - "rand", - "rand_chacha", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", + "bytes", + "prost-derive 0.11.9", ] [[package]] name = "prost" -version = "0.9.0" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", - "prost-derive", + "prost-derive 0.12.3", ] [[package]] name = "prost-derive" -version = "0.9.0" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] -name = "protobuf" -version = "2.28.0" +name = "prost-derive" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ - "bytes", + "anyhow", + "itertools 0.11.0", + "proc-macro2", + "quote", + "syn 2.0.57", ] [[package]] -name = "quick-error" -version = "1.2.3" +name = "prost-types" +version = "0.11.9" 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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" dependencies = [ - "proc-macro2", + "prost 0.11.9", ] [[package]] -name = "rand" -version = "0.8.5" +name = "prost-types" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ - "libc", - "rand_chacha", - "rand_core 0.6.4", + "prost 0.12.3", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "quote" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", + "proc-macro2", ] [[package]] @@ -1224,78 +1028,27 @@ 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.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "rfc6979" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "crypto-bigint", "hmac", - "zeroize", -] - -[[package]] -name = "rustix" -version = "0.37.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" -dependencies = [ - "bitflags", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.48.0", -] - -[[package]] -name = "rusty-fork" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" -dependencies = [ - "fnv", - "quick-error 1.2.3", - "tempfile", - "wait-timeout", + "subtle", ] [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[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", @@ -1305,9 +1058,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", @@ -1317,9 +1070,9 @@ dependencies = [ [[package]] name = "sec1" -version = "0.3.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", @@ -1331,19 +1084,28 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.17" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 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.2" @@ -1355,13 +1117,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.57", ] [[package]] @@ -1377,9 +1139,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1401,9 +1163,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -1412,40 +1174,19 @@ dependencies = [ [[package]] name = "signature" -version = "1.6.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest 0.10.7", "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" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -1476,9 +1217,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35" dependencies = [ "proc-macro2", "quote", @@ -1486,43 +1227,63 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.5.0" +name = "test-case" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +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", - "fastrand", - "redox_syscall", - "rustix", - "windows-sys 0.45.0", + "proc-macro2", + "quote", + "syn 2.0.57", +] + +[[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.57", + "test-case-core", ] [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.57", ] [[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" @@ -1536,17 +1297,11 @@ 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.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" @@ -1554,195 +1309,14 @@ 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.2.0", - "astroport-staking", - "astroport-token", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "cw-storage-plus 0.15.1", - "cw2 0.15.1", - "cw20 0.15.1", - "cw20-base", - "proptest", - "thiserror", -] - -[[package]] -name = "voting-escrow-delegation" -version = "1.0.0" -dependencies = [ - "anyhow", - "astroport-governance 1.2.0", - "astroport-nft", - "astroport-tests", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "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" -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.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", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -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", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" - [[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 19967cf3..f5a14ecd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,25 @@ [workspace] -members = ["packages/*", "contracts/*"] +resolver = "2" +members = [ + "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", +] + +[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" 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 00000000..4bc20181 Binary files /dev/null and b/assets/interchain-emissions-voting.png differ diff --git a/assets/interchain-governance-voting.png b/assets/interchain-governance-voting.png new file mode 100644 index 00000000..ba6b2bf9 Binary files /dev/null and b/assets/interchain-governance-voting.png differ diff --git a/assets/interchain-ibc-channels.png b/assets/interchain-ibc-channels.png new file mode 100644 index 00000000..92393773 Binary files /dev/null and b/assets/interchain-ibc-channels.png differ diff --git a/assets/interchain-stake-astro.png b/assets/interchain-stake-astro.png new file mode 100644 index 00000000..a3b59a93 Binary files /dev/null and b/assets/interchain-stake-astro.png differ diff --git a/assets/interchain-unlock.png b/assets/interchain-unlock.png new file mode 100644 index 00000000..bea573b9 Binary files /dev/null and b/assets/interchain-unlock.png differ diff --git a/assets/interchain-withdraw-funds.png b/assets/interchain-withdraw-funds.png new file mode 100644 index 00000000..b571b5c1 Binary files /dev/null and b/assets/interchain-withdraw-funds.png differ diff --git a/contracts/assembly/Cargo.toml b/contracts/assembly/Cargo.toml index 6ca81618..47e224d8 100644 --- a/contracts/assembly/Cargo.toml +++ b/contracts/assembly/Cargo.toml @@ -1,8 +1,10 @@ [package] name = "astro-assembly" -version = "1.5.0" +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" @@ -11,26 +13,26 @@ crate-type = ["cdylib", "rlib"] [features] backtraces = ["cosmwasm-std/backtraces"] -testnet = ["astroport-governance/testnet"] +testnet = [] 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" } -ibc-controller-package = { git = "https://github.com/astroport-fi/astroport_ibc", branch = "feat/update_deps_2023_05_22" } -thiserror = { version = "1.0" } -cosmwasm-schema = "1.1" +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/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] -cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-xastro-token = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -voting-escrow = { path = "../voting_escrow" } -voting-escrow-delegation = { path = "../voting_escrow_delegation" } -astroport-nft = { path = "../nft" } -astroport-staking = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -builder-unlock = { path = "../builder_unlock" } +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", 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/assembly/src/contract.rs b/contracts/assembly/src/contract.rs index 81cdf60d..f93ed619 100644 --- a/contracts/assembly/src/contract.rs +++ b/contracts/assembly/src/contract.rs @@ -1,44 +1,30 @@ -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, -}; -use cw2::{get_contract_version, set_contract_version}; -use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20ReceiveMsg}; -use cw_storage_plus::Bound; 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, +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, + QuerierWrapper, Response, StdError, SubMsg, Uint128, Uint64, WasmMsg, }; +use cw2::set_contract_version; +use cw_utils::must_pay; +use ibc_controller_package::ExecuteMsg as ControllerExecuteMsg; -use crate::astroport::asset::addr_opt_validate; -use astroport::xastro_token::QueryMsg as XAstroTokenQueryMsg; -use astroport_governance::builder_unlock::msg::{ - AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, StateResponse, +use astroport_governance::assembly::{ + helpers::validate_links, Config, ExecuteMsg, InstantiateMsg, Proposal, ProposalStatus, + ProposalVoteOption, UpdateConfig, }; -use astroport_governance::utils::WEEK; -use astroport_governance::voting_escrow::{QueryMsg as VotingEscrowQueryMsg, VotingPowerResponse}; -use astroport_governance::voting_escrow_delegation::QueryMsg::AdjustedBalance; +use astroport_governance::utils::check_contract_supports_channel; use crate::error::ContractError; -use crate::migration::{migrate_config_to_140, migrate_proposals_to_v140, MigrateMsg}; -use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT}; - -use ibc_controller_package::ExecuteMsg as ControllerExecuteMsg; +use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_VOTERS}; +use crate::utils::{calc_total_voting_power_at, calc_voting_power}; // Contract name and version used for migration. -const CONTRACT_NAME: &str = "astro-assembly"; -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; +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)] @@ -56,13 +42,18 @@ pub fn instantiate( validate_links(&msg.whitelisted_links)?; + 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_token_addr: deps.api.addr_validate(&msg.xastro_token_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, - )?, + xastro_denom: staking_config.xastro_denom, + xastro_denom_tracking: tracker_config.tracker_addr, ibc_controller: addr_opt_validate(deps.api, &msg.ibc_controller)?, builder_unlock_addr: deps.api.addr_validate(&msg.builder_unlock_addr)?, proposal_voting_period: msg.proposal_voting_period, @@ -74,6 +65,7 @@ pub fn instantiate( whitelisted_links: msg.whitelisted_links, }; + #[cfg(not(feature = "testnet"))] config.validate()?; CONFIG.save(deps.storage, &config)?; @@ -89,15 +81,23 @@ pub fn instantiate( /// * **ExecuteMsg::Receive(cw20_msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes /// it depending on the received template. /// +/// * **ExecuteMsg::SubmitProposal { title, description, link, messages, ibc_channel }** Submits a new proposal. +/// +/// * **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::CheckMessagesPassed {}** Closing message for the `CheckMessages` endpoint. +/// /// * **ExecuteMsg::CastVote { proposal_id, vote }** Cast a vote on a specific proposal. /// /// * **ExecuteMsg::EndProposal { proposal_id }** Sets the status of an expired/finalized proposal. /// /// * **ExecuteMsg::ExecuteProposal { proposal_id }** Executes a successful proposal. /// -/// * **ExecuteMsg::RemoveCompletedProposal { proposal_id }** Removes a finalized proposal from the proposal list. -/// /// * **ExecuteMsg::UpdateConfig(config)** Updates the contract configuration. +/// +/// * **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, @@ -106,34 +106,7 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { 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::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::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}), - ExecuteMsg::RemoveCompletedProposal { proposal_id } => { - remove_completed_proposal(deps, env, proposal_id) - } - ExecuteMsg::UpdateConfig(config) => update_config(deps, env, info, config), - ExecuteMsg::IBCProposalCompleted { - proposal_id, - status, - } => update_ibc_proposal_status(deps, info, proposal_id, status), - } -} - -/// 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 { + ExecuteMsg::SubmitProposal { title, description, link, @@ -143,18 +116,29 @@ pub fn receive_cw20( deps, env, info, - Addr::unchecked(cw20_msg.sender), - cw20_msg.amount, title, description, link, messages, ibc_channel, ), + 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(deps.api, env, messages), + ExecuteMsg::CheckMessagesPassed {} => Err(ContractError::MessagesCheckPassed {}), + ExecuteMsg::UpdateConfig(config) => update_config(deps, env, info, config), + ExecuteMsg::IBCProposalCompleted { + 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) + } } } -/// 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. /// @@ -172,33 +156,29 @@ pub fn submit_proposal( deps: DepsMut, env: Env, info: MessageInfo, - sender: Addr, - deposit_amount: Uint128, title: String, description: String, link: Option, - messages: Option>, + messages: Vec, ibc_channel: Option, ) -> 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 deposit_amount = must_pay(&info, &config.xastro_denom)?; if deposit_amount < config.proposal_required_deposit { return Err(ContractError::InsufficientDeposit {}); } // 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 { 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 {}); } @@ -206,12 +186,12 @@ 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(), against_power: Uint128::zero(), - for_voters: Vec::new(), - against_voters: Vec::new(), + outpost_against_power: Uint128::zero(), start_block: env.block.height, start_time: env.block.time.seconds(), end_block: env.block.height + config.proposal_voting_period, @@ -228,6 +208,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)?; @@ -236,7 +223,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", @@ -263,16 +250,11 @@ pub fn cast_vote( return Err(ContractError::ProposalNotActive {}); } - if proposal.submitter == info.sender { - return Err(ContractError::Unauthorized {}); - } - if env.block.height > proposal.end_block { return Err(ContractError::VotingPeriodEnded {}); } - if proposal.for_voters.contains(&info.sender) || proposal.against_voters.contains(&info.sender) - { + if PROPOSAL_VOTERS.has(deps.storage, (proposal_id, info.sender.to_string())) { return Err(ContractError::UserAlreadyVoted {}); } @@ -285,13 +267,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.clone()); } ProposalVoteOption::Against => { proposal.against_power = proposal.against_power.checked_add(voting_power)?; - proposal.against_voters.push(info.sender.clone()); } }; + PROPOSAL_VOTERS.save( + deps.storage, + (proposal_id, info.sender.to_string()), + &vote_option, + )?; PROPOSALS.save(deps.storage, proposal_id, &proposal)?; @@ -304,7 +289,8 @@ pub fn cast_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)?; @@ -322,18 +308,10 @@ pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result= config.proposal_required_quorum @@ -347,19 +325,15 @@ pub fn end_proposal(deps: DepsMut, env: Env, proposal_id: u64) -> Result proposal.expiration_block { - return Err(ContractError::ExecuteProposalExpired {}); - } - - let messages; - if let Some(channel) = &proposal.ibc_channel { - let config = CONFIG.load(deps.storage)?; - - messages = match &proposal.messages { - Some(messages) => { - 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)?; + 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; - PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - - messages = proposal.messages.unwrap_or_default() + response + .messages + .extend(proposal.messages.iter().cloned().map(SubMsg::new)) } - Ok(Response::new() - .add_attribute("action", "execute_proposal") - .add_attribute("proposal_id", proposal_id.to_string()) - .add_messages(messages)) -} - -/// 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_binary(&ExecuteMsg::CheckMessagesPassed {})?, - funds: vec![], - })); + PROPOSALS.save(deps.storage, proposal_id, &proposal)?; - Ok(Response::new() - .add_attribute("action", "check_messages") - .add_messages(messages)) + Ok(response.add_attribute("proposal_status", proposal.status.to_string())) } -/// Removes an expired or rejected proposal from the general proposal list. -pub fn remove_completed_proposal( - deps: DepsMut, +/// Checks that proposal messages are correct. +pub fn check_messages( + api: &dyn Api, env: Env, - proposal_id: u64, + mut messages: Vec, ) -> 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 {}); - } + messages.iter().try_for_each(|msg| match msg { + 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 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(()), + })?; - PROPOSALS.remove(deps.storage, proposal_id); + messages.push( + wasm_execute( + env.contract.address, + &ExecuteMsg::CheckMessagesPassed {}, + vec![], + )? + .into(), + ); Ok(Response::new() - .add_attribute("action", "remove_completed_proposal") - .add_attribute("proposal_id", proposal_id.to_string())) + .add_attribute("action", "check_messages") + .add_messages(messages)) } /// Updates Assembly contract parameters. @@ -484,62 +442,77 @@ pub fn update_config( return Err(ContractError::Unauthorized {}); } - if let Some(xastro_token_addr) = updated_config.xastro_token_addr { - config.xastro_token_addr = deps.api.addr_validate(&xastro_token_addr)?; - } - - 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.as_str())?, - ) - } + 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 { @@ -547,16 +520,22 @@ 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 {}); } } + #[cfg(not(feature = "testnet"))] config.validate()?; 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 @@ -596,285 +575,23 @@ fn update_ibc_proposal_status( } } -/// 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_binary(&CONFIG.load(deps.storage)?), - QueryMsg::Proposals { start, limit } => to_binary(&query_proposals(deps, start, limit)?), - QueryMsg::Proposal { proposal_id } => { - to_binary(&PROPOSALS.load(deps.storage, proposal_id)?) - } - QueryMsg::ProposalVotes { proposal_id } => { - to_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)?) - } - QueryMsg::TotalVotingPower { proposal_id } => { - let proposal = PROPOSALS.load(deps.storage, proposal_id)?; - to_binary(&calc_total_voting_power_at(deps, &proposal)?) - } - QueryMsg::ProposalVoters { - proposal_id, - vote_option, - start, - limit, - } => to_binary(&query_proposal_voters( - deps, - proposal_id, - vote_option, - start, - 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 proposal's voters. -pub fn query_proposal_voters( - deps: Deps, - proposal_id: u64, - vote_option: ProposalVoteOption, - start: 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)?; - - 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()) -} - -/// 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, - }, - )?; - - let mut total = xastro_amount.balance; - - 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 { - 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::UserDepositAtHeight { - user: sender, - height: proposal.start_block, - }, - )?; - - 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, - }, - )?; - - // 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 - 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) -} - -/// 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( +pub fn exec_from_multisig( 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 { - 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_140(deps.branch(), msg)?; - migrate_proposals_to_v140(deps.branch(), &cfg)?; - } - _ => return Err(ContractError::MigrationError {}), - }, - _ => return Err(ContractError::MigrationError {}), - }; - - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + info: MessageInfo, + env: Env, + messages: Vec, +) -> Result { + match querier + .query_wasm_contract_info(&env.contract.address)? + .admin + { + None => 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(()), + }?; - 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)) + Ok(Response::new().add_messages(messages)) } diff --git a/contracts/assembly/src/error.rs b/contracts/assembly/src/error.rs index a89cdf17..d794ac6e 100644 --- a/contracts/assembly/src/error.rs +++ b/contracts/assembly/src/error.rs @@ -1,13 +1,22 @@ -use astroport_governance::assembly::ProposalStatus; use cosmwasm_std::{OverflowError, StdError}; +use cw2::VersionError; +use cw_utils::PaymentError; use thiserror::Error; +use astroport_governance::assembly::ProposalStatus; + /// This enum describes Assembly contract errors -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("{0}")] + VersionError(#[from] VersionError), + #[error("Unauthorized")] Unauthorized {}, @@ -26,33 +35,21 @@ pub enum ContractError { #[error("Voting period not ended yet!")] VotingPeriodNotEnded {}, - #[error("Proposal expired!")] - ExecuteProposalExpired {}, - #[error("Insufficient token deposit!")] InsufficientDeposit {}, #[error("Proposal not passed!")] ProposalNotPassed {}, - #[error("Proposal not completed!")] - ProposalNotCompleted {}, - #[error("Proposal delay not ended!")] ProposalDelayNotEnded {}, - #[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 {}, @@ -67,10 +64,7 @@ pub enum ContractError { #[error("Sender is not an IBC controller installed in the assembly")] InvalidIBCController {}, -} -impl From for ContractError { - fn from(o: OverflowError) -> Self { - StdError::from(o).into() - } + #[error("{0}")] + PaymentError(#[from] PaymentError), } 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 fe535ffc..6163c541 100644 --- a/contracts/assembly/src/lib.rs +++ b/contracts/assembly/src/lib.rs @@ -2,8 +2,12 @@ pub mod contract; pub mod error; pub mod state; -mod migration; +/// 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; -// 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 migration; +#[cfg(test)] +mod unit_tests; diff --git a/contracts/assembly/src/migration.rs b/contracts/assembly/src/migration.rs index 15cc4a18..883c3456 100644 --- a/contracts/assembly/src/migration.rs +++ b/contracts/assembly/src/migration.rs @@ -1,151 +1,62 @@ -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, -} - -#[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.4.0 -pub(crate) fn migrate_proposals_to_v140(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, - against_power: proposal.against_power, - for_voters: proposal.for_voters, - against_voters: proposal.against_voters, - 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.4.0 -pub(crate) fn migrate_config_to_140(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, - 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, +use crate::contract::{instantiate, CONTRACT_NAME, CONTRACT_VERSION}; +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") + })?, }; - CONFIG.save(deps.storage, &cfg)?; - - Ok(cfg) + 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 contract_version = cw2::get_contract_version(deps.storage)?; + + instantiate(deps, env, info, msg).map(|resp| { + resp.add_message(close_msg).add_attributes([ + ("previous_contract_name", contract_version.contract.as_str()), + ( + "previous_contract_version", + contract_version.version.as_str(), + ), + ("new_contract_name", CONTRACT_NAME), + ("new_contract_version", CONTRACT_VERSION), + ]) + }) } diff --git a/contracts/assembly/src/queries.rs b/contracts/assembly/src/queries.rs new file mode 100644 index 00000000..d099e1ea --- /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_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(&proposal.total_voting_power) + } + 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/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/contracts/assembly/src/unit_tests.rs b/contracts/assembly/src/unit_tests.rs new file mode 100644 index 00000000..85363f5f --- /dev/null +++ b/contracts/assembly/src/unit_tests.rs @@ -0,0 +1,482 @@ +use std::marker::PhantomData; +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, 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, 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::{execute, execute_proposal, submit_proposal}; +use crate::error::ContractError; +use crate::queries::query; +use crate::state::{CONFIG, PROPOSALS, PROPOSAL_COUNT}; + +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::QueryMsg::State { .. }) + ) { + SystemResult::Ok(ContractResult::Ok( + to_json_binary(&astroport_governance::builder_unlock::State { + total_astro_deposited: Default::default(), + remaining_astro_tokens: Default::default(), + unallocated_astro_tokens: Default::default(), + }) + .unwrap(), + )) + } else { + unimplemented!() + } + } + _ => 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(), + 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")] +#[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 these lines + let _ = coins(0, "keep_it"); + let _ = coin(0, "keep_it"); + + let mut deps = mock_deps(); + 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(), + ibc_controller: 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()], + }; + 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, + total_voting_power: Default::default(), + } + ); + } +} + +#[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(), + ibc_controller: 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()], + }; + 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(), + ibc_controller: 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()], + }; + 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(), + ibc_controller: 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()], + }; + 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/src/utils.rs b/contracts/assembly/src/utils.rs new file mode 100644 index 00000000..3ed2c997 --- /dev/null +++ b/contracts/assembly/src/utils.rs @@ -0,0 +1,73 @@ +use astroport::tokenfactory_tracker; +use cosmwasm_std::{Deps, QuerierWrapper, StdResult, Uint128}; + +use astroport_governance::assembly::Config; +use astroport_governance::assembly::Proposal; +use astroport_governance::builder_unlock::{ + AllocationResponse, QueryMsg as BuilderUnlockQueryMsg, State, +}; + +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 mut total: Uint128 = deps.querier.query_wasm_smart( + &config.xastro_denom_tracking, + &tokenfactory_tracker::QueryMsg::BalanceAt { + address: sender.clone(), + // Get voting power at the block before the proposal starts + timestamp: Some(proposal.start_time - 1), + }, + )?; + + let locked_amount: AllocationResponse = deps.querier.query_wasm_smart( + config.builder_unlock_addr, + &BuilderUnlockQueryMsg::Allocation { + account: sender, + timestamp: Some(proposal.start_time - 1), + }, + )?; + + total += locked_amount.status.amount - locked_amount.status.astro_withdrawn; + + Ok(total) +} + +/// 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 +/// +/// ## 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 { + timestamp: Some(timestamp), + }, + )?; + + // Total amount of ASTRO locked in the initial builder's unlock schedule + let builder_state: State = querier.query_wasm_smart( + &config.builder_unlock_addr, + &BuilderUnlockQueryMsg::State { + timestamp: Some(timestamp), + }, + )?; + + total += builder_state.remaining_astro_tokens; + + Ok(total) +} diff --git a/contracts/assembly/tests/assembly_integration.rs b/contracts/assembly/tests/assembly_integration.rs new file mode 100644 index 00000000..324638ca --- /dev/null +++ b/contracts/assembly/tests/assembly_integration.rs @@ -0,0 +1,963 @@ +use std::collections::HashMap; +use std::str::FromStr; + +use cosmwasm_std::{ + coin, coins, wasm_execute, Addr, BankMsg, CosmosMsg, Decimal, Empty, Uint128, WasmMsg, +}; +use cw_multi_test::Executor; + +use astro_assembly::error::ContractError; +use astroport_governance::assembly::{ + Config, ExecuteMsg, InstantiateMsg, ProposalListResponse, ProposalStatus, ProposalVoteOption, + ProposalVoterResponse, QueryMsg, UpdateConfig, DELAY_INTERVAL, DEPOSIT_INTERVAL, + EXPIRATION_PERIOD_INTERVAL, VOTING_PERIOD_INTERVAL, +}; + +use crate::common::helper::{ + default_init_msg, noop_contract, Helper, PROPOSAL_DELAY, PROPOSAL_EXPIRATION, + PROPOSAL_REQUIRED_DEPOSIT, PROPOSAL_VOTING_PERIOD, +}; + +mod common; + +#[test] +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(), + 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 + .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(), + 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 + .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( + 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_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); + + helper.submit_sample_proposal(&user); + + // 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_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 + ); + + // 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!( + 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() + ); + + // 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.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 + 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 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" + ); + + // 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] +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 { + ibc_controller: 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, + })), + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::Unauthorized {} + ); + + let updated_config = UpdateConfig { + ibc_controller: Some("ibc_controller".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()]), + }; + + 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.ibc_controller, + Some(Addr::unchecked("ibc_controller")) + ); + 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()] + ); +} + +#[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) + .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); + + 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_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); +} + +#[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 {} + ); +} + +#[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 new file mode 100644 index 00000000..89b1b873 --- /dev/null +++ b/contracts/assembly/tests/common/helper.rs @@ -0,0 +1,458 @@ +#![allow(dead_code)] + +use anyhow::Result as AnyResult; +use astroport::staking; +use cosmwasm_std::testing::MockApi; +use cosmwasm_std::{ + 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, + Executor, FailingModule, StakeKeeper, WasmKeeper, TOKEN_FACTORY_MODULE, +}; + +use astroport_governance::assembly::{ + 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::{CreateAllocationParams, Schedule}; + +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::query::query, + )) +} + +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(); +pub const PROPOSAL_EXPIRATION: u64 = *EXPIRATION_PERIOD_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: 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( + MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE, + 2, + ) + .unwrap() + .to_string(), + whitelisted_links: vec!["https://some.link/".to_string()], + } +} + +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 assembly_code_id: u64, +} + +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 = 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::InstantiateMsg { + owner: owner.to_string(), + astro_denom: 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 assembly = app + .instantiate_contract( + assembly_code_id, + owner.clone(), + &default_init_msg(&staking, &builder_unlock), + &[], + String::from("Astroport Assembly"), + 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(), + staking, + assembly, + builder_unlock, + xastro_denom, + assembly_code_id, + }) + } + + 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 get_xastro(&mut self, recipient: &Addr, amount: impl Into + Copy) -> AppResponse { + self.give_astro(amount.into(), recipient); + 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::ExecuteMsg::CreateAllocations { + allocations: vec![( + recipient.to_string(), + CreateAllocationParams { + amount: amount.into(), + unlock_schedule: Schedule { + duration: 10, + ..Default::default() + }, + }, + )], + }, + &coins(amount, ASTRO_DENOM), + ) + .unwrap(); + } + + pub fn submit_proposal(&mut self, submitter: &Addr, messages: Vec) { + self.app + .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 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"), + 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 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)); + 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 create_allocations(&mut self, allocations: Vec<(String, CreateAllocationParams)>) { + 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::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/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..f15f01b2 --- /dev/null +++ b/contracts/assembly/tests/common/stargate.rs @@ -0,0 +1,125 @@ +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()) + } + "/cosmos.authz.v1beta1.MsgGrant" => Ok(AppResponse::default()), + _ => 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 deleted file mode 100644 index 7e05f0ca..00000000 --- a/contracts/assembly/tests/integration.rs +++ /dev/null @@ -1,2037 +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 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, - 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); - - 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, - 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, - 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); - - // 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.clone() - }, - ), - ]; - - 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, - 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)); -} - -#[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); - - // 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, - 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); -} - -#[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); - - // 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); - - // 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); - - 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.clone(), - 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); - 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); - 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, -) -> (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, - )); - } - - let assembly_addr = instantiate_assembly_contract( - router, - &owner, - &xastro_token_addr, - &vxastro_token_addr, - &builder_unlock_addr, - delegator_addr.clone().map(String::from), - ); - - ( - 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() -} - -fn instantiate_assembly_contract( - router: &mut App, - owner: &Addr, - xastro: &Addr, - vxastro: &Addr, - builder: &Addr, - delegator: 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, - 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.clone(), - &recipient.clone(), - 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.clone(), 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 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/builder_unlock/Cargo.toml b/contracts/builder_unlock/Cargo.toml index 05192aa2..d8e51fa3 100644 --- a/contracts/builder_unlock/Cargo.toml +++ b/contracts/builder_unlock/Cargo.toml @@ -1,8 +1,10 @@ [package] name = "builder-unlock" -version = "1.2.3" +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" @@ -10,20 +12,18 @@ homepage = "https://astroport.fi" crate-type = ["cdylib", "rlib"] [features] -backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all init/handle/query exports library = [] [dependencies] -cw2 = "0.15" -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/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -thiserror = { version = "1.0" } -cosmwasm-schema = "1.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/astroport-core", version = "4", branch = "feat/astroport_v4" } [dev-dependencies] -cw-multi-test = "0.15" -astroport-token = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } +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 206f1e51..01dc9d7c 100644 --- a/contracts/builder_unlock/src/contract.rs +++ b/contracts/builder_unlock/src/contract.rs @@ -1,59 +1,54 @@ -use crate::astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +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_binary, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, - StdError, StdResult, Uint128, WasmMsg, -}; -use cw2::{get_contract_version, 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, + attr, coins, ensure, BankMsg, DepsMut, Env, MessageInfo, Response, StdError, Uint128, }; -use astroport_governance::builder_unlock::{AllocationParams, AllocationStatus, Config, Schedule}; +use cw2::set_contract_version; +use cw_utils::{may_pay, must_pay}; -use astroport_governance::{DEFAULT_LIMIT, MAX_LIMIT}; +use astroport_governance::builder_unlock::{Config, CreateAllocationParams, Schedule}; +use astroport_governance::builder_unlock::{ExecuteMsg, InstantiateMsg}; -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 = "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. #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, _info: MessageInfo, msg: InstantiateMsg, -) -> StdResult { - set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - - STATE.save(deps.storage, &Default::default())?; +) -> Result { + 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(), env.block.time.seconds())?; + 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. /// @@ -79,38 +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::Receive(cw20_msg) => execute_receive_cw20(deps, info, cw20_msg), + ExecuteMsg::CreateAllocations { 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", - )); - } - execute_increase_allocation(deps, &config, receiver, amount, None) + 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, @@ -120,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, @@ -145,69 +149,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_binary(&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 -/// * **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_binary(&CONFIG.load(deps.storage)?), - QueryMsg::State {} => to_binary(&query_state(deps)?), - QueryMsg::Allocation { account } => to_binary(&query_allocation(deps, account)?), - QueryMsg::UnlockedTokens { account } => { - to_binary(&query_tokens_unlocked(deps, env, account)?) - } - QueryMsg::SimulateWithdraw { account, timestamp } => { - to_binary(&query_simulate_withdraw(deps, env, account, timestamp)?) - } - QueryMsg::Allocations { start_after, limit } => { - to_binary(&query_allocations(deps, start_after, limit)?) - } - } -} - /// Admin function facilitating the creation of new allocations. /// /// * **creator** allocations creator (the contract admin). @@ -217,133 +158,97 @@ 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, - creator: String, - deposit_token: Addr, - deposit_amount: Uint128, - allocations: Vec<(String, AllocationParams)>, -) -> StdResult { + env: Env, + info: MessageInfo, + allocations: Vec<(String, CreateAllocationParams)>, +) -> Result { 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)?; + let expected_deposit: Uint128 = allocations.iter().map(|(_, params)| params.amount).sum(); + ensure!( + deposit_amount == expected_deposit, + ContractError::DepositAmountMismatch { + expected: expected_deposit, + got: deposit_amount, + } + ); - if deposit_amount - != allocations - .iter() - .map(|params| params.1.amount) - .sum::() - { - 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; - 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 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() { - 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)?; - status.astro_withdrawn += astro_to_withdraw; + let mut state = STATE.load(deps.storage)?; state.remaining_astro_tokens -= astro_to_withdraw; - // SAVE :: state & allocation - STATE.save(deps.storage, &state)?; + STATE.save(deps.storage, &state, block_ts)?; - // Update status - STATUS.save(deps.storage, &info.sender, &status)?; + let bank_msg = BankMsg::Send { + to_address: info.sender.to_string(), + amount: coins( + astro_to_withdraw.u128(), + CONFIG.load(deps.storage)?.astro_denom, + ), + }; Ok(Response::new() - .add_message(WasmMsg::Execute { - contract_addr: config.astro_token.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: info.sender.to_string(), - amount: astro_to_withdraw, - })?, - funds: vec![], - }) + .add_message(bank_msg) .add_attribute("astro_withdrawn", astro_to_withdraw)) } /// Allows the current allocation receiver to propose a new receiver. /// /// * **new_receiver** new proposed receiver for the allocation. -fn execute_propose_new_receiver( +pub fn execute_propose_new_receiver( deps: DepsMut, + env: Env, info: MessageInfo, new_receiver: String, -) -> 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") @@ -351,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. @@ -372,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"), @@ -424,55 +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: Option, -) -> StdResult { + deposit_amount: Uint128, +) -> 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)?; - - 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)?; - - 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") @@ -485,122 +397,61 @@ 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()); - let msg = WasmMsg::Execute { - contract_addr: config.astro_token.to_string(), - msg: to_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)?; + STATE.save(deps.storage, &state, env.block.time.seconds())?; Ok(Response::new() .add_attribute("action", "execute_transfer_unallocated") .add_attribute("amount", amount) - .add_message(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), - ])) + .add_message(bank_msg)) } /// 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; @@ -612,217 +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, - )) -} - -/// 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 -//---------------------------------------------------------------------------------------- - -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 - } - // 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); - - 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 f15a6c42..326d4720 100644 --- a/contracts/builder_unlock/src/lib.rs +++ b/contracts/builder_unlock/src/lib.rs @@ -1,7 +1,4 @@ pub mod contract; -mod migration; +pub mod error; +pub mod query; 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/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 5e85e603..9457e459 100644 --- a/contracts/builder_unlock/src/state.rs +++ b/contracts/builder_unlock/src/state.rs @@ -1,16 +1,251 @@ -use crate::astroport::common::OwnershipProposal; -use cosmwasm_std::Addr; -use cw_storage_plus::{Item, Map}; +use astroport::common::OwnershipProposal; +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/integration.rs b/contracts/builder_unlock/tests/builder_unlock_integration.rs similarity index 64% rename from contracts/builder_unlock/tests/integration.rs rename to contracts/builder_unlock/tests/builder_unlock_integration.rs index 7e949907..63c2ef53 100644 --- a/contracts/builder_unlock/tests/integration.rs +++ b/contracts/builder_unlock/tests/builder_unlock_integration.rs @@ -1,65 +1,49 @@ -use astroport::token::InstantiateMsg as TokenInstantiateMsg; -use astroport_governance::builder_unlock::{AllocationParams, Schedule}; +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 cosmwasm_std::{coin, coins, Addr, Decimal, StdResult, Timestamp, Uint128}; use cw_multi_test::{App, BasicApp, ContractWrapper, Executor}; +use cw_utils::PaymentError; -const OWNER: &str = "owner"; +use astroport_governance::builder_unlock::{ + AllocationParams, AllocationResponse, Config, ExecuteMsg, InstantiateMsg, QueryMsg, + SimulateWithdrawResponse, +}; +use astroport_governance::builder_unlock::{CreateAllocationParams, Schedule, State}; +use builder_unlock::error::ContractError; -fn mock_app() -> App { - BasicApp::default() -} +pub const ASTRO_DENOM: &str = "factory/assembly/ASTRO"; -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, - )); +const OWNER: &str = "owner"; - 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, - }; +fn mock_app() -> App { + 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, builder_unlock::contract::instantiate, - builder_unlock::contract::query, + builder_unlock::query::query, )); 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), }; @@ -67,7 +51,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", @@ -75,30 +59,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) { @@ -108,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) { @@ -130,21 +101,21 @@ 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 + let resp: Config = app .wrap() .query_wasm_smart(&unlock_instance, &QueryMsg::Config {}) .unwrap(); // 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 + 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); @@ -154,7 +125,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 @@ -189,89 +160,70 @@ fn test_transfer_ownership() { ) .unwrap(); - let resp: ConfigResponse = app + let resp: Config = app .wrap() .query_wasm_smart(&unlock_instance, &QueryMsg::Config {}) .unwrap(); // 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![]; + 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, cliff: 0u64, 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, cliff: 7776000u64, 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, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, - proposed_receiver: None, }, )); // ###### 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_binary(&ReceiveMsg::CreateAllocations { - allocations: allocations.clone(), - }) - .unwrap(), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), }, - &[], + &coins(1_000, ASTRO_DENOM), ) .unwrap_err(); assert_eq!( @@ -280,110 +232,57 @@ 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_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" + err.downcast::().unwrap(), + ContractError::PaymentError(PaymentError::MissingDenom(ASTRO_DENOM.to_string())) ); // ###### 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_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!( - 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 ###### 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_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(); // 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, @@ -401,17 +300,19 @@ 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, Schedule { start_time: 1642402274u64, cliff: 0u64, - duration: 31536000u64 + duration: 31536000u64, + percent_at_cliff: None, } ); @@ -422,17 +323,19 @@ 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, Schedule { start_time: 1642402274u64, cliff: 7776000u64, - duration: 31536000u64 + duration: 31536000u64, + percent_at_cliff: None, } ); @@ -443,151 +346,146 @@ 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, Schedule { start_time: 1642402274u64, cliff: 7776000u64, - duration: 31536000u64 + duration: 31536000u64, + percent_at_cliff: None, } ); // ###### 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_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!( - err.root_cause().to_string(), - "Generic error: Allocation (params) already exists for investor_1" + err.downcast::().unwrap(), + ContractError::AllocationExists { + user: "investor_1".to_string() + } ); } #[test] fn test_withdraw() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); + let (unlock_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 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, cliff: 0u64, 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, cliff: 7776000u64, 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, cliff: 7776000u64, 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.clone()), - astro_instance.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: unlock_instance.clone().to_string(), - amount: Uint128::from(15_000_000_000000u64), - msg: to_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 {}, &[], ) .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); - let astro_bal_before: BalanceResponse = app - .wrap() - .query_wasm_smart( - &astro_instance, - &cw20::Cw20QueryMsg::Balance { - address: "investor_1".to_string(), - }, - ) - .unwrap(); + // ###### SUCCESSFULLY WITHDRAWS ASTRO #1 ###### + 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 {}, &[], ) .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, @@ -598,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() @@ -605,29 +505,22 @@ 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: 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 ); // Check the number of unlocked tokens - let mut unlock_resp: Uint128 = app + let unlock_resp: Uint128 = app .wrap() .query_wasm_smart( &unlock_instance, @@ -636,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".clone()), - 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| { @@ -659,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, @@ -688,37 +567,48 @@ fn test_withdraw() { ); app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::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 ###### let err = app .execute_contract( - Addr::unchecked("investor_1".clone()), + 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" + err.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} ); // ###### SUCCESSFULLY WITHDRAWS ASTRO #3 ###### @@ -729,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, @@ -763,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, @@ -792,7 +682,7 @@ fn test_withdraw() { ); app.execute_contract( - Addr::unchecked("team_1".clone()), + Addr::unchecked("team_1"), unlock_instance.clone(), &ExecuteMsg::Withdraw {}, &[], @@ -805,6 +695,7 @@ fn test_withdraw() { &unlock_instance, &QueryMsg::Allocation { account: "team_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -831,74 +722,61 @@ 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![]; + 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, cliff: 0u64, 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, cliff: 7776000u64, 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, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, - proposed_receiver: None, }, )); // 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_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(), @@ -907,14 +785,16 @@ 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 ###### 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(), @@ -923,13 +803,13 @@ 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 ###### app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::ProposeNewReceiver { new_receiver: "investor_1_new".to_string(), @@ -944,6 +824,7 @@ fn test_propose_new_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -955,7 +836,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(), @@ -964,110 +845,101 @@ 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") + } ); } #[test] fn test_drop_new_receiver() { let mut app = mock_app(); - let (unlock_instance, astro_instance, _) = init_contracts(&mut app); + let (unlock_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 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, cliff: 0u64, 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, cliff: 7776000u64, 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, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, - proposed_receiver: None, }, )); // 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_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 {}, &[], ) .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" ###### let err = app .execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::DropNewReceiver {}, &[], ) .unwrap_err(); assert_eq!( - err.root_cause().to_string(), - "Generic error: Proposed receiver not set" + err.downcast::().unwrap(), + ContractError::ProposedReceiverNotSet {} ); // ###### 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(), @@ -1082,6 +954,7 @@ fn test_drop_new_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -1091,7 +964,7 @@ fn test_drop_new_receiver() { ); app.execute_contract( - Addr::unchecked("investor_1".clone()), + Addr::unchecked("investor_1"), unlock_instance.clone(), &ExecuteMsg::DropNewReceiver {}, &[], @@ -1104,6 +977,7 @@ fn test_drop_new_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -1113,88 +987,77 @@ 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![]; + 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, cliff: 0u64, 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, cliff: 7776000u64, 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, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, - proposed_receiver: None, }, )); // 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_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 {}, &[], ) .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" ###### 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(), @@ -1203,14 +1066,14 @@ 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 ###### // 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(), @@ -1225,6 +1088,7 @@ fn test_claim_receiver() { &unlock_instance, &QueryMsg::Allocation { account: "investor_1".to_string(), + timestamp: None, }, ) .unwrap(); @@ -1243,7 +1107,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(), @@ -1259,22 +1123,22 @@ 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, duration: 0u64, + percent_at_cliff: None, }, proposed_receiver: None, }, 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 @@ -1283,16 +1147,17 @@ 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, duration: alloc_resp_before.params.unlock_schedule.duration, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1336,42 +1201,29 @@ 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![( + 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, cliff: 300u64, duration: 1_534_700u64, + percent_at_cliff: None, }, - proposed_receiver: None, }, )]; 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_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(); @@ -1414,7 +1266,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(), @@ -1424,12 +1276,14 @@ 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( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::DecreaseAllocation { receiver: "investor".to_string(), @@ -1446,24 +1300,27 @@ 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), } ); // Try to increase let err = app .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::IncreaseAllocation { receiver: "investor".to_string(), @@ -1473,10 +1330,12 @@ 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; + // Transfer unallocated tokens to owner app.execute_contract( Addr::unchecked("owner".to_string()), @@ -1489,31 +1348,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_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(); @@ -1526,16 +1372,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( @@ -1557,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(), } ); } @@ -1574,74 +1415,61 @@ 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); + let (unlock_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 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, cliff: 0u64, 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, cliff: 7776000u64, 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, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, - proposed_receiver: None, }, )); // ###### 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_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(); // 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, @@ -1663,6 +1491,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 0u64, duration: 31536000u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1678,6 +1507,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1693,6 +1523,7 @@ fn test_updates_schedules() { start_time: 1642402274u64, cliff: 7776000u64, duration: 31536000u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1700,7 +1531,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![( @@ -1709,6 +1540,7 @@ fn test_updates_schedules() { start_time: 123u64, cliff: 123u64, duration: 123u64, + percent_at_cliff: None, }, )], }, @@ -1716,13 +1548,13 @@ 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 .execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::UpdateUnlockSchedules { new_unlock_schedules: vec![ @@ -1732,6 +1564,7 @@ fn test_updates_schedules() { start_time: 123u64, cliff: 123u64, duration: 123u64, + percent_at_cliff: None, }, ), ( @@ -1740,6 +1573,7 @@ fn test_updates_schedules() { start_time: 123u64, cliff: 123u64, duration: 123u64, + percent_at_cliff: None, }, ), ], @@ -1753,7 +1587,7 @@ fn test_updates_schedules() { ); app.execute_contract( - Addr::unchecked(OWNER.clone()), + Addr::unchecked(OWNER), unlock_instance.clone(), &ExecuteMsg::UpdateUnlockSchedules { new_unlock_schedules: vec![ @@ -1763,6 +1597,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, ), ( @@ -1771,6 +1606,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, ), ], @@ -1790,6 +1626,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1805,6 +1642,7 @@ fn test_updates_schedules() { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, ) .unwrap(); @@ -1825,11 +1663,11 @@ fn test_updates_schedules() { ( Addr::unchecked("advisor_1"), AllocationParams { - amount: Uint128::new(5000000000000), unlock_schedule: Schedule { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1837,11 +1675,11 @@ fn test_updates_schedules() { ( Addr::unchecked("investor_1"), AllocationParams { - amount: Uint128::new(5000000000000), unlock_schedule: Schedule { start_time: 1642402274, cliff: 0, duration: 31536000, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1849,11 +1687,11 @@ fn test_updates_schedules() { ( Addr::unchecked("team_1"), AllocationParams { - amount: Uint128::new(5000000000000), unlock_schedule: Schedule { start_time: 1642402284u64, cliff: 8776000u64, duration: 31536001u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1875,11 +1713,11 @@ 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, duration: 31536001u64, + percent_at_cliff: None, }, proposed_receiver: None, }, @@ -1897,11 +1735,274 @@ 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); Ok(()) } + +fn query_bal(app: &mut App, address: &Addr) -> u128 { + app.wrap() + .query_balance(address, ASTRO_DENOM) + .unwrap() + .amount + .u128() +} + +#[test] +fn test_create_allocations_with_custom_cliff() { + let mut app = mock_app(); + let (unlock_instance, _) = init_contracts(&mut app); + let total_astro = Uint128::new(1_000_000_000000); + + 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(), + CreateAllocationParams { + 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, + }, + }, + )); + allocations.push(( + investor2.to_string(), + CreateAllocationParams { + 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 + }, + }, + )); + allocations.push(( + investor3.to_string(), + CreateAllocationParams { + 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 + }, + }, + )); + + // Create allocations + app.execute_contract( + Addr::unchecked(OWNER), + unlock_instance.clone(), + &ExecuteMsg::CreateAllocations { + allocations: allocations.clone(), + }, + &coins(total_astro.u128(), ASTRO_DENOM), + ) + .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.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} + ); + + // Investor2 needs to wait 5 months more + let err = app + .execute_contract( + investor2.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} + ); + + // 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_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); + + // 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.downcast::().unwrap(), + ContractError::NoUnlockedAstro {} + ); + + // Investor2 receives his one sixth of the allocation + app.execute_contract( + investor2.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_bal(&mut app, &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_bal(&mut app, &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_bal(&mut app, &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_bal(&mut app, &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_bal(&mut app, &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_bal(&mut app, &investor1); + assert_eq!(balance, 500000_000000); + + // Investor2 receives whole allocation + app.execute_contract( + investor2.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_bal(&mut app, &investor2); + assert_eq!(balance, 100000_000000); + + // Investor3 receives whole allocation + app.execute_contract( + investor3.clone(), + unlock_instance.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + .unwrap(); + let balance = query_bal(&mut app, &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.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/contracts/escrow_fee_distributor/Cargo.toml b/contracts/escrow_fee_distributor/Cargo.toml index b302f37d..e021f122 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/astroport-core", branch = "feat/merge_hidden_2023_05_22" } +astroport-token = { git = "https://github.com/astroport-fi/astroport-core" } astroport-tests = { path = "../../packages/astroport-tests" } diff --git a/contracts/escrow_fee_distributor/src/contract.rs b/contracts/escrow_fee_distributor/src/contract.rs index cfd2d9fb..6163f650 100644 --- a/contracts/escrow_fee_distributor/src/contract.rs +++ b/contracts/escrow_fee_distributor/src/contract.rs @@ -1,6 +1,6 @@ use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; use cosmwasm_std::{ - attr, entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, + attr, entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128, }; use cw2::set_contract_version; @@ -299,11 +299,11 @@ fn update_config( pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> 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/Cargo.toml b/contracts/generator_controller/Cargo.toml index 6914c388..4811bc56 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/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-pair = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-factory = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-token = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-staking = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-whitelist = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } +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/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/builder_unlock/.cargo/config b/contracts/generator_controller_lite/.cargo/config similarity index 82% rename from contracts/builder_unlock/.cargo/config rename to contracts/generator_controller_lite/.cargo/config index a79b8fdb..8d4bc738 100644 --- a/contracts/builder_unlock/.cargo/config +++ b/contracts/generator_controller_lite/.cargo/config @@ -3,4 +3,4 @@ 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" +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..f10c47de --- /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/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" +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..3f89871a --- /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_json_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_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, +}; + +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_contract_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_json_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_json_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_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_json_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/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..6041e3b1 --- /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 = "1.1" +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/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..343c6559 --- /dev/null +++ b/contracts/hub/src/execute.rs @@ -0,0 +1,1602 @@ +use cosmwasm_std::{ + entry_point, from_json, to_json_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_json(&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_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_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, + }) + .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_json_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_json_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_json_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_json_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_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, + }) + .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_json_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_json_binary(&total_expected).unwrap()); + + // Trigger a timeout on minting xASTRO remotely + let mint_msg = to_json_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_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, + }) + .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_json_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_json_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_json_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_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 new file mode 100644 index 00000000..492049b3 --- /dev/null +++ b/contracts/hub/src/ibc.rs @@ -0,0 +1,678 @@ +use astroport::querier::query_token_balance; +use cosmwasm_std::{ + 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, +}; + +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_json_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_json(&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_json(&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_json_binary(&Response::Result { + action: None, + address: None, + error: None, + }) + .unwrap(), + ); + let mint_msg = to_json_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_json_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_json(&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_json(&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..d8a1aa64 --- /dev/null +++ b/contracts/hub/src/ibc_governance.rs @@ -0,0 +1,727 @@ +use cosmwasm_std::{to_json_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_json_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_json_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_json_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_json_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_json_binary(&unlock_msg)?, + funds: vec![], + }; + + // If the unlock succeeds, the ack will be sent back to the Outpost + let ack_data = to_json_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_json_binary(&blacklist_msg)?, + funds: vec![], + }; + + // If the vote succeeds, the ack will be sent back to the Outpost + let ack_data = to_json_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_json, + 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_json_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_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, + }) + .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_json_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_json(&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_json_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_json(&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_json_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_json_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_json(&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_json_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_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, + }) + .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_json_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_json(&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_json_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_json_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_json(&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_json_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_json_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_json(&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_json_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..8d2e67e7 --- /dev/null +++ b/contracts/hub/src/ibc_misc.rs @@ -0,0 +1,230 @@ +use astroport::cw20_ics20::TransferMsg; +use cosmwasm_std::{to_json_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_json_binary(&transfer_msg)?, + }; + + let msg = WasmMsg::Execute { + contract_addr: config.token_addr.to_string(), + 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_json_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_json, 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_json_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_json_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_json(&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_json_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_json(&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_json_binary(&TransferMsg { + remote_address: user.to_string(), + channel: "channel-1".to_string(), + timeout: Some(10), + memo: None, + }) + .unwrap(); + let cw_send_msg = to_json_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..47c3a8a3 --- /dev/null +++ b/contracts/hub/src/ibc_query.rs @@ -0,0 +1,170 @@ +use cosmwasm_std::{to_json_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_json_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_json, 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_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_json(&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_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_json(&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..74b3f24d --- /dev/null +++ b/contracts/hub/src/ibc_staking.rs @@ -0,0 +1,330 @@ +use astroport::querier::query_token_balance; +use cosmwasm_std::{ + to_json_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_json_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_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_json_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_json, + 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_json_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_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, + }) + .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_json_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_json(&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_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, + }) + .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_json_binary(&TransferMsg { + channel: "channel-1".to_string(), + remote_address: unstaker.to_string(), + timeout: Some(10), + memo: None, + }) + .unwrap(); + let send_msg = to_json_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_json_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..372b16b2 --- /dev/null +++ b/contracts/hub/src/mock.rs @@ -0,0 +1,296 @@ +use std::cell::Cell; + +#[cfg(test)] +use cosmwasm_std::{from_json, Uint64}; +use cosmwasm_std::{ + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + 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::{ + 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_json(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_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_json_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_json_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_json_binary(&response).into()); + } + if contract_addr != ASSEMBLY { + return SystemResult::Err(SystemError::Unknown {}); + } + match from_json(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(), + 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: vec![], + deposit_amount: Uint128::one(), + ibc_channel: None, + }; + SystemResult::Ok(to_json_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_json_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..f85be3d7 --- /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_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}; +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_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_json_binary(&HubBalance { + balance: channel_balance_at(deps.storage, &channel, timestamp.u64())?, + }), + QueryMsg::TotalChannelBalancesAt { timestamp } => to_json_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_json_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_json_binary(&HubBalance { balance: funds }) +} diff --git a/contracts/hub/src/reply.rs b/contracts/hub/src/reply.rs new file mode 100644 index 00000000..966611d8 --- /dev/null +++ b/contracts/hub/src/reply.rs @@ -0,0 +1,162 @@ +use astroport::{cw20_ics20::TransferMsg, querier::query_token_balance}; +use cosmwasm_std::{ + entry_point, to_json_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_json_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_json_binary(&transfer_msg)?, + }; + + let wasm_msg = WasmMsg::Execute { + contract_addr: config.token_addr.to_string(), + msg: to_json_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..722742f6 --- /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/astroport-core" } +astroport-governance = { path = "../../packages/astroport-governance" } +serde-json-wasm = "0.5.1" +base64 = { version = "0.13.0" } + +[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..7b5e6f64 --- /dev/null +++ b/contracts/outpost/src/execute.rs @@ -0,0 +1,1324 @@ +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_json, to_json_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_json(&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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_binary(&astroport_governance::outpost::Cw20HookMsg::Unstake {}) + .unwrap(), + }), + ) + .unwrap(); + + // Build the expected message + let ibc_message = to_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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..291fb650 --- /dev/null +++ b/contracts/outpost/src/ibc.rs @@ -0,0 +1,662 @@ +use cosmwasm_std::{ + 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, +}; + +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_json_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_json(&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_json(&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_json(&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_json_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_json(&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_json_binary(&proposal_response).unwrap()); + let mint_msg = to_json_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_json_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..7fd9e6ca --- /dev/null +++ b/contracts/outpost/src/ibc_failure.rs @@ -0,0 +1,656 @@ +use cosmwasm_std::{to_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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_json_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..06c078d8 --- /dev/null +++ b/contracts/outpost/src/ibc_mint.rs @@ -0,0 +1,180 @@ +use astroport_governance::interchain::Response; +use cosmwasm_std::{to_json_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_json_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_json_binary(&mint_msg)?, + funds: vec![], + }) +} + +#[cfg(test)] +mod tests { + use astroport_governance::interchain::Outpost; + use cosmwasm_std::{ + from_json, 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_json_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_json(&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_json(&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_json(&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_json_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..b6d77e83 --- /dev/null +++ b/contracts/outpost/src/mock.rs @@ -0,0 +1,217 @@ +#[cfg(test)] +use cosmwasm_std::{ + testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage}, + 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_json, 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_json(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_json(msg).unwrap() { + astroport::xastro_outpost_token::QueryMsg::BalanceAt { + address: _, + timestamp: _, + } => { + let balance = astroport::token::BalanceResponse { + balance: Uint128::from(1000u128), + }; + SystemResult::Ok(to_json_binary(&balance).into()) + } + _ => { + panic!("DO NOT ENTER HERE") + } + } + } else { + match from_json(msg).unwrap() { + astroport_governance::voting_escrow_lite::QueryMsg::UserDepositAt { + user:_, + timestamp:_, + } => { + let balance = astroport::token::BalanceResponse { + balance: Uint128::zero(), + }; + SystemResult::Ok(to_json_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_json_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_json_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..5cd0b865 --- /dev/null +++ b/contracts/outpost/src/query.rs @@ -0,0 +1,174 @@ +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; +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_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::ProposalVoted { proposal_id, user } => { + let user_address = deps.api.addr_validate(&user)?; + to_json_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_json_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/contracts/voting_escrow/Cargo.toml b/contracts/voting_escrow/Cargo.toml index 441fb05c..c8a68d4a 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/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-staking = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } +astroport-token = { git = "https://github.com/astroport-fi/astroport-core" } +astroport-staking = { git = "https://github.com/astroport-fi/astroport-core" } astroport-escrow-fee-distributor = { path = "../escrow_fee_distributor" } anyhow = "1" proptest = "1.0" 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/.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..fc5b22ff --- /dev/null +++ b/contracts/voting_escrow_lite/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "astroport-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"] +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" +thiserror = "1" +astroport-governance = { path = "../../packages/astroport-governance" } +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/astroport-core", branch = "feat/neutron-migration" } +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..6c59863b --- /dev/null +++ b/contracts/voting_escrow_lite/src/contract.rs @@ -0,0 +1,107 @@ +#[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 astroport_governance::utils::DEFAULT_UNLOCK_PERIOD; +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 = env!("CARGO_PKG_NAME"); +/// 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)?; + validate_native_denom(&msg.deposit_denom)?; + + 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_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)?, + 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()) +} diff --git a/contracts/voting_escrow_lite/src/error.rs b/contracts/voting_escrow_lite/src/error.rs new file mode 100644 index 00000000..55baa4c4 --- /dev/null +++ b/contracts/voting_escrow_lite/src/error.rs @@ -0,0 +1,47 @@ +use cosmwasm_std::{OverflowError, StdError}; +use cw20_base::ContractError as cw20baseError; +use cw_utils::PaymentError; +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("{0}")] + OverflowError(#[from] OverflowError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[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("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("Already unlocking")] + Unlocking {}, + + #[error("The lock has not been unlocked, call unlock first")] + NotUnlocked {}, +} diff --git a/contracts/voting_escrow_lite/src/execute.rs b/contracts/voting_escrow_lite/src/execute.rs new file mode 100644 index 00000000..78c84f3b --- /dev/null +++ b/contracts/voting_escrow_lite/src/execute.rs @@ -0,0 +1,590 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, coins, to_json_binary, Addr, BankMsg, CosmosMsg, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Storage, Uint128, WasmMsg, +}; +use cw20_base::contract::{execute_update_marketing, execute_upload_logo}; +use cw20_base::state::MARKETING_INFO; +use cw_utils::must_pay; + +use astroport_governance::voting_escrow_lite::{Config, ExecuteMsg}; +use astroport_governance::{generator_controller_lite, outpost}; + +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}; +use crate::utils::{blacklist_check, fetch_last_checkpoint}; + +/// Exposes all the execute functions available in the contract. +/// +/// ## Execute messages +/// * **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::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 } => { + blacklist_check(deps.storage, &info.sender)?; + + 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), + 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), + } +} + +/// 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_json_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_json_binary(&outpost::ExecuteMsg::KickUnlocked { user: sender })?, + funds: vec![], + }) + } + _ => { + return Err(StdError::generic_err( + "Either Generator Controller or Outpost must be set", + ) + .into()); + } + }; + + 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 = BankMsg::Send { + to_address: sender.to_string(), + amount: coins(lock.amount.u128(), &config.deposit_denom), + }; + 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: 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 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(); + + 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..1a386a47 --- /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_json_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_json_binary(&check_voters_are_blacklisted(deps, voters)?) + } + QueryMsg::BlacklistedVoters { start_after, limit } => { + to_json_binary(&get_blacklisted_voters(deps, start_after, limit)?) + } + QueryMsg::TotalVotingPower {} => to_json_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::TotalVotingPowerAt { .. } => to_json_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::TotalVotingPowerAtPeriod { .. } => to_json_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::UserVotingPower { .. } => to_json_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::UserVotingPowerAt { .. } => to_json_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::UserVotingPowerAtPeriod { .. } => to_json_binary(&VotingPowerResponse { + voting_power: Uint128::zero(), + }), + QueryMsg::TotalEmissionsVotingPower {} => { + to_json_binary(&get_total_emissions_voting_power(deps, env, None)?) + } + QueryMsg::TotalEmissionsVotingPowerAt { time } => { + to_json_binary(&get_total_emissions_voting_power(deps, env, Some(time))?) + } + QueryMsg::UserEmissionsVotingPower { user } => { + to_json_binary(&get_user_emissions_voting_power(deps, env, user, None)?) + } + QueryMsg::UserEmissionsVotingPowerAt { user, time } => to_json_binary( + &get_user_emissions_voting_power(deps, env, user, Some(time))?, + ), + QueryMsg::LockInfo { user } => to_json_binary(&get_user_lock_info(deps, env, user)?), + QueryMsg::UserDepositAt { user, timestamp } => { + to_json_binary(&get_user_deposit_at_time(deps, user, timestamp)?) + } + QueryMsg::Config {} => { + let config = CONFIG.load(deps.storage)?; + to_json_binary(&config) + } + 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)?), + } +} + +/// 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..5d02a1cc --- /dev/null +++ b/contracts/voting_escrow_lite/src/utils.rs @@ -0,0 +1,34 @@ +use cosmwasm_std::{Addr, Order, StdResult, Storage, Uint128}; +use cw_storage_plus::Bound; + +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())) + } 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/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..9137ad90 --- /dev/null +++ b/contracts/voting_escrow_lite/tests/test_utils.rs @@ -0,0 +1,440 @@ +#![allow(dead_code)] + +use anyhow::Result; +use cosmwasm_std::{coins, Addr, BlockInfo, StdResult, Timestamp, Uint128, Uint64}; +use cw20::Logo; +use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor}; + +use astroport_governance::utils::EPOCH_START; +use astroport_governance::voting_escrow_lite::{ + BlacklistedVotersResponse, ExecuteMsg, InstantiateMsg, QueryMsg, UpdateMarketingInfo, + VotingPowerResponse, +}; + +pub const MULTIPLIER: u128 = 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 vxastro: Addr, + pub generator_controller: Addr, +} + +impl Helper { + 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 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 voting_code_id = app.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_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 vxastro = app + .instantiate_contract( + voting_code_id, + owner.clone(), + &msg, + &[], + String::from("vxASTRO"), + None, + ) + .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 = app.store_code(generator_controller); + + let msg = astroport_governance::generator_controller_lite::InstantiateMsg { + owner: owner.to_string(), + assembly_addr: "assembly".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 = app + .instantiate_contract( + generator_controller_id, + owner.clone(), + &msg, + &[], + String::from("Generator Controller Lite"), + 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, + vxastro, + generator_controller, + } + } + + 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, XASTRO_DENOM), + ) + .unwrap(); + } + + pub fn check_xastro_balance(&self, user: &str, amount: u128) { + let amount = amount * MULTIPLIER; + let balance = self + .app + .wrap() + .query_balance(user, XASTRO_DENOM) + .unwrap() + .amount; + assert_eq!(balance.u128(), amount); + } + + 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.vxastro.clone(), + &ExecuteMsg::CreateLock {}, + &coins(amount, XASTRO_DENOM), + ) + } + + pub fn create_lock_u128(&mut self, user: &str, amount: u128) -> Result { + self.app.execute_contract( + Addr::unchecked(user), + self.vxastro.clone(), + &ExecuteMsg::CreateLock {}, + &coins(amount, XASTRO_DENOM), + ) + } + + 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.vxastro.clone(), + &ExecuteMsg::ExtendLockAmount {}, + &coins(amount, XASTRO_DENOM), + ) + } + + pub fn relock(&mut self, user: &str) -> Result { + self.app.execute_contract( + Addr::unchecked("outpost"), + self.vxastro.clone(), + &ExecuteMsg::Relock { + user: user.to_string(), + }, + &[], + ) + } + + 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.vxastro.clone(), + &ExecuteMsg::DepositFor { + user: to.to_string(), + }, + &coins(amount, XASTRO_DENOM), + ) + } + + pub fn unlock(&mut self, user: &str) -> Result { + self.app.execute_contract( + Addr::unchecked(user), + self.vxastro.clone(), + &ExecuteMsg::Unlock {}, + &[], + ) + } + + pub fn withdraw(&mut self, user: &str) -> Result { + self.app.execute_contract( + Addr::unchecked(user), + self.vxastro.clone(), + &ExecuteMsg::Withdraw {}, + &[], + ) + } + + pub fn update_blacklist( + &mut self, + append_addrs: Vec, + remove_addrs: Vec, + ) -> Result { + self.app.execute_contract( + Addr::unchecked("owner"), + self.vxastro.clone(), + &ExecuteMsg::UpdateBlacklist { + append_addrs, + remove_addrs, + }, + &[], + ) + } + + pub fn update_outpost_address(&mut self, new_address: String) -> Result { + self.app.execute_contract( + Addr::unchecked("owner"), + self.vxastro.clone(), + &ExecuteMsg::UpdateConfig { + new_guardian: None, + generator_controller: None, + outpost: Some(new_address), + }, + &[], + ) + } + + pub fn query_user_vp(&self, user: &str) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::UserVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_user_emissions_vp(&self, user: &str) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::UserEmissionsVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_exact_user_vp(&self, user: &str) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::UserVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128()) + } + + pub fn query_exact_user_emissions_vp(&self, user: &str) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::UserEmissionsVotingPower { + user: user.to_string(), + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128()) + } + + pub fn query_user_vp_at(&self, user: &str, time: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::UserVotingPowerAt { + user: user.to_string(), + time, + }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_user_emissions_vp_at(&self, user: &str, time: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::UserEmissionsVotingPowerAt { + 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, user: &str, period: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.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) -> 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) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::TotalEmissionsVotingPower {}, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + 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) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::TotalEmissionsVotingPower {}, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128()) + } + + 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, time: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::TotalEmissionsVotingPowerAt { time }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_vp_at_period(&self, period: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::TotalVotingPowerAtPeriod { period }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_total_emissions_vp_at_period(&self, timestamp: u64) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::TotalEmissionsVotingPowerAt { time: timestamp }, + ) + .map(|vp: VotingPowerResponse| vp.voting_power.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_locked_balance_at(&self, user: &str, timestamp: Uint64) -> StdResult { + self.app + .wrap() + .query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::UserDepositAt { + user: user.to_string(), + timestamp, + }, + ) + .map(|vp: Uint128| vp.u128() as f32 / MULTIPLIER as f32) + } + + pub fn query_blacklisted_voters( + &self, + start_after: Option, + limit: Option, + ) -> StdResult> { + self.app.wrap().query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::BlacklistedVoters { start_after, limit }, + ) + } + + pub fn check_voters_are_blacklisted( + &self, + voters: Vec, + ) -> StdResult { + self.app.wrap().query_wasm_smart( + self.vxastro.clone(), + &QueryMsg::CheckVotersAreBlacklisted { voters }, + ) + } +} diff --git a/contracts/voting_escrow_lite/tests/vxastro_lite_integration.rs b/contracts/voting_escrow_lite/tests/vxastro_lite_integration.rs new file mode 100644 index 00000000..3c1d0743 --- /dev/null +++ b/contracts/voting_escrow_lite/tests/vxastro_lite_integration.rs @@ -0,0 +1,1026 @@ +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}; + +use crate::test_utils::{Helper, MULTIPLIER}; + +mod test_utils; + +#[test] +fn lock_unlock_logic() { + let mut helper = Helper::init(); + + helper.mint_xastro("owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro("user", 100); + helper.check_xastro_balance("user", 100); + + // Try to withdraw from a non-existent lock + 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("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().unwrap(); + assert_eq!(vp, 0.0); + 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("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("user", 99); + helper.check_xastro_balance(helper.vxastro.as_str(), 1); + + // Add more xASTRO to the existing position + 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("user").unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The lock has not been unlocked, call unlock first" + ); + + helper.unlock("user").unwrap(); + + // Go in the future + 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("user").unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The lock time has not yet expired" + ); + + // Go to the future again + 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("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 + 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("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("user", 50f32).unwrap(); + + let vp = helper.query_total_emissions_vp().unwrap(); + assert_eq!(vp, 50.0); + + let vp = helper.query_user_emissions_vp("user").unwrap(); + assert_eq!(vp, 50.0); + + // Unlock the lock + helper.unlock("user").unwrap(); + + let vp = helper.query_total_emissions_vp().unwrap(); + assert_eq!(vp, 0.0); + + let vp = helper.query_user_emissions_vp("user").unwrap(); + assert_eq!(vp, 0.0); + + // Relock +} + +#[test] +fn new_lock_after_unlock() { + let mut helper = Helper::init(); + helper.mint_xastro("owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro("user", 100); + + helper.create_lock("user", 50f32).unwrap(); + + let vp = helper.query_user_vp("user").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp().unwrap(); + assert_eq!(vp, 0.0); + + let evp = helper.query_user_emissions_vp("user").unwrap(); + assert_eq!(evp, 50.0); + let evp = helper.query_total_emissions_vp().unwrap(); + assert_eq!(evp, 50.0); + + // Go to the future + helper.app.update_block(next_block); + + helper.unlock("user").unwrap(); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK * 2)); + + helper.withdraw("user").unwrap(); + helper.check_xastro_balance("user", 100); + + let vp = helper.query_user_vp("user").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp().unwrap(); + assert_eq!(vp, 0.0); + + // Create a new lock in 3 weeks from now + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK * 3)); + + 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().unwrap(); + assert_eq!(vp, 0.0); + + let evp = helper.query_user_emissions_vp("user").unwrap(); + assert_eq!(evp, 100.0); + 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 mut helper = Helper::init(); + helper.mint_xastro("owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro("user", 100); + helper.mint_xastro("user2", 100); + + helper.create_lock("user", 30f32).unwrap(); + + // Go to the future + 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("user2", 50f32).unwrap(); + let vp = helper.query_total_vp().unwrap(); + assert_eq!(vp, 0.0); + + let vp = helper.query_total_emissions_vp().unwrap(); + assert_eq!(vp, 80.0); + + // Go to the future + helper.app.update_block(next_block); + helper + .app + .update_block(|block| block.time = block.time.plus_seconds(WEEK * 4)); + + 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("user2").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp().unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_emissions_vp("user").unwrap(); + assert_eq!(vp, 100.0); + let vp = helper.query_user_emissions_vp("user2").unwrap(); + assert_eq!(vp, 50.0); + let vp = helper.query_total_emissions_vp().unwrap(); + assert_eq!(vp, 150.0); + + let res = helper + .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(helper.app.block_info().time.seconds() + WEEK) + .unwrap(); + assert_eq!(res, 0.0); + + let res = helper + .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(helper.app.block_info().time.seconds() + WEEK) + .unwrap(); + assert_eq!(res, 150.0); + + // Go to the future + 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("user2").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_total_vp().unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_emissions_vp("user").unwrap(); + assert_eq!(vp, 100.0); + let vp = helper.query_user_emissions_vp("user2").unwrap(); + assert_eq!(vp, 50.0); + let vp = helper.query_total_emissions_vp().unwrap(); + assert_eq!(vp, 150.0); +} + +#[test] +fn check_queries() { + let mut helper = Helper::init(); + helper.mint_xastro("owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + helper.mint_xastro("user", 100); + helper.check_xastro_balance("user", 100); + + // Create valid voting escrow lock + helper.create_lock("user", 90f32).unwrap(); + // Check that 90 xASTRO were actually debited + helper.check_xastro_balance("user", 10); + helper.check_xastro_balance(helper.vxastro.as_str(), 90); + + // Validate user's lock + let user_lock: LockInfoResponse = helper + .app + .wrap() + .query_wasm_smart( + helper.vxastro.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(helper.app.block_info().time.seconds()) + .unwrap(); + assert_eq!(total_vp_at_ts, 0.0); + + // Must always be 0 + 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("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("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(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("user").unwrap(); + assert_eq!(user_emissions_vp, 90.0); + + let user_emissions_vp = helper + .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("user", 90); + // SnapshotMap checkpoints the data at the next block + let start_time = Uint64::from(helper.app.block_info().time.seconds() + 1); + + let balance_timestamp = helper.query_locked_balance_at("user", start_time).unwrap(); + assert_eq!(balance_timestamp, 90f32); + + helper.app.update_block(next_block); + helper.extend_lock_amount("user", 100f32).unwrap(); + + let balance_timestamp = helper.query_locked_balance_at("user", start_time).unwrap(); + assert_eq!(balance_timestamp, 90f32); + + helper.app.update_block(|bi| { + bi.height += 100000; + bi.time = bi.time.plus_seconds(500000); + }); + + 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( + "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( + "user", + Uint64::from(helper.app.block_info().time.seconds()), // Next block adds 5 seconds + ) + .unwrap(); + assert_eq!(balance_timestamp, 190f32); + + helper.app.update_block(|bi| { + bi.height += 1; + bi.time = bi.time.plus_seconds(WEEK * 102); + }); + helper.unlock("user").unwrap(); + + // Ensure emissions voting power is 0 after unlock + let user_emissions_vp = helper + .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 + helper.app.update_block(|bi| { + bi.height += 1; + bi.time = bi.time.plus_seconds(WEEK * 102); + }); + // Withdraw + 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( + "user", + 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( + "user", + Uint64::from(helper.app.block_info().time.seconds() - 5), // Next block adds 5 seconds + ) + .unwrap(); + assert_eq!(balance_timestamp, 190f32); + + // add users to the blacklist + helper + .update_blacklist( + 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(), + ], + vec![], + ) + .unwrap(); + + // query all blacklisted voters + let blacklisted_voters = helper.query_blacklisted_voters(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(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(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(vec!["voter0".to_string(), "voter33".to_string()], vec![]) + .unwrap(); + + // query voters by specified parameters + let blacklisted_voters = helper + .query_blacklisted_voters(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(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(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(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(vec!["voter1".to_string(), "voter8".to_string()]) + .unwrap(); + assert_eq!("Voters are blacklisted!", res.to_string()); +} + +#[test] +fn check_deposit_for() { + let mut helper = Helper::init(); + helper.mint_xastro("owner", 100); + + // Mint ASTRO, stake it and mint xASTRO + 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("user1", 50f32).unwrap(); + let vp = helper.query_user_vp("user1").unwrap(); + assert_eq!(0.0, vp); + let vp = helper.query_user_emissions_vp("user1").unwrap(); + assert_eq!(50.0, vp); + + 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("user1").unwrap(); + assert_eq!(100.0, vp); + helper.check_xastro_balance("user1", 50); + helper.check_xastro_balance("user2", 50); +} + +#[test] +fn check_update_owner() { + let mut helper = Helper::init(); + + 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 = helper + .app + .execute_contract( + Addr::unchecked("not_owner"), + helper.vxastro.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim before proposal + let err = helper + .app + .execute_contract( + Addr::unchecked(new_owner.clone()), + helper.vxastro.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Ownership proposal not found" + ); + + // Propose new owner + helper + .app + .execute_contract(Addr::unchecked("owner"), helper.vxastro.clone(), &msg, &[]) + .unwrap(); + + // Claim from invalid addr + let err = helper + .app + .execute_contract( + Addr::unchecked("invalid_addr"), + helper.vxastro.clone(), + &ExecuteMsg::ClaimOwnership {}, + &[], + ) + .unwrap_err(); + assert_eq!(err.root_cause().to_string(), "Generic error: Unauthorized"); + + // Claim ownership + 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 = helper + .app + .wrap() + .query_wasm_smart(&helper.vxastro, &msg) + .unwrap(); + + assert_eq!(res.owner, new_owner) +} + +#[test] +fn check_blacklist() { + let mut helper = Helper::init(); + + // Mint ASTRO, stake it and mint xASTRO + 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(vec![], vec![]).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Generic error: Append and remove arrays are empty" + ); + + // Blacklisting user2 + let res = helper + .update_blacklist(vec!["user2".to_string()], vec![]) + .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("user1", 50f32).unwrap(); + // Try to create lock from a blacklisted address + 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("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("user2", 100); + // And they did not create a lock, thus we have no information to query + let vp = helper.query_user_vp("user2").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_emissions_vp("user2").unwrap(); + assert_eq!(vp, 0.0); + + // Go to the future + 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("user2", 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: vec!["user1".to_string()], + remove_addrs: vec![], + }; + let res = helper + .app + .execute_contract( + Addr::unchecked("guardian"), + helper.vxastro.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("user1", 10f32).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "The user1 address is blacklisted" + ); + 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("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("user1").unwrap(); + assert_eq!(vp, 0.0); + let vp = helper.query_user_emissions_vp("user1").unwrap(); + assert_eq!(vp, 0.0); + // Voting + let vp = helper + .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().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().unwrap(); + assert_eq!(vp, 0.0); + + // The only option available for a blacklisted user is to unlock and withdraw their funds + helper.unlock("user1").unwrap(); + + // Go to the future + 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("user1").unwrap(); + + // Remove user1 from the blacklist + let res = helper + .update_blacklist(vec![], 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("user1", 10f32).unwrap(); +} + +#[test] +fn check_residual() { + let mut helper = Helper::init(); + let users_num = 1000; + let lock_amount = 100_000_000; + + helper.mint_xastro("owner", 100); + + for i in 1..(users_num / 2) { + let user = &format!("user{}", i); + 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(user).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(user).unwrap(); + } + + assert_eq!(sum, helper.query_exact_total_emissions_vp().unwrap()); + + 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(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(user).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); + + helper.app.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(user).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); + + helper.app.update_block(|bi| { + bi.height += 1; + bi.time = bi.time.plus_seconds(WEEK); + }); + } +} + +#[test] +fn total_vp_multiple_slope_subtraction() { + let mut helper = Helper::init(); + + 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().unwrap(); + assert_eq!(total, 100.0); + + helper + .app + .update_block(|bi| bi.time = bi.time.plus_seconds(2 * WEEK)); + // Slope changes have been applied + let total = helper.query_total_vp().unwrap(); + assert_eq!(total, 0.0); + let total = helper.query_total_emissions_vp().unwrap(); + assert_eq!(total, 100.0); + + helper.unlock("user1").unwrap(); + + // Try to manipulate over expired lock 3 weeks later + helper + .app + .update_block(|bi| bi.time = bi.time.plus_seconds(3 * WEEK)); + + 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("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().unwrap(); + assert_eq!(total, 0f32); + let total = helper.query_total_emissions_vp().unwrap(); + assert_eq!(total, 0f32); +} + +#[test] +fn marketing_info() { + let mut helper = Helper::init(); + + let err = helper + .app + .execute_contract( + helper.owner.clone(), + helper.vxastro.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 = helper + .app + .execute_contract( + helper.owner.clone(), + helper.vxastro.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" + ); + + helper + .app + .execute_contract( + helper.owner.clone(), + helper.vxastro.clone(), + &ExecuteMsg::SetLogoUrlsWhitelist { + whitelist: vec!["example.com/".to_string()], + }, + &[], + ) + .unwrap(); + + let err = helper + .app + .execute_contract( + helper.owner.clone(), + helper.vxastro.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 = helper + .app + .execute_contract( + helper.owner.clone(), + helper.vxastro.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: " + ); + + helper + .app + .execute_contract( + helper.owner.clone(), + helper.vxastro.clone(), + &ExecuteMsg::UpdateMarketing { + project: Some("Some project".to_string()), + description: Some("Some description".to_string()), + marketing: None, + }, + &[], + ) + .unwrap(); + + let config: Config = helper + .app + .wrap() + .query_wasm_smart(&helper.vxastro, &QueryMsg::Config {}) + .unwrap(); + assert_eq!(config.logo_urls_whitelist, vec!["example.com/".to_string()]); + let marketing_info: MarketingInfoResponse = helper + .app + .wrap() + .query_wasm_smart(&helper.vxastro, &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 = helper + .app + .execute_contract( + helper.owner.clone(), + helper.vxastro.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", + ); + + helper + .app + .execute_contract( + helper.owner.clone(), + helper.vxastro.clone(), + &ExecuteMsg::UploadLogo(Logo::Url("example.com/logo.svg".to_string())), + &[], + ) + .unwrap(); + + let marketing_info: MarketingInfoResponse = helper + .app + .wrap() + .query_wasm_smart(&helper.vxastro, &QueryMsg::MarketingInfo {}) + .unwrap(); + assert_eq!( + marketing_info.logo.unwrap(), + LogoInfo::Url("example.com/logo.svg".to_string()) + ); +} diff --git a/packages/astroport-governance/Cargo.toml b/packages/astroport-governance/Cargo.toml index 974cbbd8..ea51744b 100644 --- a/packages/astroport-governance/Cargo.toml +++ b/packages/astroport-governance/Cargo.toml @@ -1,22 +1,23 @@ [package] name = "astroport-governance" -version = "1.2.0" +version = "3.0.0" 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 [features] -testnet = [] # for quicker tests, cargo test --lib # for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] [dependencies] -cw20 = "0.15" -cosmwasm-std = "1.1" -cw-storage-plus = "0.15" -cosmwasm-schema = "1.1" -astroport = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } +cw20 = "1.1" +cosmwasm-std = { workspace = true, features = ["ibc3"] } +cw-storage-plus.workspace = true +cosmwasm-schema.workspace = true +thiserror.workspace = true \ No newline at end of file diff --git a/packages/astroport-governance/src/assembly.rs b/packages/astroport-governance/src/assembly.rs index a5764418..0d475eb4 100644 --- a/packages/astroport-governance/src/assembly.rs +++ b/packages/astroport-governance/src/assembly.rs @@ -1,43 +1,24 @@ -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::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"; +/// 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..=66460; +/// 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; /// Proposal validation attributes const MIN_TITLE_LENGTH: usize = 4; @@ -53,14 +34,18 @@ 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_token_addr: 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 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 @@ -82,8 +67,16 @@ 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, + #[serde(default)] + messages: Vec, + /// 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 @@ -97,10 +90,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 @@ -108,11 +98,6 @@ pub enum ExecuteMsg { /// Proposal identifier proposal_id: u64, }, - /// 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 @@ -124,6 +109,7 @@ pub enum ExecuteMsg { proposal_id: u64, status: ProposalStatus, }, + ExecuteFromMultisig(Vec), } /// Thie enum describes all the queries available in the contract. @@ -142,14 +128,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, }, @@ -167,29 +151,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, - /// vxASTRO token address - pub vxastro_token_addr: Option, - /// Voting Escrow delegator address - pub voting_escrow_delegator_addr: Option, + /// xASTRO token denom + pub xastro_denom: String, + // xASTRO denom tracking contract + pub xastro_denom_tracking: String, /// Astroport IBC controller contract pub ibc_controller: Option, /// Builder unlock contract address @@ -265,12 +233,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(()) } } @@ -278,12 +240,6 @@ 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, - /// 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, /// Builder unlock contract address @@ -317,12 +273,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, - /// `For` votes for the proposal - pub for_voters: Vec, - /// `Against` votes for the proposal - pub against_voters: Vec, + /// `Against` power of proposal cast from all Outposts + pub outpost_against_power: Uint128, /// Start block of proposal pub start_block: u64, /// Start time of proposal @@ -340,11 +296,13 @@ 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 pub ibc_channel: Option, + /// Total voting power 1 second before the proposal was created + pub total_voting_power: Uint128, } impl Proposal { @@ -472,6 +430,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}; diff --git a/packages/astroport-governance/src/builder_unlock.rs b/packages/astroport-governance/src/builder_unlock.rs index 87ed89df..29cb0188 100644 --- a/packages/astroport-governance/src/builder_unlock.rs +++ b/packages/astroport-governance/src/builder_unlock.rs @@ -1,13 +1,121 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, 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] 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, } @@ -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. @@ -34,26 +142,25 @@ 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. #[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 ))); }; @@ -63,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(()) } +} - pub fn update_schedule( - &mut self, - new_schedule: Schedule, - account: &String, - ) -> Result<(), StdError> { +#[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, +} + +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: {}", @@ -107,152 +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 crate::builder_unlock::Schedule; - use cosmwasm_schema::{cw_serde, QueryResponses}; - use cosmwasm_std::Uint128; - use cw20::Cw20ReceiveMsg; - - 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 address - pub astro_token: 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 { - /// Receive is an implementation for the CW20 receive msg - Receive(Cw20ReceiveMsg), - /// 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 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)] - 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, - } -} 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/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 2f3beebf..874adb49 100644 --- a/packages/astroport-governance/src/lib.rs +++ b/packages/astroport-governance/src/lib.rs @@ -2,12 +2,15 @@ pub mod assembly; 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; pub mod voting_escrow; pub mod voting_escrow_delegation; - -pub use astroport; +pub mod voting_escrow_lite; // Default pagination constants pub const DEFAULT_LIMIT: u32 = 10; diff --git a/packages/astroport-governance/src/outpost.rs b/packages/astroport-governance/src/outpost.rs new file mode 100644 index 00000000..15cba93e --- /dev/null +++ b/packages/astroport-governance/src/outpost.rs @@ -0,0 +1,107 @@ +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 66960354..ec0f261f 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::{ + Addr, ChannelResponse, Decimal, Fraction, IbcQuery, OverflowError, QuerierWrapper, StdError, + StdResult, Uint128, Uint256, Uint64, +}; -use cosmwasm_std::{Decimal, Fraction, OverflowError, StdError, StdResult, Uint128, Uint256}; +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 +/// 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,51 @@ pub fn calc_voting_power( .unwrap_or_else(|_| Uint128::zero()); old_vp.saturating_sub(shift) } + +/// Checks that a contract supports a given IBC-channel. +/// ## Params +/// * **querier** is an object of type [`QuerierWrapper`]. +/// +/// * **contract** is the contract to check channel support on. +/// +/// * **given_channel** is an IBC channel id the function needs to check. +pub fn check_contract_supports_channel( + querier: QuerierWrapper, + contract: &Addr, + given_channel: &String, +) -> StdResult<()> { + let port_id = Some(format!("wasm.{contract}")); + 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 +/// ## 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_wasm_smart( + contract, + &crate::hub::QueryMsg::TotalChannelBalancesAt { + timestamp: Uint64::from(timestamp), + }, + )?; + Ok(response.balance) +} 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..e66a8fc7 --- /dev/null +++ b/packages/astroport-governance/src/voting_escrow_lite.rs @@ -0,0 +1,333 @@ +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, +}; + +/// ## 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_denom: 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 { + /// 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 + 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 { + #[serde(default)] + append_addrs: Vec, + #[serde(default)] + remove_addrs: Vec, + }, + /// 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 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_denom: String, + /// 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..359169cc --- /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/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/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" 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..adfdd7cd --- /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_json_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_json_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_denom: 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_json_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_json_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..bc202b61 --- /dev/null +++ b/packages/astroport-tests-lite/src/controller_helper.rs @@ -0,0 +1,494 @@ +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, + permissioned: 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::queries::query, + )); + + let assembly_code = router.store_code(assembly_contract); + + let assembly_default_instantiate_msg = astroport_governance::assembly::InstantiateMsg { + staking_addr: escrow_helper.staking_instance.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..a22ded4b --- /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_json_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_json_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_denom: 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_json_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_json_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_json_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_json_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() + } +} diff --git a/packages/astroport-tests/Cargo.toml b/packages/astroport-tests/Cargo.toml index 80b5d24a..c1ad76eb 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/astroport-core", branch = "feat/merge_hidden_2023_05_22" } +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 = { path = "../../contracts/voting_escrow" } generator-controller = { path = "../../contracts/generator_controller" } -astroport-generator = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-pair = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-factory = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-token = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-staking = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } -astroport-whitelist = { git = "https://github.com/astroport-fi/astroport-core", branch = "feat/merge_hidden_2023_05_22" } +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" 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(), 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/check_artifacts_size.sh b/scripts/check_artifacts_size.sh index db79a703..a342215b 100755 --- a/scripts/check_artifacts_size.sh +++ b/scripts/check_artifacts_size.sh @@ -8,7 +8,7 @@ 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 + cosmwasm/workspace-optimizer:0.15.1 # terra: https://github.com/terra-money/wasmd/blob/2308975f45eac299bdf246737674482eaa51051c/x/wasm/types/validation.go#L12 @@ -23,4 +23,4 @@ for artifact in artifacts/*.wasm; do echo "Max size: $maximum_size" exit 1 fi -done +done \ No newline at end of file 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/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 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 -}