From 37125086a464050af62bb4d15d936653cae61f31 Mon Sep 17 00:00:00 2001 From: Jake Hartnell Date: Wed, 23 Aug 2023 01:45:37 -0700 Subject: [PATCH] Token Factory DAOs (#731) --- .cargo/config | 8 +- .github/workflows/basic.yml | 2 +- .github/workflows/codecov.yml | 47 +- .github/workflows/integration_tests.yml | 13 + Cargo.lock | 906 ++++++++--- Cargo.toml | 25 +- contracts/dao-dao-core/.gitignore | 15 - contracts/external/cw-token-swap/.gitignore | 12 - .../cw-tokenfactory-issuer/.cargo/config | 4 + .../cw-tokenfactory-issuer/Cargo.toml | 53 + .../external/cw-tokenfactory-issuer/LICENSE | 202 +++ .../external/cw-tokenfactory-issuer/NOTICE | 13 + .../external/cw-tokenfactory-issuer/README.md | 49 + .../cw-tokenfactory-issuer/examples/schema.rs | 12 + .../schema/cw-tokenfactory-issuer.json | 1379 +++++++++++++++++ .../cw-tokenfactory-issuer/src/contract.rs | 249 +++ .../cw-tokenfactory-issuer/src/error.rs | 73 + .../cw-tokenfactory-issuer/src/execute.rs | 469 ++++++ .../cw-tokenfactory-issuer/src/helpers.rs | 120 ++ .../cw-tokenfactory-issuer/src/hooks.rs | 21 + .../cw-tokenfactory-issuer/src/lib.rs | 10 + .../cw-tokenfactory-issuer/src/msg.rs | 238 +++ .../cw-tokenfactory-issuer/src/queries.rs | 235 +++ .../cw-tokenfactory-issuer/src/state.rs | 25 + .../tests/cases/beforesend.rs | 154 ++ .../tests/cases/blacklist.rs | 339 ++++ .../tests/cases/burn.rs | 353 +++++ .../tests/cases/contract_owner.rs | 49 + .../tests/cases/denom_metadata.rs | 143 ++ .../tests/cases/force_transfer.rs | 52 + .../tests/cases/freeze.rs | 206 +++ .../tests/cases/instantiate.rs | 132 ++ .../tests/cases/mint.rs | 279 ++++ .../cw-tokenfactory-issuer/tests/cases/mod.rs | 12 + .../tests/cases/set_before_update_hook.rs | 32 + .../tests/cases/tokenfactory_admin.rs | 35 + .../tests/cases/whitelist.rs | 339 ++++ .../cw-tokenfactory-issuer/tests/mod.rs | 8 + .../cw-tokenfactory-issuer/tests/test_env.rs | 677 ++++++++ .../dao-proposal-condorcet/.gitignore | 16 - .../src/testing/instantiate.rs | 5 +- .../src/testing/instantiate.rs | 3 +- .../voting/dao-voting-cw20-staked/.gitignore | 15 - .../dao-voting-cw20-staked/src/contract.rs | 7 +- .../voting/dao-voting-cw20-staked/src/msg.rs | 7 +- .../dao-voting-cw20-staked/src/tests.rs | 6 +- contracts/voting/dao-voting-cw4/.gitignore | 15 - .../dao-voting-cw721-staked/src/contract.rs | 5 +- .../voting/dao-voting-cw721-staked/src/msg.rs | 7 +- .../src/testing/tests.rs | 4 +- .../dao-voting-native-staked/.gitignore | 15 - .../dao-voting-native-staked/Cargo.toml | 17 +- .../voting/dao-voting-native-staked/README.md | 1 + .../schema/dao-voting-native-staked.json | 384 ++++- .../dao-voting-native-staked/src/contract.rs | 286 +++- .../dao-voting-native-staked/src/error.rs | 15 +- .../dao-voting-native-staked/src/hooks.rs | 51 + .../dao-voting-native-staked/src/lib.rs | 1 + .../dao-voting-native-staked/src/msg.rs | 54 +- .../dao-voting-native-staked/src/state.rs | 16 +- .../dao-voting-native-staked/src/tests.rs | 600 +++++-- .../.cargo/config | 4 + .../Cargo.toml | 50 + .../dao-voting-token-factory-staked/README.md | 76 + .../examples/schema.rs | 11 + .../dao-voting-token-factory-staked.json | 1183 ++++++++++++++ .../src/contract.rs | 711 +++++++++ .../src/error.rs | 51 + .../src/hooks.rs | 52 + .../src/lib.rs | 12 + .../src/msg.rs | 138 ++ .../src/state.rs | 56 + .../src/tests/mod.rs | 10 + .../src/tests/multitest/mod.rs | 1370 ++++++++++++++++ .../src/tests/test_tube/integration_tests.rs | 244 +++ .../src/tests/test_tube/mod.rs | 6 + .../src/tests/test_tube/test_env.rs | 370 +++++ justfile | 42 +- packages/cw-hooks/src/lib.rs | 63 +- packages/dao-testing/Cargo.toml | 14 +- packages/dao-testing/src/lib.rs | 3 + .../src/test_tube/cw_tokenfactory_issuer.rs | 155 ++ packages/dao-testing/src/test_tube/mod.rs | 9 + packages/dao-voting/src/threshold.rs | 5 + 84 files changed, 12463 insertions(+), 692 deletions(-) delete mode 100644 contracts/dao-dao-core/.gitignore delete mode 100644 contracts/external/cw-token-swap/.gitignore create mode 100644 contracts/external/cw-tokenfactory-issuer/.cargo/config create mode 100644 contracts/external/cw-tokenfactory-issuer/Cargo.toml create mode 100644 contracts/external/cw-tokenfactory-issuer/LICENSE create mode 100644 contracts/external/cw-tokenfactory-issuer/NOTICE create mode 100644 contracts/external/cw-tokenfactory-issuer/README.md create mode 100644 contracts/external/cw-tokenfactory-issuer/examples/schema.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json create mode 100644 contracts/external/cw-tokenfactory-issuer/src/contract.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/src/error.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/src/execute.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/src/helpers.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/src/hooks.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/src/lib.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/src/msg.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/src/queries.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/src/state.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/beforesend.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/blacklist.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/burn.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/contract_owner.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/denom_metadata.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/force_transfer.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/freeze.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/instantiate.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/mod.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/set_before_update_hook.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/tokenfactory_admin.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/cases/whitelist.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/mod.rs create mode 100644 contracts/external/cw-tokenfactory-issuer/tests/test_env.rs delete mode 100644 contracts/proposal/dao-proposal-condorcet/.gitignore delete mode 100644 contracts/voting/dao-voting-cw20-staked/.gitignore delete mode 100644 contracts/voting/dao-voting-cw4/.gitignore delete mode 100644 contracts/voting/dao-voting-native-staked/.gitignore create mode 100644 contracts/voting/dao-voting-native-staked/src/hooks.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/.cargo/config create mode 100644 contracts/voting/dao-voting-token-factory-staked/Cargo.toml create mode 100644 contracts/voting/dao-voting-token-factory-staked/README.md create mode 100644 contracts/voting/dao-voting-token-factory-staked/examples/schema.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/schema/dao-voting-token-factory-staked.json create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/contract.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/error.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/hooks.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/lib.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/msg.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/state.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/tests/mod.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/tests/multitest/mod.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/integration_tests.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/mod.rs create mode 100644 contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/test_env.rs create mode 100644 packages/dao-testing/src/test_tube/cw_tokenfactory_issuer.rs create mode 100644 packages/dao-testing/src/test_tube/mod.rs diff --git a/.cargo/config b/.cargo/config index 2d60e2826..e5fdda2c5 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,6 +1,10 @@ [alias] wasm = "build --release --lib --target wasm32-unknown-unknown" wasm-debug = "build --lib --target wasm32-unknown-unknown" -t = "test --workspace" -unit-test = "test --workspace" +all-test = "test --workspace" +unit-test = "test --lib" integration-test = "test --package integration-tests -- --ignored --test-threads 1 -Z unstable-options --report-time" +test-tube = "test --features test-tube" + +[env] +RUSTFLAGS = "-C link-arg=-s" diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index d4b6a8256..56580ba10 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -24,7 +24,7 @@ jobs: uses: actions-rs/cargo@v1 with: toolchain: stable - command: unit-test + command: all-test args: --locked env: RUST_BACKTRACE: 1 diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 9bf9308f2..47afc4655 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -1,20 +1,35 @@ -name: coverage -"on": +# From: https://github.com/codecov/example-rust/blob/main/.github/workflows/rust.yml +name: Coverage + +on: - push + +env: + CARGO_TERM_COLOR: always + jobs: - test: - name: coverage + build: + runs-on: ubuntu-latest - container: - image: "xd009642/tarpaulin:develop-nightly" - options: "--security-opt seccomp=unconfined" + steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Generate code coverage - run: > - cargo tarpaulin --verbose --workspace --out Xml --exclude-files test-contracts/* *test*.rs packages/dao-dao-macros/* packages/dao-testing/* ci/* - - name: Upload to codecov.io - uses: codecov/codecov-action@v3 - with: - fail_ci_if_error: false + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: llvm-tools-preview + - name: cargo install cargo-llvm-cov + run: cargo install cargo-llvm-cov + - name: cargo llvm-cov + run: cargo llvm-cov --workspace --lcov --output-path lcov.info + - name: Codecov + # You may pin to the exact commit or the version. + # uses: codecov/codecov-action@81cd2dc8148241f03f5839d295e000b8f761e378 + uses: codecov/codecov-action@v3 + with: + # Repository upload token - get it from codecov.io. Required only for private repositories + # token: # optional + # Specify whether the Codecov output should be verbose + verbose: true + fail_ci_if_error: true diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index fca7b1d82..fe568df6f 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -25,6 +25,16 @@ jobs: target: wasm32-unknown-unknown override: true + - name: Clone libwasmv (needed for test-tube) + uses: actions/checkout@v2 + with: + repository: CosmWasm/wasmvm + path: ./wasmvm + ref: v1.3.0 + + - name: Install libwasmv + run: cd ./wasmvm && make build-rust && cd ../ + - name: Rust Dependencies Cache uses: actions/cache@v3 with: @@ -60,6 +70,9 @@ jobs: - name: Run Integration Tests run: just integration-test + - name: Run Test Tube Integration Tests + run: just test-tube + - name: Combine Test Gas Reports run: cd ci/integration-tests/ && jq -rs 'reduce .[] as $item ({}; . * $item)' gas_reports/*.json > gas_report.json diff --git a/Cargo.lock b/Cargo.lock index 69cd6094e..29caf23d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,18 +30,24 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "assert_matches" @@ -68,18 +74,29 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] name = "async-trait" -version = "0.1.71" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", ] [[package]] @@ -90,9 +107,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", @@ -172,6 +189,29 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bindgen" +version = "0.60.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "clap", + "env_logger 0.9.3", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + [[package]] name = "bip32" version = "0.4.0" @@ -198,9 +238,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "block-buffer" @@ -220,6 +260,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bnum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "845141a4fade3f790628b7daaaa298a25b204fb28907eb54febe5142db6ce653" + [[package]] name = "bootstrap-env" version = "0.2.0" @@ -237,7 +283,7 @@ dependencies = [ "dao-proposal-single", "dao-voting 2.2.0", "dao-voting-cw20-staked", - "env_logger", + "env_logger 0.10.0", "serde", "serde_json", "serde_yaml", @@ -275,9 +321,21 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -285,6 +343,51 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "num-traits", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "config" version = "0.13.3" @@ -306,9 +409,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "core-foundation" @@ -349,7 +452,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "596064e3608349aa302eb68b2df8ed3a66bbb51d9b470dbd9afff70843e44642" dependencies = [ "async-trait", - "cosmrs", + "cosmrs 0.10.0", "regex", "schemars", "serde", @@ -358,6 +461,17 @@ dependencies = [ "tonic 0.8.3", ] +[[package]] +name = "cosmos-sdk-proto" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b42021d8488665b1a0d9748f1f81df7235362d194f44481e2e61bf376b77b4" +dependencies = [ + "prost 0.11.9", + "prost-types", + "tendermint-proto 0.23.9", +] + [[package]] name = "cosmos-sdk-proto" version = "0.15.0" @@ -378,10 +492,31 @@ checksum = "73c9d2043a9e617b0d602fbc0a0ecd621568edbf3a9774890a6d562389bd8e1c" dependencies = [ "prost 0.11.9", "prost-types", - "tendermint-proto 0.32.0", + "tendermint-proto 0.32.2", "tonic 0.9.2", ] +[[package]] +name = "cosmrs" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3903590099dcf1ea580d9353034c9ba1dbf55d1389a5bd2ade98535c3445d1f9" +dependencies = [ + "bip32", + "cosmos-sdk-proto 0.14.0", + "ecdsa", + "eyre", + "getrandom", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "subtle-encoding", + "tendermint 0.23.9", + "tendermint-rpc 0.23.9", + "thiserror", +] + [[package]] name = "cosmrs" version = "0.10.0" @@ -398,16 +533,16 @@ dependencies = [ "serde", "serde_json", "subtle-encoding", - "tendermint", - "tendermint-rpc", + "tendermint 0.26.0", + "tendermint-rpc 0.26.0", "thiserror", ] [[package]] name = "cosmwasm-crypto" -version = "1.2.7" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb64554a91d6a9231127f4355d351130a0b94e663d5d9dc8b3a54ca17d83de49" +checksum = "51dd316b3061747d6f57c1c4a131a5ba2f9446601a9276d05a4d25ab2ce0a7e0" dependencies = [ "digest 0.10.7", "ed25519-zebra", @@ -418,18 +553,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.2.7" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0fb2ce09f41a3dae1a234d56a9988f9aff4c76441cd50ef1ee9a4f20415b028" +checksum = "03b14230c6942a301afb96f601af97ae09966601bd1007067a2c7fe8ffcfe303" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.7" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230e5d1cefae5331db8934763c81b9c871db6a2cd899056a5694fa71d292c815" +checksum = "1027bdd5941b7d4b45bd773b6d88818dcc043e8db68916bfbd5caf971024dbea" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -440,9 +575,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.7" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43dadf7c23406cb28079d69e6cb922c9c29b9157b0fe887e3b79c783b7d4bcb8" +checksum = "b6e069f6e65a9a1f55f8d7423703bed35e9311d029d91b357b17a07010d95cd7" dependencies = [ "proc-macro2", "quote", @@ -451,11 +586,12 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.7" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4337eef8dfaf8572fe6b6b415d6ec25f9308c7bb09f2da63789209fb131363be" +checksum = "c27a06f0f6c35b178563c6b1044245b3f750c4a66d9f6d2b942a6b29ad77d3ae" dependencies = [ "base64 0.13.1", + "bnum", "cosmwasm-crypto", "cosmwasm-derive", "derivative", @@ -466,14 +602,13 @@ dependencies = [ "serde-json-wasm", "sha2 0.10.7", "thiserror", - "uint", ] [[package]] name = "cosmwasm-storage" -version = "1.2.7" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8601d284db8776e39fe99b3416516c5636ca73cef14666b7bb9648ca32c4b89" +checksum = "81854e8f4cb8d6d0ff956de34af56ed70c5a09cb61431dbc854982d10f8886b7" dependencies = [ "cosmwasm-std", "serde", @@ -488,12 +623,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - [[package]] name = "crypto-bigint" version = "0.4.9" @@ -719,15 +848,14 @@ dependencies = [ [[package]] name = "cw-multi-test" version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" +source = "git+https://github.com/JakeHartnell/cw-multi-test.git?branch=bank-supply-support#8c618c8dcc014dacf2cc10627805b36bbc0bfc6e" dependencies = [ "anyhow", "cosmwasm-std", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "derivative", - "itertools", + "itertools 0.10.5", "k256", "prost 0.9.0", "schemars", @@ -896,6 +1024,26 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-tokenfactory-issuer" +version = "2.2.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-multi-test", + "cw-storage-plus 1.1.0", + "cw2 1.1.0", + "osmosis-std 0.16.2", + "osmosis-test-tube", + "prost 0.11.9", + "schemars", + "serde", + "serde_json", + "thiserror", + "token-bindings 0.11.0", +] + [[package]] name = "cw-utils" version = "0.11.1" @@ -1811,6 +1959,7 @@ dependencies = [ "cw-hooks", "cw-multi-test", "cw-proposal-single", + "cw-tokenfactory-issuer", "cw-utils 1.0.1", "cw-vesting", "cw2 1.1.0", @@ -1835,8 +1984,14 @@ dependencies = [ "dao-voting-cw721-roles", "dao-voting-cw721-staked", "dao-voting-native-staked", + "dao-voting-token-factory-staked", + "osmosis-std 0.16.2", + "osmosis-test-tube", "rand", + "serde", + "serde_json", "stake-cw20", + "token-bindings 0.10.3", ] [[package]] @@ -1992,14 +2147,44 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-controllers 1.1.0", + "cw-hooks", + "cw-multi-test", + "cw-paginate-storage 2.2.0", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "cw2 1.1.0", + "dao-dao-macros", + "dao-interface", + "dao-voting 2.2.0", + "thiserror", +] + +[[package]] +name = "dao-voting-token-factory-staked" +version = "2.2.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers 1.1.0", + "cw-hooks", "cw-multi-test", "cw-paginate-storage 2.2.0", "cw-storage-plus 1.1.0", + "cw-tokenfactory-issuer", "cw-utils 1.0.1", "cw2 1.1.0", "dao-dao-macros", "dao-interface", + "dao-testing", + "dao-voting 2.2.0", + "osmosis-std 0.17.0-rc0", + "osmosis-test-tube", + "serde", "thiserror", + "token-bindings 0.11.0", + "token-bindings-test", ] [[package]] @@ -2051,9 +2236,9 @@ checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "ecdsa" @@ -2105,9 +2290,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" @@ -2129,6 +2314,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -2144,24 +2342,24 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.27" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3" +checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a" dependencies = [ "serde", ] [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -2285,7 +2483,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] @@ -2347,6 +2545,12 @@ version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "group" version = "0.12.1" @@ -2417,6 +2621,15 @@ dependencies = [ "http", ] +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.2" @@ -2468,9 +2681,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -2495,7 +2708,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -2625,7 +2838,7 @@ dependencies = [ "dao-voting 2.2.0", "dao-voting-cw20-staked", "dao-voting-cw721-staked", - "env_logger", + "env_logger 0.10.0", "once_cell", "rand", "serde", @@ -2639,7 +2852,7 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.2", "rustix", "windows-sys", ] @@ -2653,11 +2866,20 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" @@ -2707,12 +2929,28 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2721,21 +2959,21 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "matchit" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef" [[package]] name = "memchr" @@ -2798,9 +3036,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", ] @@ -2811,7 +3049,16 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.2", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ "libc", ] @@ -2852,11 +3099,91 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "os_str_bytes" +version = "6.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" + +[[package]] +name = "osmosis-std" +version = "0.16.2" +source = "git+https://github.com/osmosis-labs/osmosis-rust?branch=autobuild-v17.0.0-rc0#36ef03c6d61b860c6bd25cac56bc52144c4e6b9c" +dependencies = [ + "chrono", + "cosmwasm-std", + "osmosis-std-derive 0.16.2 (git+https://github.com/osmosis-labs/osmosis-rust?branch=autobuild-v17.0.0-rc0)", + "prost 0.11.9", + "prost-types", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "osmosis-std" +version = "0.17.0-rc0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b022b748710ecdf1adc6a124c3bef29f17ef05e7fa1260a08889d1d53f9cc5" +dependencies = [ + "chrono", + "cosmwasm-std", + "osmosis-std-derive 0.16.2 (registry+https://github.com/rust-lang/crates.io-index)", + "prost 0.11.9", + "prost-types", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "osmosis-std-derive" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47f0b2f22adb341bb59e5a3a1b464dde033181954bd055b9ae86d6511ba465b" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "prost-types", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "osmosis-std-derive" +version = "0.16.2" +source = "git+https://github.com/osmosis-labs/osmosis-rust?branch=autobuild-v17.0.0-rc0#36ef03c6d61b860c6bd25cac56bc52144c4e6b9c" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "prost-types", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "osmosis-test-tube" +version = "17.0.0-rc0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "977a2b4f088dd704a47e96b5914e28465cfcdb1cb1145a1f9b45c219a9b145c5" +dependencies = [ + "base64 0.13.1", + "bindgen", + "cosmrs 0.9.0", + "cosmwasm-std", + "osmosis-std 0.17.0-rc0", + "prost 0.11.9", + "serde", + "serde_json", + "test-tube", + "thiserror", +] + [[package]] name = "paste" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pathdiff" @@ -2873,6 +3200,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "peg" version = "0.7.0" @@ -2908,9 +3241,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" +checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" dependencies = [ "thiserror", "ucd-trie", @@ -2918,9 +3251,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" +checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" dependencies = [ "pest", "pest_generator", @@ -2928,22 +3261,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" +checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] name = "pest_meta" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" +checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" dependencies = [ "once_cell", "pest", @@ -2952,29 +3285,29 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -3000,9 +3333,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78803b62cbf1f46fde80d7c0e803111524b9877184cfe7c3033659490ac7a7da" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -3046,7 +3379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -3059,7 +3392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -3076,9 +3409,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -3121,9 +3454,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", @@ -3133,9 +3466,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.2" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d3daa6976cffb758ec878f108ba0e062a45b2d6ca3a2cca965338855476caf" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -3144,9 +3477,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "rfc6979" @@ -3221,13 +3554,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustix" -version = "0.38.3" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", @@ -3261,15 +3600,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "same-file" @@ -3339,9 +3678,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3352,9 +3691,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -3362,19 +3701,28 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.169" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd51c3db8f9500d531e6c12dd0fd4ad13d133e9117f5aebac3cdbb8b6d9824b0" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +dependencies = [ + "serde", +] + [[package]] name = "serde-json-wasm" version = "0.5.1" @@ -3386,22 +3734,22 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a16be4fe5320ade08736447e3198294a5ea9a6d44dde6f35f0a5e06859c427a" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.169" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27738cfea0d944ab72c3ed01f3d5f23ec4322af8a1431e40ce630e4c01ea74fd" +checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] @@ -3417,9 +3765,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.100" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -3428,20 +3776,20 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.14" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] name = "serde_yaml" -version = "0.9.22" +version = "0.9.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "452e67b9c20c37fa79df53201dc03839651086ed9bbe92b3ca585ca9fdaa7d85" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" dependencies = [ "indexmap 2.0.0", "itoa", @@ -3560,6 +3908,12 @@ dependencies = [ "keccak", ] +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "signature" version = "1.6.4" @@ -3589,6 +3943,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -3664,10 +4028,10 @@ dependencies = [ ] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" @@ -3697,9 +4061,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.24" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ccaf716a23c35ff908f91c971a86a9a71af5998c1d8f10e828d9f55f68ac00" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -3712,6 +4076,37 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "tendermint" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "467f82178deeebcd357e1273a0c0b77b9a8a0313ef7c07074baebe99d87851f4" +dependencies = [ + "async-trait", + "bytes", + "ed25519", + "ed25519-dalek", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost 0.11.9", + "prost-types", + "ripemd160", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.9.9", + "signature", + "subtle", + "subtle-encoding", + "tendermint-proto 0.23.9", + "time", + "zeroize", +] + [[package]] name = "tendermint" version = "0.26.0" @@ -3743,6 +4138,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "tendermint-config" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42ee0abc27ef5fc34080cce8d43c189950d331631546e7dfb983b6274fa327" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint 0.23.9", + "toml", + "url", +] + [[package]] name = "tendermint-config" version = "0.26.0" @@ -3752,11 +4161,29 @@ dependencies = [ "flex-error", "serde", "serde_json", - "tendermint", + "tendermint 0.26.0", "toml", "url", ] +[[package]] +name = "tendermint-proto" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ce80bf536476db81ecc9ebab834dc329c9c1509a694f211a73858814bfe023" +dependencies = [ + "bytes", + "flex-error", + "num-derive", + "num-traits", + "prost 0.11.9", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + [[package]] name = "tendermint-proto" version = "0.26.0" @@ -3777,9 +4204,9 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.32.0" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23c8ff0e6634eb4c3c4aeed45076dc97dac91aac5501a905a67fa222e165b" +checksum = "c0cec054567d16d85e8c3f6a3139963d1a66d9d3051ed545d31562550e9bcc3d" dependencies = [ "bytes", "flex-error", @@ -3793,6 +4220,39 @@ dependencies = [ "time", ] +[[package]] +name = "tendermint-rpc" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f14aafe3528a0f75e9f3f410b525617b2de16c4b7830a21f717eee62882ec60" +dependencies = [ + "async-trait", + "bytes", + "flex-error", + "futures", + "getrandom", + "http", + "hyper", + "hyper-proxy", + "hyper-rustls", + "peg", + "pin-project", + "serde", + "serde_bytes", + "serde_json", + "subtle-encoding", + "tendermint 0.23.9", + "tendermint-config 0.23.9", + "tendermint-proto 0.23.9", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "uuid", + "walkdir", +] + [[package]] name = "tendermint-rpc" version = "0.26.0" @@ -3815,8 +4275,8 @@ dependencies = [ "serde_json", "subtle", "subtle-encoding", - "tendermint", - "tendermint-config", + "tendermint 0.26.0", + "tendermint-config 0.26.0", "tendermint-proto 0.26.0", "thiserror", "time", @@ -3857,51 +4317,64 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "test-tube" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b1f7cafdf7738331999fb1465d2d3032f08ac61940e1ef4601dbbef21d6a5e" +dependencies = [ + "base64 0.13.1", + "cosmrs 0.9.0", + "cosmwasm-std", + "osmosis-std 0.17.0-rc0", + "prost 0.11.9", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] name = "time" -version = "0.3.23" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" dependencies = [ - "serde", - "time-core", + "libc", + "num_threads", "time-macros", ] -[[package]] -name = "time-core" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" - [[package]] name = "time-macros" -version = "0.2.10" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" -dependencies = [ - "time-core", -] +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tinyvec" @@ -3918,20 +4391,58 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "token-bindings" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "752a7997ebaa191cf3d8436261e449732e65268e552e8bea6133c3b21b48fe36" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "token-bindings" +version = "0.11.0" +source = "git+https://github.com/CosmosContracts/token-bindings.git?rev=0cd084b68172ffc9af29eb37fb915392ce351954#0cd084b68172ffc9af29eb37fb915392ce351954" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "token-bindings-test" +version = "0.11.0" +source = "git+https://github.com/CosmosContracts/token-bindings.git?rev=0cd084b68172ffc9af29eb37fb915392ce351954#0cd084b68172ffc9af29eb37fb915392ce351954" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "itertools 0.11.0", + "schemars", + "serde", + "thiserror", + "token-bindings 0.11.0", +] + [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -3954,7 +4465,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] @@ -4114,7 +4625,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] [[package]] @@ -4154,18 +4665,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - [[package]] name = "unicode-bidi" version = "0.3.13" @@ -4174,9 +4673,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -4189,9 +4688,9 @@ dependencies = [ [[package]] name = "unsafe-libyaml" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" [[package]] name = "untrusted" @@ -4280,7 +4779,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -4302,7 +4801,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4342,6 +4841,17 @@ dependencies = [ "webpki", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4384,9 +4894,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -4399,45 +4909,45 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "wynd-utils" @@ -4477,5 +4987,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.24", + "syn 2.0.29", ] diff --git a/Cargo.toml b/Cargo.toml index e02e21d67..2d5bff497 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,9 @@ members = [ "contracts/voting/*", "packages/*", "test-contracts/*", - "ci/*" - ] -exclude = ["ci/configs/"] + "ci/*", +] +exclude = ["ci/configs/", "wasmvm/libwasmvm"] [workspace.package] edition = "2021" @@ -32,7 +32,7 @@ incremental = false overflow-checks = true [workspace.dependencies] -anyhow = { version = "1.0"} +anyhow = { version = "1.0" } assert_matches = "1.5" cosm-orc = { version = "4.0" } cosm-tome = "0.2" @@ -54,10 +54,14 @@ cw721 = "0.18" cw721-base = "0.18" env_logger = "0.10" once_cell = "1.18" +osmosis-std = { git = "https://github.com/osmosis-labs/osmosis-rust", branch = "autobuild-v17.0.0-rc0" } +osmosis-test-tube = "17.0.0-rc0" proc-macro2 = "1.0" +prost = "0.11" quote = "1.0" rand = "0.8" -serde = { version = "1.0", default-features = false, features = ["derive"]} +schemars = "0.8" +serde = { version = "1.0", default-features = false, features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" sg-std = "3.1.0" @@ -67,6 +71,9 @@ sg-multi-test = "3.1.0" syn = { version = "1.0", features = ["derive"] } test-context = "0.1" thiserror = { version = "1.0" } +# TODO use upstream when PR merged and new release tagged: https://github.com/CosmWasm/cw-multi-test/pull/51 +token-bindings = { git = "https://github.com/CosmosContracts/token-bindings.git", rev = "0cd084b68172ffc9af29eb37fb915392ce351954" } +token-bindings-test = { git = "https://github.com/CosmosContracts/token-bindings.git", rev = "0cd084b68172ffc9af29eb37fb915392ce351954" } wynd-utils = "0.4" # One commit ahead of version 0.3.0. Allows initialization with an @@ -79,6 +86,7 @@ cw-hooks = { path = "./packages/cw-hooks", version = "2.2.0" } cw-wormhole = { path = "./packages/cw-wormhole", version = "2.2.0" } cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.2.0" } cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory", version = "2.2.0" } +cw-tokenfactory-issuer = { path = "./contracts/external/cw-tokenfactory-issuer", version = "*" } cw-vesting = { path = "./contracts/external/cw-vesting", version = "2.2.0" } cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.2.0" } cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "2.2.0" } @@ -107,11 +115,12 @@ dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.2.0" dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "*" } dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.2.0" } dao-voting-native-staked = { path = "./contracts/voting/dao-voting-native-staked", version = "2.2.0" } +dao-voting-token-factory-staked = { path = "./contracts/voting/dao-voting-token-factory-staked", version = "2.2.0" } # v1 dependencies. used for state migrations. cw-core-v1 = { package = "cw-core", version = "0.1.0" } cw-proposal-single-v1 = { package = "cw-proposal-single", version = "0.1.0" } -cw-utils-v1 = {package = "cw-utils", version = "0.13"} +cw-utils-v1 = { package = "cw-utils", version = "0.13" } cw20-stake-external-rewards-v1 = { package = "stake-cw20-external-rewards", version = "0.2.6" } cw20-stake-reward-distributor-v1 = { package = "stake-cw20-reward-distributor", version = "0.1.0" } cw20-stake-v1 = { package = "cw20-stake", version = "0.2.6" } @@ -119,3 +128,7 @@ cw20-staked-balance-voting-v1 = { package = "cw20-staked-balance-voting", versio cw4-voting-v1 = { package = "cw4-voting", version = "0.1.0" } voting-v1 = { package = "dao-voting", version = "0.1.0" } stake-cw20-v03 = { package = "stake-cw20", version = "0.2.6" } + +# TODO remove when upstream PR merged and new release tagged: https://github.com/CosmWasm/cw-multi-test/pull/51 +[patch.crates-io] +cw-multi-test = { git = "https://github.com/JakeHartnell/cw-multi-test.git", branch = "bank-supply-support" } diff --git a/contracts/dao-dao-core/.gitignore b/contracts/dao-dao-core/.gitignore deleted file mode 100644 index dfdaaa6bc..000000000 --- a/contracts/dao-dao-core/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Build results -/target - -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.idea diff --git a/contracts/external/cw-token-swap/.gitignore b/contracts/external/cw-token-swap/.gitignore deleted file mode 100644 index 7016886c2..000000000 --- a/contracts/external/cw-token-swap/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.idea diff --git a/contracts/external/cw-tokenfactory-issuer/.cargo/config b/contracts/external/cw-tokenfactory-issuer/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/external/cw-tokenfactory-issuer/Cargo.toml b/contracts/external/cw-tokenfactory-issuer/Cargo.toml new file mode 100644 index 000000000..10161d182 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/Cargo.toml @@ -0,0 +1,53 @@ +[package] +authors = ["Maurits Bos ", "Sunny Aggarwal ", "Jake Hartnell "] +name = "cw-tokenfactory-issuer" +description = "A CosmWasm contract that issues new Token Factory tokens on supported chains." +version = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } + +# This repo was fork from osmosis-labs/cw-tokenfactory-issuer and does not use +# the default DAO DAO license +license = "Apache-2.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 more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +# use test tube feature to enable test-tube integration tests, for example +# cargo test --features "test-tube" +test-tube = [] +# when writing tests you may wish to enable test-tube as a default feature +# default = ["test-tube"] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cosmwasm-storage = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +osmosis-std = { workspace = true } +prost = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true, default-features = false, features = ["derive"]} +thiserror = { workspace = true } +token-bindings = { workspace = true } + +[dev-dependencies] +cosmwasm-schema = { workspace = true } +cw-multi-test = { workspace = true } +osmosis-test-tube = { workspace = true } +serde_json = { workspace = true } diff --git a/contracts/external/cw-tokenfactory-issuer/LICENSE b/contracts/external/cw-tokenfactory-issuer/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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/external/cw-tokenfactory-issuer/NOTICE b/contracts/external/cw-tokenfactory-issuer/NOTICE new file mode 100644 index 000000000..c8bebf52d --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/NOTICE @@ -0,0 +1,13 @@ +Copyright 2022 Sunny Aggarwal + +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/external/cw-tokenfactory-issuer/README.md b/contracts/external/cw-tokenfactory-issuer/README.md new file mode 100644 index 000000000..fb615da62 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/README.md @@ -0,0 +1,49 @@ +# `cw-tokenfactory-issuer` + +Forked from [osmosis-labs/cw-tokenfactory-issuer](https://github.com/osmosis-labs/cw-tokenfactory-issuer). + +This repo contains a set of contracts that when used in conjunction with the x/tokenfactory module in Osmosis, Juno, and many other chains will enable a centrally issued stablecoin with many features: +- Creating a new Token Factory token or using an existing one +- Granting and revoking allowances for the minting and burning of tokens +- Updating token metadata +- Freezing and unfreezing transfers, with a whitelist to allow some address to continue to transfer +- Force transfering tokens via the contract owner +- Updating the contract owner or Token Factory admin + +It is intended to work on multiple chains supporting Token Factory, and has been tested on Juno Network and Osmosis. + +The contract has an owner (which can be removed or updated via `ExecuteMsg::ChangeContractOwner {}`), but it can delegate capabilities to other acccounts. For example, the owner of a contract can delegate minting allowance of 1000 tokens to a new address. + +The contract is also the admin of the newly created Token Factory denom. For minting and burning, users then interact with the contract using its own ExecuteMsgs which trigger the contract's access control logic, and the contract then dispatches tokenfactory sdk.Msgs from its own contract account. + +NOTE: this contract contains a `SudoMsg::BlockBeforeSend` hook that allows for the blacklisting of specific accounts as well as the freezing of all transfers if necessary. This feature is not enabled on every chain using Token Factory, and so blacklisting and freezing features are disabled if `MsgBeforeSendHook` is not supported. DAOs wishing to leverage these features on chains after support is added can call `ExecuteMsg::SetBeforeSendHook {}`. + +## Instantiation +When instantiating `cw-tokenfactory-issuer`, you can either create a `new_token` or an `existing_token`. + +### Creating a new Token Factory token +To create a new Token Factory token, simply instantiate the contract with a `subdenom`, this will create a new contract as well as a token with a denom formatted as `factory/{contract_address}/{subdenom}`. + +Example instantiate message: + +``` json +{ + "new_token": { + "subdenom": "test" + } +} +``` + +All other updates can be preformed afterwards via this contract's `ExecuteMsg` enum. See `src/msg.rs` for available methods. + +### Using an Existing Token +You can also instantiate this contract with an existing token, however most features will not be available until the previous Token Factory admin transfers admin rights to the instantiated contract and optionally calls `ExecuteMsg::SetBeforeSendHook {}` to enable dependent features. + +Example instantiate message: +``` json +{ + "existing_token": { + "denom": "factory/{contract_address}/{subdenom}" + } +} +``` diff --git a/contracts/external/cw-tokenfactory-issuer/examples/schema.rs b/contracts/external/cw-tokenfactory-issuer/examples/schema.rs new file mode 100644 index 000000000..7f6ffa23c --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/examples/schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use cw_tokenfactory_issuer::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, SudoMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + sudo: SudoMsg, + } +} diff --git a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json new file mode 100644 index 000000000..42ade5b4f --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json @@ -0,0 +1,1379 @@ +{ + "contract_name": "cw-tokenfactory-issuer", + "contract_version": "2.2.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "oneOf": [ + { + "description": "`NewToken` will create a new token when instantiate the contract. Newly created token will have full denom as `factory//`. It will be attached to the contract setup the beforesend listener automatically.", + "type": "object", + "required": [ + "new_token" + ], + "properties": { + "new_token": { + "type": "object", + "required": [ + "subdenom" + ], + "properties": { + "subdenom": { + "description": "component of fulldenom (`factory//`).", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "`ExistingToken` will use already created token. So to set this up, Token Factory admin for the existing token needs trasfer admin over to this contract, and optionally set the `BeforeSendHook` manually.", + "type": "object", + "required": [ + "existing_token" + ], + "properties": { + "existing_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Change the admin of the Token Factory denom itself.", + "type": "object", + "required": [ + "change_token_factory_admin" + ], + "properties": { + "change_token_factory_admin": { + "type": "object", + "required": [ + "new_admin" + ], + "properties": { + "new_admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Change the owner of this contract who is allowed to call privileged methods.", + "type": "object", + "required": [ + "change_contract_owner" + ], + "properties": { + "change_contract_owner": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Set denom metadata. see: https://docs.cosmos.network/main/modules/bank#denom-metadata.", + "type": "object", + "required": [ + "set_denom_metadata" + ], + "properties": { + "set_denom_metadata": { + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/definitions/Metadata" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Grant/revoke mint allowance.", + "type": "object", + "required": [ + "set_minter_allowance" + ], + "properties": { + "set_minter_allowance": { + "type": "object", + "required": [ + "address", + "allowance" + ], + "properties": { + "address": { + "type": "string" + }, + "allowance": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Grant/revoke burn allowance.", + "type": "object", + "required": [ + "set_burner_allowance" + ], + "properties": { + "set_burner_allowance": { + "type": "object", + "required": [ + "address", + "allowance" + ], + "properties": { + "address": { + "type": "string" + }, + "allowance": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Grant/revoke permission to blacklist addresses", + "type": "object", + "required": [ + "set_blacklister" + ], + "properties": { + "set_blacklister": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Grant/revoke permission to blacklist addresses", + "type": "object", + "required": [ + "set_whitelister" + ], + "properties": { + "set_whitelister": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Attempt to SetBeforeSendHook on the token attached to this contract. This will fail if the token already has a SetBeforeSendHook or the chain still does not support it.", + "type": "object", + "required": [ + "set_before_send_hook" + ], + "properties": { + "set_before_send_hook": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Grant/revoke permission to freeze the token", + "type": "object", + "required": [ + "set_freezer" + ], + "properties": { + "set_freezer": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Mint token to address. Mint allowance is required and wiil be deducted after successful mint.", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "amount", + "to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Burn token to address. Burn allowance is required and wiil be deducted after successful burn.", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "amount", + "from_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "from_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Block target address from sending/receiving token attached to this contract tokenfactory's beforesend listener must be set to this contract in order for it to work as intended.", + "type": "object", + "required": [ + "blacklist" + ], + "properties": { + "blacklist": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Whitelist target address to be able to send tokens even if the token is frozen.", + "type": "object", + "required": [ + "whitelist" + ], + "properties": { + "whitelist": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Block every token transfers of the token attached to this contract tokenfactory's beforesend listener must be set to this contract in order for it to work as intended.", + "type": "object", + "required": [ + "freeze" + ], + "properties": { + "freeze": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Force transfer token from one address to another.", + "type": "object", + "required": [ + "force_transfer" + ], + "properties": { + "force_transfer": { + "type": "object", + "required": [ + "amount", + "from_address", + "to_address" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "from_address": { + "type": "string" + }, + "to_address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "DenomUnit": { + "description": "DenomUnit represents a struct that describes a given denomination unit of the basic token.", + "type": "object", + "required": [ + "aliases", + "denom", + "exponent" + ], + "properties": { + "aliases": { + "description": "aliases is a list of string aliases for the given denom", + "type": "array", + "items": { + "type": "string" + } + }, + "denom": { + "description": "denom represents the string name of the given denom unit (e.g uatom).", + "type": "string" + }, + "exponent": { + "description": "exponent represents power of 10 exponent that one must raise the base_denom to in order to equal the given DenomUnit's denom 1 denom = 1^exponent base_denom (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with exponent = 6, thus: 1 atom = 10^6 uatom).", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "Metadata": { + "description": "Metadata represents a struct that describes a basic token.", + "type": "object", + "required": [ + "base", + "denom_units", + "description", + "display", + "name", + "symbol" + ], + "properties": { + "base": { + "description": "base represents the base denom (should be the DenomUnit with exponent = 0).", + "type": "string" + }, + "denom_units": { + "description": "denom_units represents the list of DenomUnit's for a given coin", + "type": "array", + "items": { + "$ref": "#/definitions/DenomUnit" + } + }, + "description": { + "type": "string" + }, + "display": { + "description": "display indicates the suggested denom that should be displayed in clients.", + "type": "string" + }, + "name": { + "description": "name defines the name of the token (eg: Cosmos Atom)\n\nSince: cosmos-sdk 0.43", + "type": "string" + }, + "symbol": { + "description": "symbol is the token symbol usually shown on exchanges (eg: ATOM). This can be the same as the display.\n\nSince: cosmos-sdk 0.43", + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "IsFrozen returns if the entire token transfer functionality is frozen. Response: IsFrozenResponse", + "type": "object", + "required": [ + "is_frozen" + ], + "properties": { + "is_frozen": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Denom returns the token denom that this contract is the admin for. Response: DenomResponse", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Owner returns the owner of the contract. Response: OwnerResponse", + "type": "object", + "required": [ + "owner" + ], + "properties": { + "owner": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allowance returns the allowance of the specified address. Response: AllowanceResponse", + "type": "object", + "required": [ + "burn_allowance" + ], + "properties": { + "burn_allowance": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allowances Enumerates over all allownances. Response: Vec", + "type": "object", + "required": [ + "burn_allowances" + ], + "properties": { + "burn_allowances": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allowance returns the allowance of the specified user. Response: AllowanceResponse", + "type": "object", + "required": [ + "mint_allowance" + ], + "properties": { + "mint_allowance": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allowances Enumerates over all allownances. Response: AllowancesResponse", + "type": "object", + "required": [ + "mint_allowances" + ], + "properties": { + "mint_allowances": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "IsBlacklisted returns wether the user is blacklisted or not. Response: StatusResponse", + "type": "object", + "required": [ + "is_blacklisted" + ], + "properties": { + "is_blacklisted": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Blacklistees enumerates over all addresses on the blacklist. Response: BlacklisteesResponse", + "type": "object", + "required": [ + "blacklistees" + ], + "properties": { + "blacklistees": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "IsBlacklister returns if the addres has blacklister privileges. Response: StatusResponse", + "type": "object", + "required": [ + "is_blacklister" + ], + "properties": { + "is_blacklister": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Blacklisters Enumerates over all the addresses with blacklister privileges. Response: BlacklisterAllowancesResponse", + "type": "object", + "required": [ + "blacklister_allowances" + ], + "properties": { + "blacklister_allowances": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "IsWhitelisted returns wether the user is whitelisted or not. Response: StatusResponse", + "type": "object", + "required": [ + "is_whitelisted" + ], + "properties": { + "is_whitelisted": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Whitelistees enumerates over all addresses on the whitelist. Response: WhitelisteesResponse", + "type": "object", + "required": [ + "whitelistees" + ], + "properties": { + "whitelistees": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "IsWhitelister returns if the addres has whitelister privileges. Response: StatusResponse", + "type": "object", + "required": [ + "is_whitelister" + ], + "properties": { + "is_whitelister": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Whitelisters Enumerates over all the addresses with whitelister privileges. Response: WhitelisterAllowancesResponse", + "type": "object", + "required": [ + "whitelister_allowances" + ], + "properties": { + "whitelister_allowances": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "IsFreezer returns whether the address has freezer status. Response: StatusResponse", + "type": "object", + "required": [ + "is_freezer" + ], + "properties": { + "is_freezer": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "FreezerAllowances enumerates over all freezer addresses. Response: FreezerAllowancesResponse", + "type": "object", + "required": [ + "freezer_allowances" + ], + "properties": { + "freezer_allowances": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "SudoMsg", + "description": "SudoMsg is only exposed for internal Cosmos SDK modules to call. This is showing how we can expose \"admin\" functionality than can not be called by external users or contracts, but only trusted (native/Go) code in the blockchain", + "oneOf": [ + { + "type": "object", + "required": [ + "block_before_send" + ], + "properties": { + "block_before_send": { + "type": "object", + "required": [ + "amount", + "from", + "to" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Coin" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "responses": { + "blacklistees": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BlacklisteesResponse", + "type": "object", + "required": [ + "blacklistees" + ], + "properties": { + "blacklistees": { + "type": "array", + "items": { + "$ref": "#/definitions/StatusInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "StatusInfo": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "blacklister_allowances": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BlacklisterAllowancesResponse", + "type": "object", + "required": [ + "blacklisters" + ], + "properties": { + "blacklisters": { + "type": "array", + "items": { + "$ref": "#/definitions/StatusInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "StatusInfo": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "burn_allowance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowanceResponse", + "type": "object", + "required": [ + "allowance" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "burn_allowances": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowancesResponse", + "type": "object", + "required": [ + "allowances" + ], + "properties": { + "allowances": { + "type": "array", + "items": { + "$ref": "#/definitions/AllowanceInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "AllowanceInfo": { + "type": "object", + "required": [ + "address", + "allowance" + ], + "properties": { + "address": { + "type": "string" + }, + "allowance": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "denom": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DenomResponse", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "freezer_allowances": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "FreezerAllowancesResponse", + "type": "object", + "required": [ + "freezers" + ], + "properties": { + "freezers": { + "type": "array", + "items": { + "$ref": "#/definitions/StatusInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "StatusInfo": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "is_blacklisted": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StatusResponse", + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "is_blacklister": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StatusResponse", + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "is_freezer": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StatusResponse", + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "is_frozen": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "IsFrozenResponse", + "type": "object", + "required": [ + "is_frozen" + ], + "properties": { + "is_frozen": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "is_whitelisted": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StatusResponse", + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "is_whitelister": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StatusResponse", + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "mint_allowance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowanceResponse", + "type": "object", + "required": [ + "allowance" + ], + "properties": { + "allowance": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "mint_allowances": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowancesResponse", + "type": "object", + "required": [ + "allowances" + ], + "properties": { + "allowances": { + "type": "array", + "items": { + "$ref": "#/definitions/AllowanceInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "AllowanceInfo": { + "type": "object", + "required": [ + "address", + "allowance" + ], + "properties": { + "address": { + "type": "string" + }, + "allowance": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "owner": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerResponse", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + }, + "whitelistees": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "WhitelisteesResponse", + "type": "object", + "required": [ + "whitelistees" + ], + "properties": { + "whitelistees": { + "type": "array", + "items": { + "$ref": "#/definitions/StatusInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "StatusInfo": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "whitelister_allowances": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "WhitelisterAllowancesResponse", + "type": "object", + "required": [ + "whitelisters" + ], + "properties": { + "whitelisters": { + "type": "array", + "items": { + "$ref": "#/definitions/StatusInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "StatusInfo": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } + } +} diff --git a/contracts/external/cw-tokenfactory-issuer/src/contract.rs b/contracts/external/cw-tokenfactory-issuer/src/contract.rs new file mode 100644 index 000000000..144486cb2 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/contract.rs @@ -0,0 +1,249 @@ +use std::convert::TryInto; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, SubMsgResult, +}; +use cosmwasm_std::{CosmosMsg, Reply}; +use cw2::set_contract_version; +use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ + MsgCreateDenom, MsgCreateDenomResponse, MsgSetBeforeSendHook, +}; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; + +use crate::error::ContractError; +use crate::execute; +use crate::hooks; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SudoMsg}; +use crate::queries; +use crate::state::{BEFORE_SEND_HOOK_FEATURES_ENABLED, DENOM, IS_FROZEN, OWNER}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:cw-tokenfactory-issuer"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const CREATE_DENOM_REPLY_ID: u64 = 1; +const BEFORE_SEND_HOOK_REPLY_ID: u64 = 2; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result, ContractError> { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + OWNER.save(deps.storage, &info.sender)?; + IS_FROZEN.save(deps.storage, &false)?; + + match msg { + InstantiateMsg::NewToken { subdenom } => { + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute("owner", info.sender) + .add_attribute("subdenom", subdenom.clone()) + .add_submessage( + // create new denom, if denom is created successfully, + // set beforesend listener to this contract on reply + SubMsg::reply_on_success( + >::from(MsgCreateDenom { + sender: env.contract.address.to_string(), + subdenom, + }), + CREATE_DENOM_REPLY_ID, + ), + )) + } + InstantiateMsg::ExistingToken { denom } => { + DENOM.save(deps.storage, &denom)?; + + // BeforeSendHook cannot be set with existing tokens + // features that rely on it are disabled + BEFORE_SEND_HOOK_FEATURES_ENABLED.save(deps.storage, &false)?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute("owner", info.sender) + .add_attribute("denom", denom)) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + match msg { + // Executive Functions + ExecuteMsg::Mint { to_address, amount } => { + execute::mint(deps, env, info, to_address, amount) + } + ExecuteMsg::Burn { + amount, + from_address: address, + } => execute::burn(deps, env, info, amount, address), + ExecuteMsg::Blacklist { address, status } => { + execute::blacklist(deps, info, address, status) + } + ExecuteMsg::Whitelist { address, status } => { + execute::whitelist(deps, info, address, status) + } + ExecuteMsg::Freeze { status } => execute::freeze(deps, info, status), + ExecuteMsg::ForceTransfer { + amount, + from_address, + to_address, + } => execute::force_transfer(deps, env, info, amount, from_address, to_address), + + // Admin functions + ExecuteMsg::ChangeTokenFactoryAdmin { new_admin } => { + execute::change_tokenfactory_admin(deps, info, new_admin) + } + ExecuteMsg::ChangeContractOwner { new_owner } => { + execute::change_contract_owner(deps, info, new_owner) + } + ExecuteMsg::SetMinterAllowance { address, allowance } => { + execute::set_minter(deps, info, address, allowance) + } + ExecuteMsg::SetBurnerAllowance { address, allowance } => { + execute::set_burner(deps, info, address, allowance) + } + ExecuteMsg::SetBeforeSendHook {} => execute::set_before_send_hook(deps, env, info), + ExecuteMsg::SetBlacklister { address, status } => { + execute::set_blacklister(deps, info, address, status) + } + ExecuteMsg::SetWhitelister { address, status } => { + execute::set_whitelister(deps, info, address, status) + } + ExecuteMsg::SetFreezer { address, status } => { + execute::set_freezer(deps, info, address, status) + } + ExecuteMsg::SetDenomMetadata { metadata } => { + execute::set_denom_metadata(deps, env, info, metadata) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn sudo( + deps: DepsMut, + _env: Env, + msg: SudoMsg, +) -> Result { + match msg { + SudoMsg::BlockBeforeSend { from, to, amount } => { + hooks::beforesend_hook(deps, from, to, amount) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::IsFrozen {} => to_binary(&queries::query_is_frozen(deps)?), + QueryMsg::Denom {} => to_binary(&queries::query_denom(deps)?), + QueryMsg::Owner {} => to_binary(&queries::query_owner(deps)?), + QueryMsg::BurnAllowance { address } => { + to_binary(&queries::query_burn_allowance(deps, address)?) + } + QueryMsg::BurnAllowances { start_after, limit } => { + to_binary(&queries::query_burn_allowances(deps, start_after, limit)?) + } + QueryMsg::MintAllowance { address } => { + to_binary(&queries::query_mint_allowance(deps, address)?) + } + QueryMsg::MintAllowances { start_after, limit } => { + to_binary(&queries::query_mint_allowances(deps, start_after, limit)?) + } + QueryMsg::IsBlacklisted { address } => { + to_binary(&queries::query_is_blacklisted(deps, address)?) + } + QueryMsg::Blacklistees { start_after, limit } => { + to_binary(&queries::query_blacklistees(deps, start_after, limit)?) + } + QueryMsg::IsBlacklister { address } => { + to_binary(&queries::query_is_blacklister(deps, address)?) + } + QueryMsg::BlacklisterAllowances { start_after, limit } => to_binary( + &queries::query_blacklister_allowances(deps, start_after, limit)?, + ), + QueryMsg::IsWhitelisted { address } => { + to_binary(&queries::query_is_whitelisted(deps, address)?) + } + QueryMsg::Whitelistees { start_after, limit } => { + to_binary(&queries::query_whitelistees(deps, start_after, limit)?) + } + QueryMsg::IsWhitelister { address } => { + to_binary(&queries::query_is_whitelister(deps, address)?) + } + QueryMsg::WhitelisterAllowances { start_after, limit } => to_binary( + &queries::query_whitelister_allowances(deps, start_after, limit)?, + ), + QueryMsg::IsFreezer { address } => to_binary(&queries::query_is_freezer(deps, address)?), + QueryMsg::FreezerAllowances { start_after, limit } => to_binary( + &queries::query_freezer_allowances(deps, start_after, limit)?, + ), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + deps: DepsMut, + _env: Env, + _msg: MigrateMsg, +) -> Result { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::new().add_attribute("action", "migrate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result, ContractError> { + match msg.id { + CREATE_DENOM_REPLY_ID => { + let MsgCreateDenomResponse { new_token_denom } = msg.result.try_into()?; + DENOM.save(deps.storage, &new_token_denom)?; + + // SetBeforeSendHook to this contract + // this will trigger sudo endpoint before any bank send + // which makes blacklisting / freezing possible + let msg_set_beforesend_hook: CosmosMsg = MsgSetBeforeSendHook { + sender: env.contract.address.to_string(), + denom: new_token_denom.clone(), + cosmwasm_address: env.contract.address.to_string(), + } + .into(); + + Ok(Response::new() + .add_attribute("denom", new_token_denom) + .add_submessage(SubMsg::reply_always( + msg_set_beforesend_hook, + BEFORE_SEND_HOOK_REPLY_ID, + ))) + } + BEFORE_SEND_HOOK_REPLY_ID => match msg.result { + SubMsgResult::Ok(_) => { + // Enable features with BeforeSendHook requirement + BEFORE_SEND_HOOK_FEATURES_ENABLED.save(deps.storage, &true)?; + + Ok(Response::new().add_attribute("extra_features", "enabled")) + } + SubMsgResult::Err(_) => { + // MsgSetBeforeSendHook failed, disable extra features that require it + BEFORE_SEND_HOOK_FEATURES_ENABLED.save(deps.storage, &false)?; + + Ok(Response::new().add_attribute("extra_features", "disabled")) + } + }, + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/external/cw-tokenfactory-issuer/src/error.rs b/contracts/external/cw-tokenfactory-issuer/src/error.rs new file mode 100644 index 000000000..221b3fd0a --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/error.rs @@ -0,0 +1,73 @@ +use cosmwasm_std::{StdError, Uint128}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("The chain you are using does not support MsgBeforeSendHook at this time. Features requiring it are disabled.")] + BeforeSendHookFeaturesDisabled {}, + + #[error("MsgBeforeSendHook is already configured. Features requiring it are already enabled.")] + BeforeSendHookAlreadyEnabled {}, + + #[error("The address '{address}' is blacklisted")] + Blacklisted { address: String }, + + #[error("The contract is frozen for denom {denom:?}")] + ContractFrozen { denom: String }, + + #[error("Invalid subdenom: {subdenom:?}")] + InvalidSubdenom { subdenom: String }, + + #[error("Invalid denom: {denom:?} {message:?}")] + InvalidDenom { denom: String, message: String }, + + #[error("Not enough {denom:?} ({funds:?}) in funds. {needed:?} {denom:?} needed")] + NotEnoughFunds { + denom: String, + funds: u128, + needed: u128, + }, + + #[error("Not enough {action} allowance: attempted to {action} {amount}, but remaining allowance is {allowance}")] + NotEnoughAllowance { + action: String, + amount: Uint128, + allowance: Uint128, + }, + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("amount was zero, must be positive")] + ZeroAmount {}, +} + +impl ContractError { + pub fn not_enough_mint_allowance( + amount: impl Into, + allowance: impl Into, + ) -> ContractError { + ContractError::NotEnoughAllowance { + action: "mint".to_string(), + amount: amount.into(), + allowance: allowance.into(), + } + } + + pub fn not_enough_burn_allowance( + amount: impl Into, + allowance: impl Into, + ) -> ContractError { + ContractError::NotEnoughAllowance { + action: "burn".to_string(), + amount: amount.into(), + allowance: allowance.into(), + } + } +} diff --git a/contracts/external/cw-tokenfactory-issuer/src/execute.rs b/contracts/external/cw-tokenfactory-issuer/src/execute.rs new file mode 100644 index 000000000..916a11d9b --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/execute.rs @@ -0,0 +1,469 @@ +use cosmwasm_std::{coins, BankMsg, Coin, CosmosMsg, DepsMut, Env, MessageInfo, Response, Uint128}; +use osmosis_std::types::cosmos::bank::v1beta1::Metadata; +use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ + MsgBurn, MsgForceTransfer, MsgSetBeforeSendHook, MsgSetDenomMetadata, +}; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; + +use crate::error::ContractError; +use crate::helpers::{ + check_before_send_hook_features_enabled, check_bool_allowance, check_is_contract_owner, +}; +use crate::state::{ + BEFORE_SEND_HOOK_FEATURES_ENABLED, BLACKLISTED_ADDRESSES, BLACKLISTER_ALLOWANCES, + BURNER_ALLOWANCES, DENOM, FREEZER_ALLOWANCES, IS_FROZEN, MINTER_ALLOWANCES, OWNER, + WHITELISTED_ADDRESSES, WHITELISTER_ALLOWANCES, +}; + +pub fn mint( + deps: DepsMut, + env: Env, + info: MessageInfo, + to_address: String, + amount: Uint128, +) -> Result, ContractError> { + // validate that to_address is a valid address + deps.api.addr_validate(&to_address)?; + + // don't allow minting of 0 coins + if amount.is_zero() { + return Err(ContractError::ZeroAmount {}); + } + + // decrease minter allowance + let allowance = MINTER_ALLOWANCES + .may_load(deps.storage, &info.sender)? + .unwrap_or_else(Uint128::zero); + + // if minter allowance goes negative, throw error + let updated_allowance = allowance + .checked_sub(amount) + .map_err(|_| ContractError::not_enough_mint_allowance(amount, allowance))?; + + // if minter allowance goes 0, remove from storage + if updated_allowance.is_zero() { + MINTER_ALLOWANCES.remove(deps.storage, &info.sender); + } else { + MINTER_ALLOWANCES.save(deps.storage, &info.sender, &updated_allowance)?; + } + + // get token denom from contract + let denom = DENOM.load(deps.storage)?; + + // create tokenfactory MsgMint which mints coins to the contract address + let mint_tokens_msg = TokenFactoryMsg::mint_contract_tokens( + denom.clone(), + amount, + env.contract.address.into_string(), + ); + + // send newly minted coins from contract to designated recipient + let send_tokens_msg = BankMsg::Send { + to_address: to_address.clone(), + amount: coins(amount.u128(), denom), + }; + + // dispatch msgs + Ok(Response::new() + .add_message(mint_tokens_msg) + .add_message(send_tokens_msg) + .add_attribute("action", "mint") + .add_attribute("to", to_address) + .add_attribute("amount", amount)) +} + +pub fn burn( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + address: String, +) -> Result, ContractError> { + // don't allow burning of 0 coins + if amount.is_zero() { + return Err(ContractError::ZeroAmount {}); + } + + // decrease burner allowance + let allowance = BURNER_ALLOWANCES + .may_load(deps.storage, &info.sender)? + .unwrap_or_else(Uint128::zero); + + // if burner allowance goes negative, throw error + let updated_allowance = allowance + .checked_sub(amount) + .map_err(|_| ContractError::not_enough_burn_allowance(amount, allowance))?; + + // if burner allowance goes 0, remove from storage + if updated_allowance.is_zero() { + BURNER_ALLOWANCES.remove(deps.storage, &info.sender); + } else { + BURNER_ALLOWANCES.save(deps.storage, &info.sender, &updated_allowance)?; + } + + // get token denom from contract config + let denom = DENOM.load(deps.storage)?; + + // create tokenfactory MsgBurn which burns coins from the contract address + // NOTE: this requires the contract to own the tokens already + let burn_from_address = deps.api.addr_validate(&address)?; + let burn_tokens_msg: cosmwasm_std::CosmosMsg = MsgBurn { + sender: env.contract.address.to_string(), + amount: Some(Coin::new(amount.u128(), denom).into()), + burn_from_address: burn_from_address.to_string(), + } + .into(); + + // dispatch msg + Ok(Response::new() + .add_message(burn_tokens_msg) + .add_attribute("action", "burn") + .add_attribute("from", info.sender) + .add_attribute("amount", amount)) +} + +pub fn change_contract_owner( + deps: DepsMut, + info: MessageInfo, + new_owner: String, +) -> Result, ContractError> { + // Only allow current contract owner to change owner + check_is_contract_owner(deps.as_ref(), info.sender)?; + + // validate that new owner is a valid address + let new_owner_addr = deps.api.addr_validate(&new_owner)?; + + // update the contract owner in the contract config + OWNER.save(deps.storage, &new_owner_addr)?; + + // return OK + Ok(Response::new() + .add_attribute("action", "change_contract_owner") + .add_attribute("new_owner", new_owner)) +} + +pub fn change_tokenfactory_admin( + deps: DepsMut, + info: MessageInfo, + new_admin: String, +) -> Result, ContractError> { + // Only allow current contract owner to change tokenfactory admin + check_is_contract_owner(deps.as_ref(), info.sender)?; + + // validate that the new admin is a valid address + let new_admin_addr = deps.api.addr_validate(&new_admin)?; + + // construct tokenfactory change admin msg + let change_admin_msg = TokenFactoryMsg::ChangeAdmin { + denom: DENOM.load(deps.storage)?, + new_admin_address: new_admin_addr.into(), + }; + + // dispatch change admin msg + Ok(Response::new() + .add_message(change_admin_msg) + .add_attribute("action", "change_tokenfactory_admin") + .add_attribute("new_admin", new_admin)) +} + +pub fn set_denom_metadata( + deps: DepsMut, + env: Env, + info: MessageInfo, + metadata: Metadata, +) -> Result, ContractError> { + // only allow current contract owner to set denom metadata + check_is_contract_owner(deps.as_ref(), info.sender)?; + + Ok(Response::new() + .add_attribute("action", "set_denom_metadata") + .add_message(MsgSetDenomMetadata { + sender: env.contract.address.to_string(), + metadata: Some(metadata), + })) +} + +pub fn set_blacklister( + deps: DepsMut, + info: MessageInfo, + address: String, + status: bool, +) -> Result, ContractError> { + check_before_send_hook_features_enabled(deps.as_ref())?; + + // Only allow current contract owner to set blacklister permission + check_is_contract_owner(deps.as_ref(), info.sender)?; + + let address = deps.api.addr_validate(&address)?; + + // set blacklister status + // NOTE: Does not check if new status is same as old status + // but if status is false, remove if exist to reduce space usage + if status { + BLACKLISTER_ALLOWANCES.save(deps.storage, &address, &status)?; + } else { + BLACKLISTER_ALLOWANCES.remove(deps.storage, &address); + } + + // Return OK + Ok(Response::new() + .add_attribute("action", "set_blacklister") + .add_attribute("blacklister", address) + .add_attribute("status", status.to_string())) +} + +pub fn set_whitelister( + deps: DepsMut, + info: MessageInfo, + address: String, + status: bool, +) -> Result, ContractError> { + check_before_send_hook_features_enabled(deps.as_ref())?; + + // Only allow current contract owner to set blacklister permission + check_is_contract_owner(deps.as_ref(), info.sender)?; + + let address = deps.api.addr_validate(&address)?; + + // set blacklister status + // NOTE: Does not check if new status is same as old status + // but if status is false, remove if exist to reduce space usage + if status { + WHITELISTER_ALLOWANCES.save(deps.storage, &address, &status)?; + } else { + WHITELISTER_ALLOWANCES.remove(deps.storage, &address); + } + + // Return OK + Ok(Response::new() + .add_attribute("action", "set_blacklister") + .add_attribute("blacklister", address) + .add_attribute("status", status.to_string())) +} + +pub fn set_freezer( + deps: DepsMut, + info: MessageInfo, + address: String, + status: bool, +) -> Result, ContractError> { + check_before_send_hook_features_enabled(deps.as_ref())?; + + // Only allow current contract owner to set freezer permission + check_is_contract_owner(deps.as_ref(), info.sender)?; + + let address = deps.api.addr_validate(&address)?; + + // set freezer status + // NOTE: Does not check if new status is same as old status + // but if status is false, remove if exist to reduce space usage + if status { + FREEZER_ALLOWANCES.save(deps.storage, &address, &status)?; + } else { + FREEZER_ALLOWANCES.remove(deps.storage, &address); + } + + // return OK + Ok(Response::new() + .add_attribute("action", "set_freezer") + .add_attribute("freezer", address) + .add_attribute("status", status.to_string())) +} + +pub fn set_before_send_hook( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result, ContractError> { + // Only allow current contract owner + check_is_contract_owner(deps.as_ref(), info.sender)?; + + // Return error if BeforeSendHook already enabled + if BEFORE_SEND_HOOK_FEATURES_ENABLED.load(deps.storage)? { + return Err(ContractError::BeforeSendHookAlreadyEnabled {}); + } + + // Load the Token Factory denom + let denom = DENOM.load(deps.storage)?; + + // SetBeforeSendHook to this contract + // this will trigger sudo endpoint before any bank send + // which makes blacklisting / freezing possible + let msg_set_beforesend_hook: CosmosMsg = MsgSetBeforeSendHook { + sender: env.contract.address.to_string(), + denom: denom.clone(), + cosmwasm_address: env.contract.address.to_string(), + } + .into(); + + // Enable BeforeSendHook features + BEFORE_SEND_HOOK_FEATURES_ENABLED.save(deps.storage, &true)?; + + Ok(Response::new() + .add_attribute("action", "set_before_send_hook") + .add_message(msg_set_beforesend_hook)) +} + +pub fn set_burner( + deps: DepsMut, + info: MessageInfo, + address: String, + allowance: Uint128, +) -> Result, ContractError> { + // Only allow current contract owner to set burner allowance + check_is_contract_owner(deps.as_ref(), info.sender)?; + + // validate that burner is a valid address + let address = deps.api.addr_validate(&address)?; + + // update allowance of burner + // remove key from state if set to 0 + if allowance.is_zero() { + BURNER_ALLOWANCES.remove(deps.storage, &address); + } else { + BURNER_ALLOWANCES.save(deps.storage, &address, &allowance)?; + } + + // return OK + Ok(Response::new() + .add_attribute("action", "set_burner") + .add_attribute("burner", address) + .add_attribute("allowance", allowance)) +} + +pub fn set_minter( + deps: DepsMut, + info: MessageInfo, + address: String, + allowance: Uint128, +) -> Result, ContractError> { + // Only allow current contract owner to set minter allowance + check_is_contract_owner(deps.as_ref(), info.sender)?; + + // validate that minter is a valid address + let address = deps.api.addr_validate(&address)?; + + // update allowance of minter + // remove key from state if set to 0 + if allowance.is_zero() { + MINTER_ALLOWANCES.remove(deps.storage, &address); + } else { + MINTER_ALLOWANCES.save(deps.storage, &address, &allowance)?; + } + + // return OK + Ok(Response::new() + .add_attribute("action", "set_minter") + .add_attribute("minter", address) + .add_attribute("amount", allowance)) +} + +pub fn freeze( + deps: DepsMut, + info: MessageInfo, + status: bool, +) -> Result, ContractError> { + check_before_send_hook_features_enabled(deps.as_ref())?; + + // check to make sure that the sender has freezer permissions + check_bool_allowance(deps.as_ref(), info, FREEZER_ALLOWANCES)?; + + // Update config frozen status + // NOTE: Does not check if new status is same as old status + IS_FROZEN.save(deps.storage, &status)?; + + // return OK + Ok(Response::new() + .add_attribute("action", "freeze") + .add_attribute("status", status.to_string())) +} + +pub fn blacklist( + deps: DepsMut, + info: MessageInfo, + address: String, + status: bool, +) -> Result, ContractError> { + check_before_send_hook_features_enabled(deps.as_ref())?; + + // check to make sure that the sender has blacklister permissions + check_bool_allowance(deps.as_ref(), info, BLACKLISTER_ALLOWANCES)?; + + let address = deps.api.addr_validate(&address)?; + + // update blacklisted status + // validate that blacklisteed is a valid address + // NOTE: Does not check if new status is same as old status + // but if status is false, remove if exist to reduce space usage + if status { + BLACKLISTED_ADDRESSES.save(deps.storage, &address, &status)?; + } else { + BLACKLISTED_ADDRESSES.remove(deps.storage, &address); + } + + // return OK + Ok(Response::new() + .add_attribute("action", "blacklist") + .add_attribute("address", address) + .add_attribute("status", status.to_string())) +} + +pub fn whitelist( + deps: DepsMut, + info: MessageInfo, + address: String, + status: bool, +) -> Result, ContractError> { + check_before_send_hook_features_enabled(deps.as_ref())?; + + // check to make sure that the sender has blacklister permissions + check_bool_allowance(deps.as_ref(), info, WHITELISTER_ALLOWANCES)?; + + let address = deps.api.addr_validate(&address)?; + + // update blacklisted status + // validate that blacklisteed is a valid address + // NOTE: Does not check if new status is same as old status + // but if status is false, remove if exist to reduce space usage + if status { + WHITELISTED_ADDRESSES.save(deps.storage, &address, &status)?; + } else { + WHITELISTED_ADDRESSES.remove(deps.storage, &address); + } + + // return OK + Ok(Response::new() + .add_attribute("action", "blacklist") + .add_attribute("address", address) + .add_attribute("status", status.to_string())) +} + +pub fn force_transfer( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, + from_address: String, + to_address: String, +) -> Result, ContractError> { + // Only allow current contract owner to change owner + check_is_contract_owner(deps.as_ref(), info.sender)?; + + // Load TF denom for this contract + let denom = DENOM.load(deps.storage)?; + + // Force transfer tokens + let force_transfer_msg: CosmosMsg = MsgForceTransfer { + transfer_from_address: from_address.clone(), + transfer_to_address: to_address.clone(), + amount: Some(Coin::new(amount.u128(), denom.clone()).into()), + sender: env.contract.address.to_string(), + } + .into(); + + Ok(Response::new() + .add_attribute("action", "force_transfer") + .add_attribute("from_address", from_address) + .add_attribute("to_address", to_address) + .add_message(force_transfer_msg)) +} diff --git a/contracts/external/cw-tokenfactory-issuer/src/helpers.rs b/contracts/external/cw-tokenfactory-issuer/src/helpers.rs new file mode 100644 index 000000000..5262f9a79 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/helpers.rs @@ -0,0 +1,120 @@ +use crate::state::{ + BEFORE_SEND_HOOK_FEATURES_ENABLED, BLACKLISTED_ADDRESSES, DENOM, IS_FROZEN, OWNER, + WHITELISTED_ADDRESSES, +}; +use crate::ContractError; +use cosmwasm_std::{Addr, Coin, Deps, MessageInfo, Uint128}; +use cw_storage_plus::Map; +use token_bindings::TokenFactoryQuery; + +pub fn check_contract_has_funds( + denom: String, + funds: &[Coin], + amount: Uint128, +) -> Result<(), ContractError> { + if let Some(c) = funds.iter().find(|c| c.denom == denom) { + if c.amount < amount { + Err(ContractError::NotEnoughFunds { + denom, + funds: c.amount.u128(), + needed: amount.u128(), + }) + } else { + Ok(()) + } + } else { + Err(ContractError::NotEnoughFunds { + denom, + funds: 0u128, + needed: amount.u128(), + }) + } +} + +pub fn check_is_contract_owner( + deps: Deps, + sender: Addr, +) -> Result<(), ContractError> { + let owner = OWNER.load(deps.storage)?; + if owner != sender { + Err(ContractError::Unauthorized {}) + } else { + Ok(()) + } +} + +pub fn check_before_send_hook_features_enabled( + deps: Deps, +) -> Result<(), ContractError> { + let enabled = BEFORE_SEND_HOOK_FEATURES_ENABLED.load(deps.storage)?; + if !enabled { + Err(ContractError::BeforeSendHookFeaturesDisabled {}) + } else { + Ok(()) + } +} + +pub fn check_bool_allowance( + deps: Deps, + info: MessageInfo, + allowances: Map<&Addr, bool>, +) -> Result<(), ContractError> { + let res = allowances.load(deps.storage, &info.sender); + match res { + Ok(authorized) => { + if !authorized { + return Err(ContractError::Unauthorized {}); + } + } + Err(error) => { + if let cosmwasm_std::StdError::NotFound { .. } = error { + return Err(ContractError::Unauthorized {}); + } else { + return Err(ContractError::Std(error)); + } + } + } + Ok(()) +} + +pub fn check_is_not_blacklisted( + deps: Deps, + address: String, +) -> Result<(), ContractError> { + let addr = deps.api.addr_validate(&address)?; + if let Some(is_blacklisted) = BLACKLISTED_ADDRESSES.may_load(deps.storage, &addr)? { + if is_blacklisted { + return Err(ContractError::Blacklisted { address }); + } + }; + Ok(()) +} + +pub fn check_is_not_frozen( + deps: Deps, + address: &str, + denom: &str, +) -> Result<(), ContractError> { + let is_frozen = IS_FROZEN.load(deps.storage)?; + let contract_denom = DENOM.load(deps.storage)?; + + // check if issuer is configured to be frozen and the arriving denom is the same + // as this contract denom. + // Denom can be different since setting beforesend listener doesn't check + // contract's denom. + let is_denom_frozen = is_frozen && denom == contract_denom; + if is_denom_frozen { + let addr = deps.api.addr_validate(address)?; + if let Some(is_whitelisted) = WHITELISTED_ADDRESSES.may_load(deps.storage, &addr)? { + if is_whitelisted { + return Ok(()); + } + }; + + return Err(ContractError::ContractFrozen { + denom: contract_denom, + }); + } + + Ok(()) +} diff --git a/contracts/external/cw-tokenfactory-issuer/src/hooks.rs b/contracts/external/cw-tokenfactory-issuer/src/hooks.rs new file mode 100644 index 000000000..af4166d35 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/hooks.rs @@ -0,0 +1,21 @@ +use cosmwasm_std::{Coin, DepsMut, Response}; +use token_bindings::TokenFactoryQuery; + +use crate::error::ContractError; +use crate::helpers::{check_is_not_blacklisted, check_is_not_frozen}; + +pub fn beforesend_hook( + deps: DepsMut, + from: String, + to: String, + coin: Coin, +) -> Result { + // assert that denom of this contract is not frozen + check_is_not_frozen(deps.as_ref(), &from, &coin.denom)?; + + // assert that neither 'from' or 'to' address is blacklisted + check_is_not_blacklisted(deps.as_ref(), from)?; + check_is_not_blacklisted(deps.as_ref(), to)?; + + Ok(Response::new().add_attribute("action", "before_send")) +} diff --git a/contracts/external/cw-tokenfactory-issuer/src/lib.rs b/contracts/external/cw-tokenfactory-issuer/src/lib.rs new file mode 100644 index 000000000..f10d4f9af --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/lib.rs @@ -0,0 +1,10 @@ +pub mod contract; +mod error; +pub mod execute; +pub mod helpers; +pub mod hooks; +pub mod msg; +pub mod queries; +pub mod state; + +pub use crate::error::ContractError; diff --git a/contracts/external/cw-tokenfactory-issuer/src/msg.rs b/contracts/external/cw-tokenfactory-issuer/src/msg.rs new file mode 100644 index 000000000..6d0bc0eea --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/msg.rs @@ -0,0 +1,238 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Coin, Uint128}; +pub use osmosis_std::types::cosmos::bank::v1beta1::{DenomUnit, Metadata}; + +#[cw_serde] +pub enum InstantiateMsg { + /// `NewToken` will create a new token when instantiate the contract. + /// Newly created token will have full denom as `factory//`. + /// It will be attached to the contract setup the beforesend listener automatically. + NewToken { + /// component of fulldenom (`factory//`). + subdenom: String, + }, + /// `ExistingToken` will use already created token. So to set this up, + /// Token Factory admin for the existing token needs trasfer admin over + /// to this contract, and optionally set the `BeforeSendHook` manually. + ExistingToken { denom: String }, +} + +#[cw_serde] +pub struct MigrateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + /// Change the admin of the Token Factory denom itself. + ChangeTokenFactoryAdmin { new_admin: String }, + + /// Change the owner of this contract who is allowed to call privileged methods. + ChangeContractOwner { new_owner: String }, + + /// Set denom metadata. see: https://docs.cosmos.network/main/modules/bank#denom-metadata. + SetDenomMetadata { metadata: Metadata }, + + /// Grant/revoke mint allowance. + SetMinterAllowance { address: String, allowance: Uint128 }, + + /// Grant/revoke burn allowance. + SetBurnerAllowance { address: String, allowance: Uint128 }, + + /// Grant/revoke permission to blacklist addresses + SetBlacklister { address: String, status: bool }, + + /// Grant/revoke permission to blacklist addresses + SetWhitelister { address: String, status: bool }, + + /// Attempt to SetBeforeSendHook on the token attached to this contract. + /// This will fail if the token already has a SetBeforeSendHook or the chain + /// still does not support it. + SetBeforeSendHook {}, + + /// Grant/revoke permission to freeze the token + SetFreezer { address: String, status: bool }, + + /// Mint token to address. Mint allowance is required and wiil be deducted after successful mint. + Mint { to_address: String, amount: Uint128 }, + + /// Burn token to address. Burn allowance is required and wiil be deducted after successful burn. + Burn { + from_address: String, + amount: Uint128, + }, + + /// Block target address from sending/receiving token attached to this contract + /// tokenfactory's beforesend listener must be set to this contract in order for it to work as intended. + Blacklist { address: String, status: bool }, + + /// Whitelist target address to be able to send tokens even if the token is frozen. + Whitelist { address: String, status: bool }, + + /// Block every token transfers of the token attached to this contract + /// tokenfactory's beforesend listener must be set to this contract in order for it to work as intended. + Freeze { status: bool }, + + /// Force transfer token from one address to another. + ForceTransfer { + amount: Uint128, + from_address: String, + to_address: String, + }, +} + +/// SudoMsg is only exposed for internal Cosmos SDK modules to call. +/// This is showing how we can expose "admin" functionality than can not be called by +/// external users or contracts, but only trusted (native/Go) code in the blockchain +#[cw_serde] +pub enum SudoMsg { + BlockBeforeSend { + from: String, + to: String, + amount: Coin, + }, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// IsFrozen returns if the entire token transfer functionality is frozen. Response: IsFrozenResponse + #[returns(IsFrozenResponse)] + IsFrozen {}, + /// Denom returns the token denom that this contract is the admin for. Response: DenomResponse + #[returns(DenomResponse)] + Denom {}, + /// Owner returns the owner of the contract. Response: OwnerResponse + #[returns(OwnerResponse)] + Owner {}, + /// Allowance returns the allowance of the specified address. Response: AllowanceResponse + #[returns(AllowanceResponse)] + BurnAllowance { address: String }, + /// Allowances Enumerates over all allownances. Response: Vec + #[returns(AllowancesResponse)] + BurnAllowances { + start_after: Option, + limit: Option, + }, + /// Allowance returns the allowance of the specified user. Response: AllowanceResponse + #[returns(AllowanceResponse)] + MintAllowance { address: String }, + /// Allowances Enumerates over all allownances. Response: AllowancesResponse + #[returns(AllowancesResponse)] + MintAllowances { + start_after: Option, + limit: Option, + }, + /// IsBlacklisted returns wether the user is blacklisted or not. Response: StatusResponse + #[returns(StatusResponse)] + IsBlacklisted { address: String }, + /// Blacklistees enumerates over all addresses on the blacklist. Response: BlacklisteesResponse + #[returns(BlacklisteesResponse)] + Blacklistees { + start_after: Option, + limit: Option, + }, + /// IsBlacklister returns if the addres has blacklister privileges. Response: StatusResponse + #[returns(StatusResponse)] + IsBlacklister { address: String }, + /// Blacklisters Enumerates over all the addresses with blacklister privileges. Response: BlacklisterAllowancesResponse + #[returns(BlacklisterAllowancesResponse)] + BlacklisterAllowances { + start_after: Option, + limit: Option, + }, + /// IsWhitelisted returns wether the user is whitelisted or not. Response: StatusResponse + #[returns(StatusResponse)] + IsWhitelisted { address: String }, + /// Whitelistees enumerates over all addresses on the whitelist. Response: WhitelisteesResponse + #[returns(WhitelisteesResponse)] + Whitelistees { + start_after: Option, + limit: Option, + }, + /// IsWhitelister returns if the addres has whitelister privileges. Response: StatusResponse + #[returns(StatusResponse)] + IsWhitelister { address: String }, + /// Whitelisters Enumerates over all the addresses with whitelister privileges. Response: WhitelisterAllowancesResponse + #[returns(WhitelisterAllowancesResponse)] + WhitelisterAllowances { + start_after: Option, + limit: Option, + }, + /// IsFreezer returns whether the address has freezer status. Response: StatusResponse + #[returns(StatusResponse)] + IsFreezer { address: String }, + /// FreezerAllowances enumerates over all freezer addresses. Response: FreezerAllowancesResponse + #[returns(FreezerAllowancesResponse)] + FreezerAllowances { + start_after: Option, + limit: Option, + }, +} + +// We define a custom struct for each query response +#[cw_serde] +pub struct IsFrozenResponse { + pub is_frozen: bool, +} + +// We define a custom struct for each query response +#[cw_serde] +pub struct DenomResponse { + pub denom: String, +} + +#[cw_serde] +pub struct OwnerResponse { + pub address: String, +} + +#[cw_serde] +pub struct AllowanceResponse { + pub allowance: Uint128, +} + +#[cw_serde] +pub struct AllowanceInfo { + pub address: String, + pub allowance: Uint128, +} + +#[cw_serde] +pub struct AllowancesResponse { + pub allowances: Vec, +} + +#[cw_serde] +pub struct StatusResponse { + pub status: bool, +} + +#[cw_serde] +pub struct StatusInfo { + pub address: String, + pub status: bool, +} + +#[cw_serde] +pub struct BlacklisteesResponse { + pub blacklistees: Vec, +} + +#[cw_serde] +pub struct BlacklisterAllowancesResponse { + pub blacklisters: Vec, +} + +#[cw_serde] +pub struct WhitelisteesResponse { + pub whitelistees: Vec, +} + +#[cw_serde] +pub struct WhitelisterAllowancesResponse { + pub whitelisters: Vec, +} + +#[cw_serde] +pub struct FreezerAllowancesResponse { + pub freezers: Vec, +} diff --git a/contracts/external/cw-tokenfactory-issuer/src/queries.rs b/contracts/external/cw-tokenfactory-issuer/src/queries.rs new file mode 100644 index 000000000..be3d4f14f --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/queries.rs @@ -0,0 +1,235 @@ +use cosmwasm_std::{Addr, Deps, Order, StdResult, Uint128}; +use cw_storage_plus::{Bound, Map}; +use token_bindings::TokenFactoryQuery; + +use crate::msg::{ + AllowanceInfo, AllowanceResponse, AllowancesResponse, BlacklisteesResponse, + BlacklisterAllowancesResponse, DenomResponse, FreezerAllowancesResponse, IsFrozenResponse, + OwnerResponse, StatusInfo, StatusResponse, WhitelisteesResponse, WhitelisterAllowancesResponse, +}; +use crate::state::{ + BLACKLISTED_ADDRESSES, BLACKLISTER_ALLOWANCES, BURNER_ALLOWANCES, DENOM, FREEZER_ALLOWANCES, + IS_FROZEN, MINTER_ALLOWANCES, OWNER, WHITELISTED_ADDRESSES, WHITELISTER_ALLOWANCES, +}; + +// Default settings for pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +pub fn query_denom(deps: Deps) -> StdResult { + let denom = DENOM.load(deps.storage)?; + Ok(DenomResponse { denom }) +} + +pub fn query_is_frozen(deps: Deps) -> StdResult { + let is_frozen = IS_FROZEN.load(deps.storage)?; + Ok(IsFrozenResponse { is_frozen }) +} + +pub fn query_owner(deps: Deps) -> StdResult { + let owner = OWNER.load(deps.storage)?; + Ok(OwnerResponse { + address: owner.into_string(), + }) +} + +pub fn query_mint_allowance( + deps: Deps, + address: String, +) -> StdResult { + let allowance = MINTER_ALLOWANCES + .may_load(deps.storage, &deps.api.addr_validate(&address)?)? + .unwrap_or_else(Uint128::zero); + Ok(AllowanceResponse { allowance }) +} + +pub fn query_burn_allowance( + deps: Deps, + address: String, +) -> StdResult { + let allowance = BURNER_ALLOWANCES + .may_load(deps.storage, &deps.api.addr_validate(&address)?)? + .unwrap_or_else(Uint128::zero); + Ok(AllowanceResponse { allowance }) +} + +pub fn query_allowances( + deps: Deps, + start_after: Option, + limit: Option, + allowances: Map<&Addr, Uint128>, +) -> StdResult> { + // based on this query written by larry https://github.com/st4k3h0us3/steak-contracts/blob/854c15c8d1a62303b931a785494a6ecd4b6eaf2a/contracts/hub/src/queries.rs#L90 + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let addr: Addr; + let start = match start_after { + None => None, + Some(addr_str) => { + addr = deps.api.addr_validate(&addr_str)?; + Some(Bound::exclusive(&addr)) + } + }; + + // this code is based on the code from mars protocol. https://github.com/mars-protocol/fields-of-mars/blob/598af9ff3de7fa9ce65db713a3125fb442ebcf5c/contracts/martian-field/src/queries.rs#L37 + allowances + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (k, v) = item?; + Ok(AllowanceInfo { + address: k.to_string(), + allowance: v, + }) + }) + .collect() +} + +pub fn query_mint_allowances( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(AllowancesResponse { + allowances: query_allowances(deps, start_after, limit, MINTER_ALLOWANCES)?, + }) +} + +pub fn query_burn_allowances( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(AllowancesResponse { + allowances: query_allowances(deps, start_after, limit, BURNER_ALLOWANCES)?, + }) +} + +pub fn query_is_blacklisted( + deps: Deps, + address: String, +) -> StdResult { + let status = BLACKLISTED_ADDRESSES + .load(deps.storage, &deps.api.addr_validate(&address)?) + .unwrap_or(false); + Ok(StatusResponse { status }) +} + +pub fn query_is_whitelisted( + deps: Deps, + address: String, +) -> StdResult { + let status = WHITELISTED_ADDRESSES + .load(deps.storage, &deps.api.addr_validate(&address)?) + .unwrap_or(false); + Ok(StatusResponse { status }) +} + +pub fn query_status_map( + deps: Deps, + start_after: Option, + limit: Option, + map: Map<&Addr, bool>, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let addr: Addr; + let start = match start_after { + None => None, + Some(addr_str) => { + addr = deps.api.addr_validate(&addr_str)?; + Some(Bound::exclusive(&addr)) + } + }; + + map.range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (address, status) = item?; + Ok(StatusInfo { + address: address.to_string(), + status, + }) + }) + .collect() +} + +pub fn query_blacklistees( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(BlacklisteesResponse { + blacklistees: query_status_map(deps, start_after, limit, BLACKLISTED_ADDRESSES)?, + }) +} + +pub fn query_is_blacklister( + deps: Deps, + address: String, +) -> StdResult { + let status = BLACKLISTER_ALLOWANCES + .load(deps.storage, &deps.api.addr_validate(&address)?) + .unwrap_or(false); + Ok(StatusResponse { status }) +} + +pub fn query_blacklister_allowances( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(BlacklisterAllowancesResponse { + blacklisters: query_status_map(deps, start_after, limit, BLACKLISTER_ALLOWANCES)?, + }) +} + +pub fn query_whitelistees( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(WhitelisteesResponse { + whitelistees: query_status_map(deps, start_after, limit, WHITELISTED_ADDRESSES)?, + }) +} + +pub fn query_is_whitelister( + deps: Deps, + address: String, +) -> StdResult { + let status = WHITELISTER_ALLOWANCES + .load(deps.storage, &deps.api.addr_validate(&address)?) + .unwrap_or(false); + Ok(StatusResponse { status }) +} + +pub fn query_whitelister_allowances( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(WhitelisterAllowancesResponse { + whitelisters: query_status_map(deps, start_after, limit, WHITELISTER_ALLOWANCES)?, + }) +} + +pub fn query_freezer_allowances( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(FreezerAllowancesResponse { + freezers: query_status_map(deps, start_after, limit, FREEZER_ALLOWANCES)?, + }) +} + +pub fn query_is_freezer( + deps: Deps, + address: String, +) -> StdResult { + let status = FREEZER_ALLOWANCES + .load(deps.storage, &deps.api.addr_validate(&address)?) + .unwrap_or(false); + Ok(StatusResponse { status }) +} + +// query inspiration see https://github.com/mars-protocol/fields-of-mars/blob/v1.0.0/packages/fields-of-mars/src/martian_field.rs#L465-L473 diff --git a/contracts/external/cw-tokenfactory-issuer/src/state.rs b/contracts/external/cw-tokenfactory-issuer/src/state.rs new file mode 100644 index 000000000..1f71f4437 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/state.rs @@ -0,0 +1,25 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; + +pub const OWNER: Item = Item::new("owner"); +pub const DENOM: Item = Item::new("denom"); + +/// Blacklisted addresses prevented from transferring tokens +pub const BLACKLISTED_ADDRESSES: Map<&Addr, bool> = Map::new("blacklisted_addresses"); + +/// Addresses allowed to transfer tokens even if the token is frozen +pub const WHITELISTED_ADDRESSES: Map<&Addr, bool> = Map::new("whitelisted_addresses"); + +/// Whether or not features that require MsgBeforeSendHook are enabled +/// Many Token Factory chains do not yet support MsgBeforeSendHook +pub const BEFORE_SEND_HOOK_FEATURES_ENABLED: Item = Item::new("hook_features_enabled"); + +/// Whether or not token transfers are frozen +pub const IS_FROZEN: Item = Item::new("is_frozen"); + +/// Allowances +pub const BLACKLISTER_ALLOWANCES: Map<&Addr, bool> = Map::new("blacklister_allowances"); +pub const WHITELISTER_ALLOWANCES: Map<&Addr, bool> = Map::new("whitelister_allowances"); +pub const BURNER_ALLOWANCES: Map<&Addr, Uint128> = Map::new("burner_allowances"); +pub const FREEZER_ALLOWANCES: Map<&Addr, bool> = Map::new("freezer_allowances"); +pub const MINTER_ALLOWANCES: Map<&Addr, Uint128> = Map::new("minter_allowances"); diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/beforesend.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/beforesend.rs new file mode 100644 index 000000000..9464d8035 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/beforesend.rs @@ -0,0 +1,154 @@ +use cosmwasm_std::coins; +use osmosis_test_tube::{Account, RunnerError}; + +use crate::test_env::TestEnv; + +#[test] +fn before_send_should_not_block_anything_by_default() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + // mint to self + env.cw_tokenfactory_issuer + .set_minter(&owner.address(), 10000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&owner.address(), 10000, owner) + .unwrap(); + + // bank send should pass + env.send_tokens(env.test_accs[1].address(), coins(10000, denom), owner) + .unwrap(); +} + +#[test] +fn before_send_should_block_on_frozen() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + // freeze + env.cw_tokenfactory_issuer + .set_freezer(&owner.address(), true, owner) + .unwrap(); + + env.cw_tokenfactory_issuer.freeze(true, owner).unwrap(); + + // bank send should fail + let err = env + .send_tokens( + env.test_accs[1].address(), + coins(10000, denom.clone()), + owner, + ) + .unwrap_err(); + + assert_eq!(err, RunnerError::ExecuteError { msg: format!("failed to execute message; message index: 0: failed to call before send hook for denom {denom}: The contract is frozen for denom \"{denom}\": execute wasm contract failed") }); +} + +#[test] +fn white_listed_addresses_can_transfer_when_token_frozen() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + let whitelistee = &env.test_accs[1]; + let other = &env.test_accs[2]; + + // freeze + env.cw_tokenfactory_issuer + .set_freezer(&owner.address(), true, owner) + .unwrap(); + env.cw_tokenfactory_issuer.freeze(true, owner).unwrap(); + + // bank send should fail + let err = env + .send_tokens(whitelistee.address(), coins(10000, denom.clone()), owner) + .unwrap_err(); + assert_eq!(err, RunnerError::ExecuteError { msg: format!("failed to execute message; message index: 0: failed to call before send hook for denom {denom}: The contract is frozen for denom \"{denom}\": execute wasm contract failed") }); + + // Whitelist address + env.cw_tokenfactory_issuer + .set_whitelister(&owner.address(), true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .whitelist(&whitelistee.address(), true, owner) + .unwrap(); + + // bank send should pass + env.send_tokens(other.address(), coins(10000, denom.clone()), whitelistee) + .unwrap_err(); + // Non whitelisted address can't transfer, bank send should fail + let err = env + .send_tokens(other.address(), coins(10000, denom.clone()), owner) + .unwrap_err(); + assert_eq!(err, RunnerError::ExecuteError { msg: format!("failed to execute message; message index: 0: failed to call before send hook for denom {denom}: The contract is frozen for denom \"{denom}\": execute wasm contract failed") }); +} + +#[test] +fn before_send_should_block_sending_from_blacklisted_address() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let blacklistee = &env.test_accs[1]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + // mint to blacklistee + env.cw_tokenfactory_issuer + .set_minter(&owner.address(), 20000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&blacklistee.address(), 20000, owner) + .unwrap(); + + // blacklist + env.cw_tokenfactory_issuer + .set_blacklister(&owner.address(), true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .blacklist(&blacklistee.address(), true, owner) + .unwrap(); + + // bank send should fail + let err = env + .send_tokens( + env.test_accs[2].address(), + coins(10000, denom.clone()), + blacklistee, + ) + .unwrap_err(); + + let blacklistee_addr = blacklistee.address(); + assert_eq!(err, RunnerError::ExecuteError { msg: format!("failed to execute message; message index: 0: failed to call before send hook for denom {denom}: The address '{blacklistee_addr}' is blacklisted: execute wasm contract failed") }); +} + +#[test] +fn before_send_should_block_sending_to_blacklisted_address() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let blacklistee = &env.test_accs[1]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + // mint to self + env.cw_tokenfactory_issuer + .set_minter(&owner.address(), 10000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&owner.address(), 10000, owner) + .unwrap(); + + // blacklist + env.cw_tokenfactory_issuer + .set_blacklister(&owner.address(), true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .blacklist(&blacklistee.address(), true, owner) + .unwrap(); + + // bank send should fail + let err = env + .send_tokens(blacklistee.address(), coins(10000, denom.clone()), owner) + .unwrap_err(); + + let blacklistee_addr = blacklistee.address(); + assert_eq!(err, RunnerError::ExecuteError { msg: format!("failed to execute message; message index: 0: failed to call before send hook for denom {denom}: The address '{blacklistee_addr}' is blacklisted: execute wasm contract failed") }); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/blacklist.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/blacklist.rs new file mode 100644 index 000000000..ceb738ba8 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/blacklist.rs @@ -0,0 +1,339 @@ +use cw_tokenfactory_issuer::{msg::StatusInfo, ContractError}; +use osmosis_test_tube::Account; + +use crate::test_env::{ + test_query_over_default_limit, test_query_within_default_limit, TestEnv, TokenfactoryIssuer, +}; + +#[test] +fn set_blacklister_performed_by_contract_owner_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + env.cw_tokenfactory_issuer + .set_blacklister(&non_owner.address(), true, owner) + .unwrap(); + + let is_blacklister = env + .cw_tokenfactory_issuer + .query_is_blacklister(&env.test_accs[1].address()) + .unwrap() + .status; + + assert!(is_blacklister); + + env.cw_tokenfactory_issuer + .set_blacklister(&non_owner.address(), false, owner) + .unwrap(); + + let is_blacklister = env + .cw_tokenfactory_issuer + .query_is_blacklister(&env.test_accs[1].address()) + .unwrap() + .status; + + assert!(!is_blacklister); +} + +#[test] +fn set_blacklister_performed_by_non_contract_owner_should_fail() { + let env = TestEnv::default(); + let non_owner = &env.test_accs[1]; + + let err = env + .cw_tokenfactory_issuer + .set_blacklister(&non_owner.address(), true, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn set_blacklister_to_false_should_remove_it_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let mut sorted_addrs = env + .test_accs + .iter() + .map(|acc| acc.address()) + .collect::>(); + sorted_addrs.sort(); + + env.cw_tokenfactory_issuer + .set_blacklister(&sorted_addrs[0], true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .set_blacklister(&sorted_addrs[1], true, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_blacklister_allowances(None, None) + .unwrap() + .blacklisters, + vec![ + StatusInfo { + address: sorted_addrs[0].clone(), + status: true + }, + StatusInfo { + address: sorted_addrs[1].clone(), + status: true + } + ] + ); + + env.cw_tokenfactory_issuer + .set_blacklister(&sorted_addrs[1], false, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_blacklister_allowances(None, None) + .unwrap() + .blacklisters, + vec![StatusInfo { + address: sorted_addrs[0].clone(), + status: true + },] + ); + + assert!( + !env.cw_tokenfactory_issuer + .query_is_blacklister(&sorted_addrs[1]) + .unwrap() + .status + ); +} + +#[test] +fn blacklist_by_blacklister_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + let blacklistee = &env.test_accs[2]; + + env.cw_tokenfactory_issuer + .set_blacklister(&non_owner.address(), true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .blacklist(&blacklistee.address(), true, non_owner) + .unwrap(); + + // should be blacklisted after set true + assert!( + env.cw_tokenfactory_issuer + .query_is_blacklisted(&blacklistee.address()) + .unwrap() + .status + ); + + env.cw_tokenfactory_issuer + .blacklist(&blacklistee.address(), false, non_owner) + .unwrap(); + + // should be unblacklisted after set false + assert!( + !env.cw_tokenfactory_issuer + .query_is_blacklisted(&blacklistee.address()) + .unwrap() + .status + ); +} + +#[test] +fn blacklist_by_non_blacklister_should_fail() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let blacklistee = &env.test_accs[2]; + let err = env + .cw_tokenfactory_issuer + .blacklist(&blacklistee.address(), true, owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn set_blacklist_to_false_should_remove_it_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let mut sorted_addrs = env + .test_accs + .iter() + .map(|acc| acc.address()) + .collect::>(); + sorted_addrs.sort(); + + env.cw_tokenfactory_issuer + .set_blacklister(&owner.address(), true, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .blacklist(&sorted_addrs[0], true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .blacklist(&sorted_addrs[1], true, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_blacklistees(None, None) + .unwrap() + .blacklistees, + vec![ + StatusInfo { + address: sorted_addrs[0].clone(), + status: true + }, + StatusInfo { + address: sorted_addrs[1].clone(), + status: true + } + ] + ); + + env.cw_tokenfactory_issuer + .blacklist(&sorted_addrs[1], false, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_blacklistees(None, None) + .unwrap() + .blacklistees, + vec![StatusInfo { + address: sorted_addrs[0].clone(), + status: true + },] + ); + + assert!( + !env.cw_tokenfactory_issuer + .query_is_blacklisted(&sorted_addrs[1]) + .unwrap() + .status + ); +} + +// query blacklisters +#[test] +fn query_blacklister_within_default_limit() { + test_query_within_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_blacklister(&allowance.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_blacklister_allowances(start_after, limit) + .unwrap() + .blacklisters + } + }, + ); +} + +#[test] +fn query_blacklister_over_default_limit() { + test_query_over_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_blacklister(&allowance.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_blacklister_allowances(start_after, limit) + .unwrap() + .blacklisters + } + }, + ); +} +// query blacklistees +#[test] +fn query_blacklistee_within_default_limit() { + test_query_within_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |expected_result| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_blacklister(&owner.address(), true, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .blacklist(&expected_result.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_blacklistees(start_after, limit) + .unwrap() + .blacklistees + } + }, + ); +} + +#[test] +fn query_blacklistee_over_default_limit() { + test_query_over_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |expected_result| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_blacklister(&owner.address(), true, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .blacklist(&expected_result.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_blacklistees(start_after, limit) + .unwrap() + .blacklistees + } + }, + ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/burn.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/burn.rs new file mode 100644 index 000000000..66977a1e9 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/burn.rs @@ -0,0 +1,353 @@ +use cosmwasm_std::Uint128; +use cw_tokenfactory_issuer::{msg::AllowanceInfo, ContractError}; +use osmosis_test_tube::{ + osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest, Account, RunnerError, +}; + +use crate::test_env::{ + test_query_over_default_limit, test_query_within_default_limit, TestEnv, TokenfactoryIssuer, +}; + +#[test] +fn set_burner_performed_by_contract_owner_should_work() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + let allowance = 1000000; + env.cw_tokenfactory_issuer + .set_burner(&non_owner.address(), allowance, owner) + .unwrap(); + + let burn_allowance = env + .cw_tokenfactory_issuer + .query_burn_allowance(&env.test_accs[1].address()) + .unwrap() + .allowance; + + assert_eq!(burn_allowance.u128(), allowance); +} + +#[test] +fn set_burner_performed_by_non_contract_owner_should_fail() { + let env = TestEnv::default(); + let non_owner = &env.test_accs[1]; + + let allowance = 1000000; + + let err = env + .cw_tokenfactory_issuer + .set_burner(&non_owner.address(), allowance, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn set_allowance_to_0_should_remove_it_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let burner = &env.test_accs[1]; + + // set allowance to some value + let allowance = 1000000; + env.cw_tokenfactory_issuer + .set_burner(&burner.address(), allowance, owner) + .unwrap(); + + // set allowance to 0 + env.cw_tokenfactory_issuer + .set_burner(&burner.address(), 0, owner) + .unwrap(); + + // check if key for the minter address is removed + assert_eq!( + env.cw_tokenfactory_issuer + .query_burn_allowances(None, None) + .unwrap() + .allowances, + vec![] + ); +} + +#[test] +fn used_up_allowance_should_be_removed_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let burner = &env.test_accs[1]; + + // set allowance to some value + let allowance = 1000000; + env.cw_tokenfactory_issuer + .set_minter(&burner.address(), allowance, owner) + .unwrap(); + + // mint the whole allowance to be burned the same amount later + env.cw_tokenfactory_issuer + .mint(&burner.address(), allowance, burner) + .unwrap(); + + env.cw_tokenfactory_issuer + .set_burner(&burner.address(), allowance, owner) + .unwrap(); + + // use all allowance + env.cw_tokenfactory_issuer + .burn(&burner.address(), allowance, burner) + .unwrap(); + + // check if key for the burner address is removed + assert_eq!( + env.cw_tokenfactory_issuer + .query_burn_allowances(None, None) + .unwrap() + .allowances, + vec![] + ); +} + +#[test] +fn burn_whole_balance_but_less_than_or_eq_allowance_should_work_and_deduct_allowance() { + let cases = vec![ + (u128::MAX, u128::MAX), + (u128::MAX, u128::MAX - 1), + (u128::MAX, 1), + (2, 1), + (1, 1), + ]; + + cases.into_iter().for_each(|(allowance, burn_amount)| { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + let burner = &env.test_accs[1]; + let burn_to = &env.test_accs[2]; + + // mint + env.cw_tokenfactory_issuer + .set_minter(&burner.address(), allowance, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .mint(&burn_to.address(), burn_amount, burner) + .unwrap(); + + // burn + env.cw_tokenfactory_issuer + .set_burner(&burner.address(), allowance, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .burn(&burn_to.address(), burn_amount, burner) + .unwrap(); + + // check if allowance is deducted properly + let resulted_allowance = env + .cw_tokenfactory_issuer + .query_burn_allowance(&burner.address()) + .unwrap() + .allowance + .u128(); + + assert_eq!(resulted_allowance, allowance - burn_amount); + + // check if resulted balance is 0 + let amount = env + .bank() + .query_balance(&QueryBalanceRequest { + address: burn_to.address(), + denom, + }) + .unwrap() + .balance + .unwrap() + .amount; + + assert_eq!(amount, "0"); + }); +} + +#[test] +fn burn_more_than_balance_should_fail_and_not_deduct_allowance() { + let cases = vec![(u128::MAX - 1, u128::MAX), (1, 2)]; + + cases.into_iter().for_each(|(balance, burn_amount)| { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + let burner = &env.test_accs[1]; + let burn_from = &env.test_accs[2]; + + let allowance = burn_amount; + + // mint + env.cw_tokenfactory_issuer + .set_minter(&burner.address(), balance, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .mint(&burn_from.address(), balance, burner) + .unwrap(); + + // burn + env.cw_tokenfactory_issuer + .set_burner(&burner.address(), allowance, owner) + .unwrap(); + + let err = env + .cw_tokenfactory_issuer + .burn(&burn_from.address(), allowance, burner) + .unwrap_err(); + + assert_eq!( + err, + RunnerError::ExecuteError { + msg: format!("failed to execute message; message index: 0: dispatch: submessages: {balance}{denom} is smaller than {burn_amount}{denom}: insufficient funds") + } + ); + + // check if allowance stays the same + let resulted_allowance = env + .cw_tokenfactory_issuer + .query_burn_allowance(&burner.address()) + .unwrap() + .allowance + .u128(); + + assert_eq!(resulted_allowance, allowance); + }); +} + +#[test] +fn burn_over_allowance_should_fail_and_not_deduct_allowance() { + let cases = vec![(u128::MAX - 1, u128::MAX), (0, 1)]; + + cases.into_iter().for_each(|(allowance, burn_amount)| { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let burner = &env.test_accs[1]; + let burn_from = &env.test_accs[2]; + + env.cw_tokenfactory_issuer + .set_burner(&burner.address(), allowance, owner) + .unwrap(); + + let err = env + .cw_tokenfactory_issuer + .burn(&burn_from.address(), burn_amount, burner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::not_enough_burn_allowance( + burn_amount, + allowance + )) + ); + + // check if allowance stays the same + let resulted_allowance = env + .cw_tokenfactory_issuer + .query_burn_allowance(&burner.address()) + .unwrap() + .allowance + .u128(); + + assert_eq!(resulted_allowance, allowance); + }); +} + +#[test] +fn burn_0_should_fail_and_not_deduct_allowance() { + let cases = vec![(u128::MAX, 0), (0, 0)]; + + cases.into_iter().for_each(|(allowance, burn_amount)| { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let burner = &env.test_accs[1]; + let burn_to = &env.test_accs[2]; + + env.cw_tokenfactory_issuer + .set_burner(&burner.address(), allowance, owner) + .unwrap(); + + let err = env + .cw_tokenfactory_issuer + .burn(&burn_to.address(), burn_amount, burner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::ZeroAmount {}) + ); + + // check if allowance stays the same + let resulted_allowance = env + .cw_tokenfactory_issuer + .query_burn_allowance(&burner.address()) + .unwrap() + .allowance + .u128(); + + assert_eq!(resulted_allowance, allowance); + }); +} + +#[test] +fn test_query_burn_allowances_within_default_limit() { + test_query_within_default_limit::( + |(i, addr)| AllowanceInfo { + address: addr.to_string(), + allowance: Uint128::from((i as u128 + 1) * 10000u128), // generate distincted allowance + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_burner(&allowance.address, allowance.allowance.u128(), owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_burn_allowances(start_after, limit) + .unwrap() + .allowances + } + }, + ); +} + +#[test] +fn test_query_burn_allowance_over_default_limit() { + test_query_over_default_limit::( + |(i, addr)| AllowanceInfo { + address: addr.to_string(), + allowance: Uint128::from((i as u128 + 1) * 10000u128), // generate distincted allowance + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_burner(&allowance.address, allowance.allowance.u128(), owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_burn_allowances(start_after, limit) + .unwrap() + .allowances + } + }, + ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/contract_owner.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/contract_owner.rs new file mode 100644 index 000000000..3140fac06 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/contract_owner.rs @@ -0,0 +1,49 @@ +use cw_tokenfactory_issuer::ContractError; +use osmosis_test_tube::Account; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn change_owner_by_owner_should_work() { + let env = TestEnv::default(); + let prev_owner = &env.test_accs[0]; + let new_owner = &env.test_accs[1]; + + assert_eq!( + prev_owner.address(), + env.cw_tokenfactory_issuer.query_owner().unwrap().address + ); + + env.cw_tokenfactory_issuer + .change_contract_owner(&new_owner.address(), prev_owner) + .unwrap(); + + assert_eq!( + new_owner.address(), + env.cw_tokenfactory_issuer.query_owner().unwrap().address + ); + + // previous owner should not be able to execute owner action + assert_eq!( + env.cw_tokenfactory_issuer + .change_contract_owner(&prev_owner.address(), prev_owner) + .unwrap_err(), + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn change_owner_by_non_owner_should_fail() { + let env = TestEnv::default(); + let new_owner = &env.test_accs[1]; + + let err = env + .cw_tokenfactory_issuer + .change_contract_owner(&new_owner.address(), new_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/denom_metadata.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/denom_metadata.rs new file mode 100644 index 000000000..6076d200b --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/denom_metadata.rs @@ -0,0 +1,143 @@ +use cw_tokenfactory_issuer::{msg::InstantiateMsg, ContractError}; +// use osmosis_test_tube::osmosis_std::types::cosmos::bank::v1beta1::{ +// DenomUnit, Metadata, QueryDenomMetadataRequest, +// }; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn set_denom_metadata_by_contract_owner_should_work() { + let subdenom = "usthb".to_string(); + + // set no metadata + let env = TestEnv::new(InstantiateMsg::NewToken { subdenom }, 0).unwrap(); + let owner = &env.test_accs[0]; + + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + let metadata = cw_tokenfactory_issuer::msg::Metadata { + base: denom.clone(), + description: "Thai Baht stablecoin".to_string(), + denom_units: vec![ + cw_tokenfactory_issuer::msg::DenomUnit { + denom: denom.clone(), + exponent: 0, + aliases: vec!["sthb".to_string()], + }, + cw_tokenfactory_issuer::msg::DenomUnit { + denom: "sthb".to_string(), + exponent: 6, + aliases: vec![], + }, + ], + display: "sthb".to_string(), + name: "Stable Thai Baht".to_string(), + symbol: "STHB".to_string(), + }; + env.cw_tokenfactory_issuer + .set_denom_metadata(metadata, owner) + .unwrap(); +} + +#[test] +fn set_denom_metadata_by_contract_non_owner_should_fail() { + let subdenom = "usthb".to_string(); + + // set no metadata + let env = TestEnv::new(InstantiateMsg::NewToken { subdenom }, 0).unwrap(); + let non_owner = &env.test_accs[1]; + + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + let metadata = cw_tokenfactory_issuer::msg::Metadata { + base: denom.clone(), + description: "Thai Baht stablecoin".to_string(), + denom_units: vec![ + cw_tokenfactory_issuer::msg::DenomUnit { + denom, + exponent: 0, + aliases: vec!["sthb".to_string()], + }, + cw_tokenfactory_issuer::msg::DenomUnit { + denom: "sthb".to_string(), + exponent: 6, + aliases: vec![], + }, + ], + display: "sthb".to_string(), + name: "Stable Thai Baht".to_string(), + symbol: "STHB".to_string(), + }; + + // set denom metadata + let err = env + .cw_tokenfactory_issuer + .set_denom_metadata(metadata, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ) +} + +#[test] +fn set_denom_metadata_with_base_denom_unit_should_overides_default_base_denom_unit() { + let subdenom = "usthb".to_string(); + + // set no metadata + let env = TestEnv::new(InstantiateMsg::NewToken { subdenom }, 0).unwrap(); + let owner = &env.test_accs[0]; + + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + let metadata = cw_tokenfactory_issuer::msg::Metadata { + base: denom.clone(), + description: "Thai Baht stablecoin".to_string(), + denom_units: vec![ + cw_tokenfactory_issuer::msg::DenomUnit { + denom: denom.clone(), + exponent: 0, + aliases: vec!["sthb".to_string()], + }, + cw_tokenfactory_issuer::msg::DenomUnit { + denom: "sthb".to_string(), + exponent: 6, + aliases: vec![], + }, + ], + display: "sthb".to_string(), + name: "Stable Thai Baht".to_string(), + symbol: "STHB".to_string(), + }; + + // set denom metadata + env.cw_tokenfactory_issuer + .set_denom_metadata(metadata.clone(), owner) + .unwrap(); + + // should update metadata + + // assert_eq!( + // env.bank() + // .query_denom_metadata(&QueryDenomMetadataRequest { + // denom: denom.clone() + // }) + // .unwrap() + // .metadata + // .unwrap(), + // Metadata { + // description: metadata.description, + // denom_units: metadata + // .denom_units + // .into_iter() + // .map(|d| DenomUnit { + // denom: d.denom, + // exponent: d.exponent, + // aliases: d.aliases, + // }) + // .collect(), + // base: denom, + // display: metadata.display, + // name: metadata.name, + // symbol: metadata.symbol, + // } + // ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/force_transfer.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/force_transfer.rs new file mode 100644 index 000000000..db8c4c5a4 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/force_transfer.rs @@ -0,0 +1,52 @@ +use cosmwasm_std::Uint128; +use cw_tokenfactory_issuer::ContractError; +use osmosis_test_tube::Account; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn test_force_transfer() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + // Give owner permission to mint tokens + let allowance = 100000000000; + env.cw_tokenfactory_issuer + .set_minter(&owner.address(), allowance, owner) + .unwrap(); + + // Mint tokens for owner and non_owner + env.cw_tokenfactory_issuer + .mint(&owner.address(), 10000000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&non_owner.address(), 10000000, owner) + .unwrap(); + + // Non-owner cannot force transfer tokens + let err = env + .cw_tokenfactory_issuer + .force_transfer( + non_owner, + Uint128::new(10000), + owner.address(), + non_owner.address(), + ) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); + + // Owner can force transfer tokens + env.cw_tokenfactory_issuer + .force_transfer( + owner, + Uint128::new(10000), + non_owner.address(), + owner.address(), + ) + .unwrap(); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/freeze.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/freeze.rs new file mode 100644 index 000000000..c72b9cd46 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/freeze.rs @@ -0,0 +1,206 @@ +use cw_tokenfactory_issuer::{msg::StatusInfo, ContractError}; +use osmosis_test_tube::Account; + +use crate::test_env::{ + test_query_over_default_limit, test_query_within_default_limit, TestEnv, TokenfactoryIssuer, +}; + +#[test] +fn set_freezer_performed_by_contract_owner_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + env.cw_tokenfactory_issuer + .set_freezer(&non_owner.address(), true, owner) + .unwrap(); + + let is_freezer = env + .cw_tokenfactory_issuer + .query_is_freezer(&env.test_accs[1].address()) + .unwrap() + .status; + + assert!(is_freezer); + + env.cw_tokenfactory_issuer + .set_freezer(&non_owner.address(), false, owner) + .unwrap(); + + let is_freezer = env + .cw_tokenfactory_issuer + .query_is_freezer(&env.test_accs[1].address()) + .unwrap() + .status; + + assert!(!is_freezer); +} + +#[test] +fn set_freezer_performed_by_non_contract_owner_should_fail() { + let env = TestEnv::default(); + let non_owner = &env.test_accs[1]; + + let err = env + .cw_tokenfactory_issuer + .set_freezer(&non_owner.address(), true, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn set_freezer_to_false_should_remove_it_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let mut sorted_addrs = env + .test_accs + .iter() + .map(|acc| acc.address()) + .collect::>(); + sorted_addrs.sort(); + + env.cw_tokenfactory_issuer + .set_freezer(&sorted_addrs[0], true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .set_freezer(&sorted_addrs[1], true, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_freezer_allowances(None, None) + .unwrap() + .freezers, + vec![ + StatusInfo { + address: sorted_addrs[0].clone(), + status: true + }, + StatusInfo { + address: sorted_addrs[1].clone(), + status: true + } + ] + ); + + env.cw_tokenfactory_issuer + .set_freezer(&sorted_addrs[1], false, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_freezer_allowances(None, None) + .unwrap() + .freezers, + vec![StatusInfo { + address: sorted_addrs[0].clone(), + status: true + },] + ); + + assert!( + !env.cw_tokenfactory_issuer + .query_is_freezer(&sorted_addrs[1]) + .unwrap() + .status + ); +} + +#[test] +fn freeze_by_freezer_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + env.cw_tokenfactory_issuer + .set_freezer(&non_owner.address(), true, owner) + .unwrap(); + env.cw_tokenfactory_issuer.freeze(true, non_owner).unwrap(); + + // should be frozen after set true + assert!( + env.cw_tokenfactory_issuer + .query_is_frozen() + .unwrap() + .is_frozen + ); + + env.cw_tokenfactory_issuer.freeze(false, non_owner).unwrap(); + + // should be unfrozen after set false + assert!( + !env.cw_tokenfactory_issuer + .query_is_frozen() + .unwrap() + .is_frozen + ); +} + +#[test] +fn freeze_by_non_freezer_should_fail() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let err = env.cw_tokenfactory_issuer.freeze(true, owner).unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn query_freezer_within_default_limit() { + test_query_within_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_freezer(&allowance.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_freezer_allowances(start_after, limit) + .unwrap() + .freezers + } + }, + ); +} + +#[test] +fn query_freezer_over_default_limit() { + test_query_over_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_freezer(&allowance.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_freezer_allowances(start_after, limit) + .unwrap() + .freezers + } + }, + ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/instantiate.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/instantiate.rs new file mode 100644 index 000000000..786653642 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/instantiate.rs @@ -0,0 +1,132 @@ +use cosmwasm_std::coins; +use cw_tokenfactory_issuer::msg::InstantiateMsg; +use osmosis_test_tube::{Account, OsmosisTestApp, RunnerError}; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn instantiate_with_new_token_shoud_set_initial_state_correctly() { + let subdenom = "uthb".to_string(); + let env = TestEnv::new( + InstantiateMsg::NewToken { + subdenom: subdenom.clone(), + }, + 0, + ) + .unwrap(); + + let owner = &env.test_accs[0]; + + // check tokenfactory's token admin + let denom = format!( + "factory/{}/{}", + env.cw_tokenfactory_issuer.contract_addr, subdenom + ); + + assert_eq!( + env.token_admin(&denom), + env.cw_tokenfactory_issuer.contract_addr, + "token admin must be tokenfactory-issuer contract" + ); + + // check initial contract state + let contract_denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + assert_eq!( + denom, contract_denom, + "denom stored in contract must be `factory//`" + ); + + let is_frozen = env + .cw_tokenfactory_issuer + .query_is_frozen() + .unwrap() + .is_frozen; + assert!(!is_frozen, "newly instantiated contract must not be frozen"); + + let owner_addr = env.cw_tokenfactory_issuer.query_owner().unwrap().address; + assert_eq!( + owner_addr, + owner.address(), + "owner must be contract instantiate tx signer" + ); +} + +#[test] +fn instantiate_with_new_token_shoud_set_hook_correctly() { + let subdenom = "uthb".to_string(); + let env = TestEnv::new( + InstantiateMsg::NewToken { + subdenom: subdenom.clone(), + }, + 0, + ) + .unwrap(); + + let owner = &env.test_accs[0]; + + let denom = format!( + "factory/{}/{}", + env.cw_tokenfactory_issuer.contract_addr, subdenom + ); + + // freeze + env.cw_tokenfactory_issuer + .set_freezer(&owner.address(), true, owner) + .unwrap(); + + env.cw_tokenfactory_issuer.freeze(true, owner).unwrap(); + + // bank send should fail + let err = env + .send_tokens( + env.test_accs[1].address(), + coins(10000, denom.clone()), + owner, + ) + .unwrap_err(); + + assert_eq!(err, RunnerError::ExecuteError { msg: format!("failed to execute message; message index: 0: failed to call before send hook for denom {denom}: The contract is frozen for denom \"{denom}\": execute wasm contract failed") }); +} + +#[test] +fn instantiate_with_existing_token_should_set_initial_state_correctly() { + let app = OsmosisTestApp::new(); + let test_accs = TestEnv::create_default_test_accs(&app, 1); + + let denom = format!("factory/{}/uthb", test_accs[0].address()); + let cw_tokenfactory_issuer = TokenfactoryIssuer::new( + app, + &InstantiateMsg::ExistingToken { + denom: denom.clone(), + }, + &test_accs[0], + ) + .unwrap(); + + let env = TestEnv { + cw_tokenfactory_issuer, + test_accs, + }; + + let owner = &env.test_accs[0]; + + let contract_denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + assert_eq!( + denom, contract_denom, + "denom stored in contract must be `factory//`" + ); + + let is_frozen = env + .cw_tokenfactory_issuer + .query_is_frozen() + .unwrap() + .is_frozen; + assert!(!is_frozen, "newly instantiated contract must not be frozen"); + + let owner_addr = env.cw_tokenfactory_issuer.query_owner().unwrap().address; + assert_eq!( + owner_addr, + owner.address(), + "owner must be contract instantiate tx signer" + ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs new file mode 100644 index 000000000..cc632bc38 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs @@ -0,0 +1,279 @@ +use cosmwasm_std::Uint128; +use cw_tokenfactory_issuer::{msg::AllowanceInfo, ContractError}; +use osmosis_test_tube::{osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest, Account}; + +use crate::test_env::{ + test_query_over_default_limit, test_query_within_default_limit, TestEnv, TokenfactoryIssuer, +}; + +#[test] +fn set_minter_performed_by_contract_owner_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + let allowance = 1000000; + env.cw_tokenfactory_issuer + .set_minter(&non_owner.address(), allowance, owner) + .unwrap(); + + let mint_allowance = env + .cw_tokenfactory_issuer + .query_mint_allowance(&env.test_accs[1].address()) + .unwrap() + .allowance; + + assert_eq!(mint_allowance.u128(), allowance); +} + +#[test] +fn set_minter_performed_by_non_contract_owner_should_fail() { + let env = TestEnv::default(); + let non_owner = &env.test_accs[1]; + + let allowance = 1000000; + + let err = env + .cw_tokenfactory_issuer + .set_minter(&non_owner.address(), allowance, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn set_allowance_to_0_should_remove_it_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let minter = &env.test_accs[1]; + + // set allowance to some value + let allowance = 1000000; + env.cw_tokenfactory_issuer + .set_minter(&minter.address(), allowance, owner) + .unwrap(); + + // set allowance to 0 + env.cw_tokenfactory_issuer + .set_minter(&minter.address(), 0, owner) + .unwrap(); + + // check if key for the minter address is removed + assert_eq!( + env.cw_tokenfactory_issuer + .query_mint_allowances(None, None) + .unwrap() + .allowances, + vec![] + ); +} + +#[test] +fn used_up_allowance_should_be_removed_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let minter = &env.test_accs[1]; + + // set allowance to some value + let allowance = 1000000; + env.cw_tokenfactory_issuer + .set_minter(&minter.address(), allowance, owner) + .unwrap(); + + // use all allowance + env.cw_tokenfactory_issuer + .mint(&minter.address(), allowance, minter) + .unwrap(); + + // check if key for the minter address is removed + assert_eq!( + env.cw_tokenfactory_issuer + .query_mint_allowances(None, None) + .unwrap() + .allowances, + vec![] + ); +} + +#[test] +fn mint_less_than_or_eq_allowance_should_pass_and_deduct_allowance() { + let cases = vec![ + (u128::MAX, u128::MAX), + (u128::MAX, u128::MAX - 1), + (u128::MAX, 1), + (2, 1), + (1, 1), + ]; + + cases.into_iter().for_each(|(allowance, mint_amount)| { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + let minter = &env.test_accs[1]; + let mint_to = &env.test_accs[2]; + + env.cw_tokenfactory_issuer + .set_minter(&minter.address(), allowance, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .mint(&mint_to.address(), mint_amount, minter) + .unwrap(); + + // check if allowance is deducted properly + let resulted_allowance = env + .cw_tokenfactory_issuer + .query_mint_allowance(&minter.address()) + .unwrap() + .allowance + .u128(); + + assert_eq!(resulted_allowance, allowance - mint_amount); + + let amount = env + .bank() + .query_balance(&QueryBalanceRequest { + address: mint_to.address(), + denom, + }) + .unwrap() + .balance + .unwrap() + .amount; + + assert_eq!(amount, mint_amount.to_string()); + }); +} + +#[test] +fn mint_over_allowance_should_fail_and_not_deduct_allowance() { + let cases = vec![(u128::MAX - 1, u128::MAX), (0, 1)]; + + cases.into_iter().for_each(|(allowance, mint_amount)| { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let minter = &env.test_accs[1]; + let mint_to = &env.test_accs[2]; + + env.cw_tokenfactory_issuer + .set_minter(&minter.address(), allowance, owner) + .unwrap(); + + let err = env + .cw_tokenfactory_issuer + .mint(&mint_to.address(), mint_amount, minter) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::not_enough_mint_allowance( + mint_amount, + allowance + )) + ); + + // check if allowance stays the same + let resulted_allowance = env + .cw_tokenfactory_issuer + .query_mint_allowance(&minter.address()) + .unwrap() + .allowance + .u128(); + + assert_eq!(resulted_allowance, allowance); + }); +} + +#[test] +fn mint_0_should_fail_and_not_deduct_allowance() { + let cases = vec![(u128::MAX, 0), (0, 0)]; + + cases.into_iter().for_each(|(allowance, mint_amount)| { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let minter = &env.test_accs[1]; + let mint_to = &env.test_accs[2]; + + env.cw_tokenfactory_issuer + .set_minter(&minter.address(), allowance, owner) + .unwrap(); + + let err = env + .cw_tokenfactory_issuer + .mint(&mint_to.address(), mint_amount, minter) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::ZeroAmount {}) + ); + + // check if allowance stays the same + let resulted_allowance = env + .cw_tokenfactory_issuer + .query_mint_allowance(&minter.address()) + .unwrap() + .allowance + .u128(); + + assert_eq!(resulted_allowance, allowance); + }); +} + +#[test] +fn test_query_mint_allowances_within_default_limit() { + test_query_within_default_limit::( + |(i, addr)| AllowanceInfo { + address: addr.to_string(), + allowance: Uint128::from((i as u128 + 1) * 10000u128), // generate distincted allowance + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_minter(&allowance.address, allowance.allowance.u128(), owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_mint_allowances(start_after, limit) + .unwrap() + .allowances + } + }, + ); +} + +#[test] +fn test_query_mint_allowance_over_default_limit() { + test_query_over_default_limit::( + |(i, addr)| AllowanceInfo { + address: addr.to_string(), + allowance: Uint128::from((i as u128 + 1) * 10000u128), // generate distincted allowance + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_minter(&allowance.address, allowance.allowance.u128(), owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_mint_allowances(start_after, limit) + .unwrap() + .allowances + } + }, + ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/mod.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/mod.rs new file mode 100644 index 000000000..3fe1f6085 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/mod.rs @@ -0,0 +1,12 @@ +mod beforesend; +mod blacklist; +mod burn; +mod contract_owner; +mod denom_metadata; +mod force_transfer; +mod freeze; +mod instantiate; +mod mint; +mod set_before_update_hook; +mod tokenfactory_admin; +mod whitelist; diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/set_before_update_hook.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/set_before_update_hook.rs new file mode 100644 index 000000000..39299fd93 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/set_before_update_hook.rs @@ -0,0 +1,32 @@ +use cw_tokenfactory_issuer::ContractError; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn test_set_before_update_hook() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + // Non-owner cannot set before update hook + let err = env + .cw_tokenfactory_issuer + .set_before_send_hook(non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); + + // Owner can set before update hook, but hook is already set + let err = env + .cw_tokenfactory_issuer + .set_before_send_hook(owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::BeforeSendHookAlreadyEnabled {}) + ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/tokenfactory_admin.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/tokenfactory_admin.rs new file mode 100644 index 000000000..65a3151e8 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/tokenfactory_admin.rs @@ -0,0 +1,35 @@ +use cw_tokenfactory_issuer::ContractError; +use osmosis_test_tube::Account; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn transfer_token_factory_admin_by_contract_owner_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let new_admin = &env.test_accs[1]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + env.cw_tokenfactory_issuer + .change_tokenfactory_admin(&new_admin.address(), owner) + .unwrap(); + + assert_eq!(new_admin.address(), env.token_admin(&denom)); +} + +#[test] +fn transfer_token_factory_admin_by_non_contract_owner_should_fail() { + let env = TestEnv::default(); + let non_owner = &env.test_accs[1]; + let someone_else = &env.test_accs[1]; + + let err = env + .cw_tokenfactory_issuer + .change_tokenfactory_admin(&someone_else.address(), non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ) +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/whitelist.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/whitelist.rs new file mode 100644 index 000000000..a4a39f385 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/whitelist.rs @@ -0,0 +1,339 @@ +use cw_tokenfactory_issuer::{msg::StatusInfo, ContractError}; +use osmosis_test_tube::Account; + +use crate::test_env::{ + test_query_over_default_limit, test_query_within_default_limit, TestEnv, TokenfactoryIssuer, +}; + +#[test] +fn set_whitelister_performed_by_contract_owner_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + env.cw_tokenfactory_issuer + .set_whitelister(&non_owner.address(), true, owner) + .unwrap(); + + let is_whitelister = env + .cw_tokenfactory_issuer + .query_is_whitelister(&env.test_accs[1].address()) + .unwrap() + .status; + + assert!(is_whitelister); + + env.cw_tokenfactory_issuer + .set_whitelister(&non_owner.address(), false, owner) + .unwrap(); + + let is_whitelister = env + .cw_tokenfactory_issuer + .query_is_whitelister(&env.test_accs[1].address()) + .unwrap() + .status; + + assert!(!is_whitelister); +} + +#[test] +fn set_whitelister_performed_by_non_contract_owner_should_fail() { + let env = TestEnv::default(); + let non_owner = &env.test_accs[1]; + + let err = env + .cw_tokenfactory_issuer + .set_whitelister(&non_owner.address(), true, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn set_whitelister_to_false_should_remove_it_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let mut sorted_addrs = env + .test_accs + .iter() + .map(|acc| acc.address()) + .collect::>(); + sorted_addrs.sort(); + + env.cw_tokenfactory_issuer + .set_whitelister(&sorted_addrs[0], true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .set_whitelister(&sorted_addrs[1], true, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_whitelister_allowances(None, None) + .unwrap() + .whitelisters, + vec![ + StatusInfo { + address: sorted_addrs[0].clone(), + status: true + }, + StatusInfo { + address: sorted_addrs[1].clone(), + status: true + } + ] + ); + + env.cw_tokenfactory_issuer + .set_whitelister(&sorted_addrs[1], false, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_whitelister_allowances(None, None) + .unwrap() + .whitelisters, + vec![StatusInfo { + address: sorted_addrs[0].clone(), + status: true + },] + ); + + assert!( + !env.cw_tokenfactory_issuer + .query_is_whitelister(&sorted_addrs[1]) + .unwrap() + .status + ); +} + +#[test] +fn whitelist_by_whitelister_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + let whitelistee = &env.test_accs[2]; + + env.cw_tokenfactory_issuer + .set_whitelister(&non_owner.address(), true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .whitelist(&whitelistee.address(), true, non_owner) + .unwrap(); + + // should be whitelisted after set true + assert!( + env.cw_tokenfactory_issuer + .query_is_whitelisted(&whitelistee.address()) + .unwrap() + .status + ); + + env.cw_tokenfactory_issuer + .whitelist(&whitelistee.address(), false, non_owner) + .unwrap(); + + // should be unwhitelisted after set false + assert!( + !env.cw_tokenfactory_issuer + .query_is_whitelisted(&whitelistee.address()) + .unwrap() + .status + ); +} + +#[test] +fn whitelist_by_non_whitelister_should_fail() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let whitelistee = &env.test_accs[2]; + let err = env + .cw_tokenfactory_issuer + .whitelist(&whitelistee.address(), true, owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Unauthorized {}) + ); +} + +#[test] +fn set_whitelist_to_false_should_remove_it_from_storage() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + let mut sorted_addrs = env + .test_accs + .iter() + .map(|acc| acc.address()) + .collect::>(); + sorted_addrs.sort(); + + env.cw_tokenfactory_issuer + .set_whitelister(&owner.address(), true, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .whitelist(&sorted_addrs[0], true, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .whitelist(&sorted_addrs[1], true, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_whitelistees(None, None) + .unwrap() + .whitelistees, + vec![ + StatusInfo { + address: sorted_addrs[0].clone(), + status: true + }, + StatusInfo { + address: sorted_addrs[1].clone(), + status: true + } + ] + ); + + env.cw_tokenfactory_issuer + .whitelist(&sorted_addrs[1], false, owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer + .query_whitelistees(None, None) + .unwrap() + .whitelistees, + vec![StatusInfo { + address: sorted_addrs[0].clone(), + status: true + },] + ); + + assert!( + !env.cw_tokenfactory_issuer + .query_is_whitelisted(&sorted_addrs[1]) + .unwrap() + .status + ); +} + +// query whitelisters +#[test] +fn query_whitelister_within_default_limit() { + test_query_within_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_whitelister(&allowance.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_whitelister_allowances(start_after, limit) + .unwrap() + .whitelisters + } + }, + ); +} + +#[test] +fn query_whitelister_over_default_limit() { + test_query_over_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |allowance| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_whitelister(&allowance.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_whitelister_allowances(start_after, limit) + .unwrap() + .whitelisters + } + }, + ); +} +// query whitelistees +#[test] +fn query_whitelistee_within_default_limit() { + test_query_within_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |expected_result| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_whitelister(&owner.address(), true, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .whitelist(&expected_result.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_whitelistees(start_after, limit) + .unwrap() + .whitelistees + } + }, + ); +} + +#[test] +fn query_whitelistee_over_default_limit() { + test_query_over_default_limit::( + |(_, addr)| StatusInfo { + address: addr.to_string(), + status: true, + }, + |env| { + move |expected_result| { + let owner = &env.test_accs[0]; + env.cw_tokenfactory_issuer + .set_whitelister(&owner.address(), true, owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .whitelist(&expected_result.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_whitelistees(start_after, limit) + .unwrap() + .whitelistees + } + }, + ); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/mod.rs b/contracts/external/cw-tokenfactory-issuer/tests/mod.rs new file mode 100644 index 000000000..73b60899d --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/mod.rs @@ -0,0 +1,8 @@ +// Ignore integration tests for code coverage since there will be problems with dynamic linking libosmosistesttube +// and also, tarpaulin will not be able read coverage out of wasm binary anyway +#![cfg(not(tarpaulin))] + +#[cfg(feature = "test-tube")] +mod cases; +#[cfg(feature = "test-tube")] +mod test_env; diff --git a/contracts/external/cw-tokenfactory-issuer/tests/test_env.rs b/contracts/external/cw-tokenfactory-issuer/tests/test_env.rs new file mode 100644 index 000000000..af9079487 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/test_env.rs @@ -0,0 +1,677 @@ +// The code is used in tests but reported as dead code +// see https://github.com/rust-lang/rust/issues/46379 +#![allow(dead_code)] + +use cosmwasm_std::{Coin, Uint128}; + +use cw_tokenfactory_issuer::msg::{ + BlacklisteesResponse, BlacklisterAllowancesResponse, Metadata, MigrateMsg, + WhitelisteesResponse, WhitelisterAllowancesResponse, +}; +use cw_tokenfactory_issuer::{ + msg::{ + AllowanceResponse, AllowancesResponse, DenomResponse, ExecuteMsg, + FreezerAllowancesResponse, InstantiateMsg, IsFrozenResponse, OwnerResponse, QueryMsg, + StatusResponse, + }, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::{ + cosmos::bank::v1beta1::{MsgSend, MsgSendResponse}, + cosmwasm::wasm::v1::{ + MsgExecuteContractResponse, MsgMigrateContract, MsgMigrateContractResponse, + }, + osmosis::tokenfactory::v1beta1::QueryDenomAuthorityMetadataRequest, + }, + Account, Bank, Module, OsmosisTestApp, Runner, RunnerError, RunnerExecuteResult, + SigningAccount, TokenFactory, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; +use std::rc::Rc; + +pub struct TestEnv { + pub test_accs: Vec, + pub cw_tokenfactory_issuer: TokenfactoryIssuer, +} + +impl TestEnv { + pub fn new(instantiate_msg: InstantiateMsg, signer_index: usize) -> Result { + let app = OsmosisTestApp::new(); + let test_accs_count: u64 = 4; + let test_accs = Self::create_default_test_accs(&app, test_accs_count); + + let cw_tokenfactory_issuer = + TokenfactoryIssuer::new(app, &instantiate_msg, &test_accs[signer_index])?; + + Ok(Self { + test_accs, + cw_tokenfactory_issuer, + }) + } + + pub fn create_default_test_accs( + app: &OsmosisTestApp, + test_accs_count: u64, + ) -> Vec { + let default_initial_balance = [Coin::new(100_000_000_000, "uosmo")]; + + app.init_accounts(&default_initial_balance, test_accs_count) + .unwrap() + } + + pub fn app(&self) -> &OsmosisTestApp { + &self.cw_tokenfactory_issuer.app + } + + pub fn tokenfactory(&self) -> TokenFactory<'_, OsmosisTestApp> { + TokenFactory::new(self.app()) + } + + pub fn bank(&self) -> Bank<'_, OsmosisTestApp> { + Bank::new(self.app()) + } + + pub fn token_admin(&self, denom: &str) -> String { + self.tokenfactory() + .query_denom_authority_metadata(&QueryDenomAuthorityMetadataRequest { + denom: denom.to_string(), + }) + .unwrap() + .authority_metadata + .unwrap() + .admin + } + + pub fn send_tokens( + &self, + to: String, + coins: Vec, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.app().execute::( + MsgSend { + from_address: signer.address(), + to_address: to, + amount: coins + .into_iter() + .map( + |c| osmosis_test_tube::osmosis_std::types::cosmos::base::v1beta1::Coin { + denom: c.denom, + amount: c.amount.to_string(), + }, + ) + .collect(), + }, + "/cosmos.bank.v1beta1.MsgSend", + signer, + ) + } +} + +impl Default for TestEnv { + fn default() -> Self { + Self::new( + InstantiateMsg::NewToken { + subdenom: "uusd".to_string(), + }, + 0, + ) + .unwrap() + } +} + +#[derive(Debug)] +pub struct TokenfactoryIssuer { + pub app: OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl TokenfactoryIssuer { + pub fn new( + app: OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(&app); + let token_creation_fee = Coin::new(10000000, "uosmo"); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(&self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + pub fn change_contract_owner( + &self, + new_owner: &str, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::ChangeContractOwner { + new_owner: new_owner.to_string(), + }, + &[], + signer, + ) + } + pub fn change_tokenfactory_admin( + &self, + new_admin: &str, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::ChangeTokenFactoryAdmin { + new_admin: new_admin.to_string(), + }, + &[], + signer, + ) + } + pub fn set_denom_metadata( + &self, + metadata: Metadata, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute(&ExecuteMsg::SetDenomMetadata { metadata }, &[], signer) + } + + pub fn set_minter( + &self, + address: &str, + allowance: u128, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::SetMinterAllowance { + address: address.to_string(), + allowance: allowance.into(), + }, + &[], + signer, + ) + } + pub fn mint( + &self, + address: &str, + amount: u128, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::Mint { + to_address: address.to_string(), + amount: amount.into(), + }, + &[], + signer, + ) + } + + pub fn set_burner( + &self, + address: &str, + allowance: u128, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::SetBurnerAllowance { + address: address.to_string(), + allowance: allowance.into(), + }, + &[], + signer, + ) + } + + pub fn burn( + &self, + address: &str, + amount: u128, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::Burn { + from_address: address.to_string(), + amount: amount.into(), + }, + &[], + signer, + ) + } + + pub fn set_freezer( + &self, + address: &str, + status: bool, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::SetFreezer { + address: address.to_string(), + status, + }, + &[], + signer, + ) + } + + pub fn set_before_send_hook( + &self, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute(&ExecuteMsg::SetBeforeSendHook {}, &[], signer) + } + + pub fn set_blacklister( + &self, + address: &str, + status: bool, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::SetBlacklister { + address: address.to_string(), + status, + }, + &[], + signer, + ) + } + + pub fn set_whitelister( + &self, + address: &str, + status: bool, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::SetWhitelister { + address: address.to_string(), + status, + }, + &[], + signer, + ) + } + + pub fn force_transfer( + &self, + signer: &SigningAccount, + amount: Uint128, + from_address: String, + to_address: String, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::ForceTransfer { + amount, + from_address, + to_address, + }, + &[], + signer, + ) + } + + pub fn freeze( + &self, + status: bool, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute(&ExecuteMsg::Freeze { status }, &[], signer) + } + + pub fn blacklist( + &self, + address: &str, + status: bool, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::Blacklist { + address: address.to_string(), + status, + }, + &[], + signer, + ) + } + + pub fn whitelist( + &self, + address: &str, + status: bool, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::Whitelist { + address: address.to_string(), + status, + }, + &[], + signer, + ) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(&self.app); + wasm.query(&self.contract_addr, query_msg) + } + + pub fn query_denom(&self) -> Result { + self.query(&QueryMsg::Denom {}) + } + + pub fn query_is_freezer(&self, address: &str) -> Result { + self.query(&QueryMsg::IsFreezer { + address: address.to_string(), + }) + } + + pub fn query_is_blacklister(&self, address: &str) -> Result { + self.query(&QueryMsg::IsBlacklister { + address: address.to_string(), + }) + } + + pub fn query_is_whitelister(&self, address: &str) -> Result { + self.query(&QueryMsg::IsWhitelister { + address: address.to_string(), + }) + } + + pub fn query_freezer_allowances( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::FreezerAllowances { start_after, limit }) + } + + pub fn query_blacklister_allowances( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::BlacklisterAllowances { start_after, limit }) + } + + pub fn query_blacklistees( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::Blacklistees { start_after, limit }) + } + + pub fn query_whitelister_allowances( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::WhitelisterAllowances { start_after, limit }) + } + + pub fn query_whitelistees( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::Whitelistees { start_after, limit }) + } + + pub fn query_is_frozen(&self) -> Result { + self.query(&QueryMsg::IsFrozen {}) + } + + pub fn query_is_blacklisted(&self, address: &str) -> Result { + self.query(&QueryMsg::IsBlacklisted { + address: address.to_string(), + }) + } + + pub fn query_is_whitelisted(&self, address: &str) -> Result { + self.query(&QueryMsg::IsWhitelisted { + address: address.to_string(), + }) + } + + pub fn query_owner(&self) -> Result { + self.query(&QueryMsg::Owner {}) + } + + pub fn query_mint_allowance(&self, address: &str) -> Result { + self.query(&QueryMsg::MintAllowance { + address: address.to_string(), + }) + } + + pub fn query_mint_allowances( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::MintAllowances { start_after, limit }) + } + + pub fn query_burn_allowance(&self, address: &str) -> Result { + self.query(&QueryMsg::BurnAllowance { + address: address.to_string(), + }) + } + + pub fn query_burn_allowances( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::BurnAllowances { start_after, limit }) + } + + pub fn migrate( + &self, + testdata: &str, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(&self.app); + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let wasm_byte_code = + std::fs::read(manifest_path.join("tests").join("testdata").join(testdata)).unwrap(); + + let code_id = wasm.store_code(&wasm_byte_code, None, signer)?.data.code_id; + self.app.execute( + MsgMigrateContract { + sender: signer.address(), + contract: self.contract_addr.clone(), + code_id, + msg: serde_json::to_vec(&MigrateMsg {}).unwrap(), + }, + "/cosmwasm.wasm.v1.MsgMigrateContract", + signer, + ) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("cw_tokenfactory_issuer.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("cw_tokenfactory_issuer-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} + +pub fn test_query_within_default_limit( + gen_result: impl FnMut((usize, &String)) -> QueryResult, + set_state: impl Fn(Rc) -> SetStateClosure, + query_state: impl Fn(Rc) -> QueryStateClosure, +) where + QueryResult: PartialEq + Debug + Clone, + SetStateClosure: Fn(QueryResult), + QueryStateClosure: Fn(Option, Option) -> Vec, +{ + let env = Rc::new(TestEnv::default()); + let test_accs_count = 10; + let test_accs_with_allowance = + TestEnv::create_default_test_accs(&env.cw_tokenfactory_issuer.app, test_accs_count); + + let mut sorted_addrs = test_accs_with_allowance + .iter() + .map(|acc| acc.address()) + .collect::>(); + sorted_addrs.sort(); + + let allowances = sorted_addrs + .iter() + .enumerate() + .map(gen_result) + .collect::>(); + + allowances + .iter() + .for_each(|allowance| set_state(env.clone())(allowance.clone())); + + let query = query_state(env); + + // let be allowance for the sorted_addrs with index n + + // query from start with default limit + // = [<0>..<10>] (since test_accs_count is 10) + assert_eq!(query(None, None), allowances); + + // query from start with limit 1 + // = [<0>] + assert_eq!(query(None, Some(1)), allowances[0..1]); + + // query start after sorted_addrs[1], limit 1 + // = [<2>] + assert_eq!( + query(Some(sorted_addrs[1].clone()), Some(1)), + allowances[2..3] + ); + + // query start after sorted_addrs[1], limit 10 + // = [<2>..<10>] (since test_accs_count is 10) + assert_eq!( + query(Some(sorted_addrs[1].clone()), Some(10)), + allowances[2..10] + ); + + // query start after sorted_addrs[9], with default limit + // = [] + assert_eq!(query(Some(sorted_addrs[9].clone()), None), vec![]); +} + +pub fn test_query_over_default_limit( + gen_result: impl FnMut((usize, &String)) -> QueryResult, + set_state: impl Fn(Rc) -> SetStateClosure, + query_state: impl Fn(Rc) -> QueryStateClosure, +) where + QueryResult: PartialEq + Debug + Clone, + SetStateClosure: Fn(QueryResult), + QueryStateClosure: Fn(Option, Option) -> Vec, +{ + let env = Rc::new(TestEnv::default()); + let test_accs_count = 40; + let test_accs_with_allowance = + TestEnv::create_default_test_accs(&env.cw_tokenfactory_issuer.app, test_accs_count); + + let mut sorted_addrs = test_accs_with_allowance + .iter() + .map(|acc| acc.address()) + .collect::>(); + sorted_addrs.sort(); + + let allowances = sorted_addrs + .iter() + .enumerate() + .map(gen_result) + .collect::>(); + + allowances + .iter() + .for_each(|allowance| set_state(env.clone())(allowance.clone())); + + let query = query_state(env); + + // let be allowance for the sorted_addrs with index n + + // query from start with default limit + // = [<0>..<10>] + assert_eq!(query(None, None), allowances[..10]); + + // query start after sorted_addrs[4] with default limit + // = [<5>..<15>] (<5> is after <4>, <15> is <5> + limit 10) + assert_eq!( + query(Some(sorted_addrs[4].clone()), None), + allowances[5..15] + ); + + // max limit = 30 + assert_eq!(query(None, Some(40)), allowances[..30]); + + // start after nth, get n+1 .. n+1+limit (30) + assert_eq!( + query(Some(sorted_addrs[4].clone()), Some(40)), + allowances[5..35] + ); +} diff --git a/contracts/proposal/dao-proposal-condorcet/.gitignore b/contracts/proposal/dao-proposal-condorcet/.gitignore deleted file mode 100644 index 5a6557d7b..000000000 --- a/contracts/proposal/dao-proposal-condorcet/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# Build results -/target - -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.editorconfig -.idea diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs index 34fc5b7ba..45a76e9d2 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs @@ -1,11 +1,9 @@ use cosmwasm_std::{to_binary, Addr, Coin, Empty, Uint128}; use cw20::Cw20Coin; - use cw_multi_test::{next_block, App, BankSudo, ContractWrapper, Executor, SudoMsg}; use cw_utils::Duration; use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_pre_propose_multiple as cppm; - use dao_testing::contracts::{ cw20_balances_voting_contract, cw20_base_contract, cw20_stake_contract, cw20_staked_balances_voting_contract, cw4_group_contract, cw721_base_contract, @@ -268,10 +266,9 @@ pub fn _instantiate_with_native_staked_balances_governance( voting_module_instantiate_info: ModuleInstantiateInfo { code_id: native_stake_id, msg: to_binary(&dao_voting_native_staked::msg::InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: None, denom: "ujuno".to_string(), unstaking_duration: None, + active_threshold: None, }) .unwrap(), admin: None, diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index 9e0fe9811..8f1756edf 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -264,10 +264,9 @@ pub(crate) fn instantiate_with_native_staked_balances_governance( voting_module_instantiate_info: ModuleInstantiateInfo { code_id: native_stake_id, msg: to_binary(&dao_voting_native_staked::msg::InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: None, denom: "ujuno".to_string(), unstaking_duration: None, + active_threshold: None, }) .unwrap(), admin: None, diff --git a/contracts/voting/dao-voting-cw20-staked/.gitignore b/contracts/voting/dao-voting-cw20-staked/.gitignore deleted file mode 100644 index dfdaaa6bc..000000000 --- a/contracts/voting/dao-voting-cw20-staked/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Build results -/target - -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.idea diff --git a/contracts/voting/dao-voting-cw20-staked/src/contract.rs b/contracts/voting/dao-voting-cw20-staked/src/contract.rs index 53ab5b4d6..4033d5247 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/contract.rs @@ -8,14 +8,11 @@ use cw2::set_contract_version; use cw20::{Cw20Coin, TokenInfoResponse}; use cw_utils::parse_reply_instantiate_data; use dao_interface::voting::IsActiveResponse; -use dao_voting::threshold::ActiveThreshold; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; use std::convert::TryInto; use crate::error::ContractError; -use crate::msg::{ - ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StakingInfo, - TokenInfo, -}; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StakingInfo, TokenInfo}; use crate::state::{ ACTIVE_THRESHOLD, DAO, STAKING_CONTRACT, STAKING_CONTRACT_CODE_ID, STAKING_CONTRACT_UNSTAKING_DURATION, TOKEN, diff --git a/contracts/voting/dao-voting-cw20-staked/src/msg.rs b/contracts/voting/dao-voting-cw20-staked/src/msg.rs index 434c0b739..33753fcae 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/msg.rs @@ -5,7 +5,7 @@ use cw20_base::msg::InstantiateMarketingInfo; use cw_utils::Duration; use dao_dao_macros::{active_query, token_query, voting_module_query}; -use dao_voting::threshold::ActiveThreshold; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; /// Information about the staking contract to be used with this voting /// module. @@ -84,10 +84,5 @@ pub enum QueryMsg { ActiveThreshold {}, } -#[cw_serde] -pub struct ActiveThresholdResponse { - pub active_threshold: Option, -} - #[cw_serde] pub struct MigrateMsg {} diff --git a/contracts/voting/dao-voting-cw20-staked/src/tests.rs b/contracts/voting/dao-voting-cw20-staked/src/tests.rs index ca793736b..b6d664604 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/tests.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/tests.rs @@ -6,11 +6,11 @@ use cw2::ContractVersion; use cw20::{BalanceResponse, Cw20Coin, MinterResponse, TokenInfoResponse}; use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor}; use dao_interface::voting::{InfoResponse, IsActiveResponse, VotingPowerAtHeightResponse}; -use dao_voting::threshold::ActiveThreshold; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, - msg::{ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StakingInfo}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StakingInfo}, }; const DAO_ADDR: &str = "dao"; @@ -1108,7 +1108,7 @@ fn test_active_threshold_percent_rounds_up() { assert!(!is_active.active); // Stake 1 more token as creator, should now be active. - stake_tokens(&mut app, staking_addr, token_addr, CREATOR_ADDR, 2); + stake_tokens(&mut app, staking_addr, token_addr, CREATOR_ADDR, 1); app.update_block(next_block); let is_active: IsActiveResponse = app diff --git a/contracts/voting/dao-voting-cw4/.gitignore b/contracts/voting/dao-voting-cw4/.gitignore deleted file mode 100644 index dfdaaa6bc..000000000 --- a/contracts/voting/dao-voting-cw4/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Build results -/target - -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.idea diff --git a/contracts/voting/dao-voting-cw721-staked/src/contract.rs b/contracts/voting/dao-voting-cw721-staked/src/contract.rs index 3d5b1d849..613edc917 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/contract.rs @@ -1,6 +1,5 @@ use crate::hooks::{stake_hook_msgs, unstake_hook_msgs}; -use crate::msg::{ActiveThresholdResponse, MigrateMsg, NftContract}; -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg}; use crate::state::{ register_staked_nft, register_unstaked_nfts, Config, ACTIVE_THRESHOLD, CONFIG, DAO, HOOKS, INITITIAL_NFTS, MAX_CLAIMS, NFT_BALANCES, NFT_CLAIMS, STAKED_NFTS_PER_OWNER, TOTAL_STAKED_NFTS, @@ -19,7 +18,7 @@ use cw_storage_plus::Bound; use cw_utils::{parse_reply_instantiate_data, Duration}; use dao_interface::state::Admin; use dao_interface::voting::IsActiveResponse; -use dao_voting::threshold::ActiveThreshold; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cw721-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/contracts/voting/dao-voting-cw721-staked/src/msg.rs b/contracts/voting/dao-voting-cw721-staked/src/msg.rs index 067716bd9..3ce904c8a 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/msg.rs @@ -4,7 +4,7 @@ use cw721::Cw721ReceiveMsg; use cw_utils::Duration; use dao_dao_macros::{active_query, voting_module_query}; use dao_interface::state::Admin; -use dao_voting::threshold::ActiveThreshold; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; #[cw_serde] #[allow(clippy::large_enum_variant)] @@ -93,10 +93,5 @@ pub enum QueryMsg { ActiveThreshold {}, } -#[cw_serde] -pub struct ActiveThresholdResponse { - pub active_threshold: Option, -} - #[cw_serde] pub struct MigrateMsg {} diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs index ba8eb5915..85b984636 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs @@ -7,14 +7,14 @@ use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor}; use cw_utils::Duration; use dao_interface::{state::Admin, voting::IsActiveResponse}; use dao_testing::contracts::{cw721_base_contract, voting_cw721_staked_contract}; -use dao_voting::threshold::ActiveThreshold; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; use sg721::CollectionInfo; use sg_multi_test::StargazeApp; use sg_std::StargazeMsgWrapper; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, - msg::{ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg}, state::{Config, MAX_CLAIMS}, testing::{ execute::{ diff --git a/contracts/voting/dao-voting-native-staked/.gitignore b/contracts/voting/dao-voting-native-staked/.gitignore deleted file mode 100644 index dfdaaa6bc..000000000 --- a/contracts/voting/dao-voting-native-staked/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Build results -/target - -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.idea diff --git a/contracts/voting/dao-voting-native-staked/Cargo.toml b/contracts/voting/dao-voting-native-staked/Cargo.toml index 495e4b215..01131e006 100644 --- a/contracts/voting/dao-voting-native-staked/Cargo.toml +++ b/contracts/voting/dao-voting-native-staked/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dao-voting-native-staked" -authors = ["Callum Anderson "] -description = "A DAO DAO voting module based on staked cw721 tokens." +authors = ["Callum Anderson ", "Jake Hartnell "] +description = "A DAO DAO voting module based on staked native tokens. If your chain uses Token Factory, consider using dao-voting-token-factory-staked for additional functionality including creating new tokens." edition = { workspace = true } license = { workspace = true } repository = { workspace = true } @@ -17,18 +17,19 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cosmwasm-std = { workspace = true } +cosmwasm-std = { workspace = true, features = ["cosmwasm_1_1"] } cosmwasm-schema = { workspace = true } cosmwasm-storage = { workspace = true } -cw-storage-plus = { workspace = true } cw2 = { workspace = true } -cw-utils = { workspace = true } cw-controllers = { workspace = true } - -thiserror = { workspace = true } +cw-hooks = { workspace = true } +cw-paginate-storage = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } -cw-paginate-storage = { workspace = true } +dao-voting = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/voting/dao-voting-native-staked/README.md b/contracts/voting/dao-voting-native-staked/README.md index eacb5d9e8..a9084cc3f 100644 --- a/contracts/voting/dao-voting-native-staked/README.md +++ b/contracts/voting/dao-voting-native-staked/README.md @@ -7,3 +7,4 @@ arbitrary height. This contract implements the interface needed to be a DAO DAO [voting module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module). +If your chain uses Token Factory, consider using `dao-voting-token-factory-staked` for additional functionality including creating new tokens. diff --git a/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json b/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json index f553cbbab..ae2140695 100644 --- a/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json +++ b/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json @@ -10,25 +10,21 @@ "denom" ], "properties": { - "denom": { - "type": "string" - }, - "manager": { - "type": [ - "string", - "null" - ] - }, - "owner": { + "active_threshold": { + "description": "The number or percentage of tokens that must be staked for the DAO to be active", "anyOf": [ { - "$ref": "#/definitions/Admin" + "$ref": "#/definitions/ActiveThreshold" }, { "type": "null" } ] }, + "denom": { + "description": "Token denom e.g. ujuno, or some ibc denom", + "type": "string" + }, "unstaking_duration": { "anyOf": [ { @@ -42,24 +38,24 @@ }, "additionalProperties": false, "definitions": { - "Admin": { - "description": "Information about the CosmWasm level admin of a contract. Used in conjunction with `ModuleInstantiateInfo` to instantiate modules.", + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", "oneOf": [ { - "description": "Set the admin to a specified address.", + "description": "The absolute number of tokens that must be staked for the module to be active.", "type": "object", "required": [ - "address" + "absolute_count" ], "properties": { - "address": { + "absolute_count": { "type": "object", "required": [ - "addr" + "count" ], "properties": { - "addr": { - "type": "string" + "count": { + "$ref": "#/definitions/Uint128" } }, "additionalProperties": false @@ -68,14 +64,22 @@ "additionalProperties": false }, { - "description": "Sets the admin as the core module address.", + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", "type": "object", "required": [ - "core_module" + "percentage" ], "properties": { - "core_module": { + "percentage": { "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, "additionalProperties": false } }, @@ -83,6 +87,10 @@ } ] }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "Duration": { "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", "oneOf": [ @@ -116,6 +124,10 @@ "additionalProperties": false } ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" } } }, @@ -124,6 +136,7 @@ "title": "ExecuteMsg", "oneOf": [ { + "description": "Stakes tokens with the contract to get voting power in the DAO", "type": "object", "required": [ "stake" @@ -137,6 +150,7 @@ "additionalProperties": false }, { + "description": "Unstakes tokens so that they begin unbonding", "type": "object", "required": [ "unstake" @@ -158,6 +172,7 @@ "additionalProperties": false }, { + "description": "Updates the contract configuration", "type": "object", "required": [ "update_config" @@ -175,18 +190,6 @@ "type": "null" } ] - }, - "manager": { - "type": [ - "string", - "null" - ] - }, - "owner": { - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -195,6 +198,7 @@ "additionalProperties": false }, { + "description": "Claims unstaked tokens that have completed the unbonding period", "type": "object", "required": [ "claim" @@ -206,9 +210,132 @@ } }, "additionalProperties": false + }, + { + "description": "Sets the active threshold to a new value. Only the instantiator of this contract (a DAO most likely) may call this method.", + "type": "object", + "required": [ + "update_active_threshold" + ], + "properties": { + "update_active_threshold": { + "type": "object", + "properties": { + "new_threshold": { + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Adds a hook that fires on staking / unstaking", + "type": "object", + "required": [ + "add_hook" + ], + "properties": { + "add_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes a hook that fires on staking / unstaking", + "type": "object", + "required": [ + "remove_hook" + ], + "properties": { + "remove_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "Duration": { "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", "oneOf": [ @@ -287,6 +414,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "get_denom" + ], + "properties": { + "get_denom": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -316,6 +456,32 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "active_threshold" + ], + "properties": { + "active_threshold": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_hooks" + ], + "properties": { + "get_hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns the voting power for an address at a given height.", "type": "object", @@ -397,6 +563,19 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "is_active" + ], + "properties": { + "is_active": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -408,6 +587,83 @@ }, "sudo": null, "responses": { + "active_threshold": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ActiveThresholdResponse", + "type": "object", + "properties": { + "active_threshold": { + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, "claims": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ClaimsResponse", @@ -523,26 +779,6 @@ "denom": { "type": "string" }, - "manager": { - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "owner": { - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, "unstaking_duration": { "anyOf": [ { @@ -556,10 +792,6 @@ }, "additionalProperties": false, "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, "Duration": { "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", "oneOf": [ @@ -596,6 +828,37 @@ } } }, + "get_denom": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DenomResponse", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "get_hooks": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetHooksResponse", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "info": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InfoResponse", @@ -630,6 +893,11 @@ } } }, + "is_active": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "list_stakers": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ListStakersResponse", diff --git a/contracts/voting/dao-voting-native-staked/src/contract.rs b/contracts/voting/dao-voting-native-staked/src/contract.rs index eb93fa0fe..80a413a9f 100644 --- a/contracts/voting/dao-voting-native-staked/src/contract.rs +++ b/contracts/voting/dao-voting-native-staked/src/contract.rs @@ -1,24 +1,33 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - coins, to_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, - StdResult, Uint128, + coins, to_binary, BankMsg, BankQuery, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, + MessageInfo, Response, StdResult, Uint128, Uint256, }; use cw2::set_contract_version; use cw_controllers::ClaimsResponse; use cw_utils::{must_pay, Duration}; -use dao_interface::state::Admin; -use dao_interface::voting::{TotalPowerAtHeightResponse, VotingPowerAtHeightResponse}; +use dao_interface::voting::{ + IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; use crate::error::ContractError; use crate::msg::{ - ExecuteMsg, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, + DenomResponse, ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, + QueryMsg, StakerBalanceResponse, +}; +use crate::state::{ + Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, HOOKS, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL, }; -use crate::state::{Config, CLAIMS, CONFIG, DAO, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-native-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +// We multiply by this when calculating needed power for being active +// when using active threshold with percent +const PRECISION_FACTOR: u128 = 10u128.pow(9); + fn validate_duration(duration: Option) -> Result<(), ContractError> { if let Some(unstaking_duration) = duration { match unstaking_duration { @@ -46,47 +55,47 @@ pub fn instantiate( ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - let owner = msg - .owner - .as_ref() - .map(|owner| match owner { - Admin::Address { addr } => deps.api.addr_validate(addr), - Admin::CoreModule {} => Ok(info.sender.clone()), - }) - .transpose()?; - let manager = msg - .manager - .map(|manager| deps.api.addr_validate(&manager)) - .transpose()?; - validate_duration(msg.unstaking_duration)?; let config = Config { - owner, - manager, - denom: msg.denom, + denom: msg.denom.clone(), unstaking_duration: msg.unstaking_duration, }; CONFIG.save(deps.storage, &config)?; DAO.save(deps.storage, &info.sender)?; - Ok(Response::new() - .add_attribute("action", "instantiate") - .add_attribute( - "owner", - config - .owner - .map(|a| a.to_string()) - .unwrap_or_else(|| "None".to_string()), - ) - .add_attribute( - "manager", - config - .manager - .map(|a| a.to_string()) - .unwrap_or_else(|| "None".to_string()), - )) + if let Some(active_threshold) = msg.active_threshold.as_ref() { + match active_threshold { + ActiveThreshold::AbsoluteCount { count } => { + assert_valid_absolute_count_threshold(deps.as_ref(), &msg.denom, *count)?; + } + ActiveThreshold::Percentage { percent } => { + if *percent > Decimal::percent(100) || *percent <= Decimal::percent(0) { + return Err(ContractError::InvalidActivePercentage {}); + } + } + } + + ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?; + } + + Ok(Response::new().add_attribute("action", "instantiate")) +} + +pub fn assert_valid_absolute_count_threshold( + deps: Deps, + token_denom: &str, + count: Uint128, +) -> Result<(), ContractError> { + if count.is_zero() { + return Err(ContractError::ZeroActiveCount {}); + } + let supply: Coin = deps.querier.query_supply(token_denom.to_string())?; + if count > supply.amount { + return Err(ContractError::InvalidAbsoluteCount {}); + } + Ok(()) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -99,12 +108,13 @@ pub fn execute( match msg { ExecuteMsg::Stake {} => execute_stake(deps, env, info), ExecuteMsg::Unstake { amount } => execute_unstake(deps, env, info, amount), - ExecuteMsg::UpdateConfig { - owner, - manager, - duration, - } => execute_update_config(deps, info, owner, manager, duration), + ExecuteMsg::UpdateConfig { duration } => execute_update_config(deps, info, duration), ExecuteMsg::Claim {} => execute_claim(deps, env, info), + ExecuteMsg::UpdateActiveThreshold { new_threshold } => { + execute_update_active_threshold(deps, env, info, new_threshold) + } + ExecuteMsg::AddHook { addr } => execute_add_hook(deps, env, info, addr), + ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, env, info, addr), } } @@ -204,50 +214,22 @@ pub fn execute_unstake( pub fn execute_update_config( deps: DepsMut, info: MessageInfo, - new_owner: Option, - new_manager: Option, duration: Option, ) -> Result { let mut config: Config = CONFIG.load(deps.storage)?; - if Some(info.sender.clone()) != config.owner && Some(info.sender.clone()) != config.manager { + + // Only the DAO can update the config + let dao = DAO.load(deps.storage)?; + if info.sender != dao { return Err(ContractError::Unauthorized {}); } - let new_owner = new_owner - .map(|new_owner| deps.api.addr_validate(&new_owner)) - .transpose()?; - let new_manager = new_manager - .map(|new_manager| deps.api.addr_validate(&new_manager)) - .transpose()?; - validate_duration(duration)?; - if Some(info.sender) != config.owner && new_owner != config.owner { - return Err(ContractError::OnlyOwnerCanChangeOwner {}); - }; - - config.owner = new_owner; - config.manager = new_manager; - config.unstaking_duration = duration; CONFIG.save(deps.storage, &config)?; - Ok(Response::new() - .add_attribute("action", "update_config") - .add_attribute( - "owner", - config - .owner - .map(|a| a.to_string()) - .unwrap_or_else(|| "None".to_string()), - ) - .add_attribute( - "manager", - config - .manager - .map(|a| a.to_string()) - .unwrap_or_else(|| "None".to_string()), - )) + Ok(Response::new().add_attribute("action", "update_config")) } pub fn execute_claim( @@ -273,6 +255,73 @@ pub fn execute_claim( .add_attribute("amount", release)) } +pub fn execute_update_active_threshold( + deps: DepsMut, + _env: Env, + info: MessageInfo, + new_active_threshold: Option, +) -> Result { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + if let Some(active_threshold) = new_active_threshold { + match active_threshold { + ActiveThreshold::Percentage { percent } => { + if percent > Decimal::percent(100) || percent.is_zero() { + return Err(ContractError::InvalidActivePercentage {}); + } + } + ActiveThreshold::AbsoluteCount { count } => { + let denom = CONFIG.load(deps.storage)?.denom; + assert_valid_absolute_count_threshold(deps.as_ref(), &denom, count)?; + } + } + ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; + } else { + ACTIVE_THRESHOLD.remove(deps.storage); + } + + Ok(Response::new().add_attribute("action", "update_active_threshold")) +} + +pub fn execute_add_hook( + deps: DepsMut, + _env: Env, + info: MessageInfo, + addr: String, +) -> Result { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + let hook = deps.api.addr_validate(&addr)?; + HOOKS.add_hook(deps.storage, hook)?; + Ok(Response::new() + .add_attribute("action", "add_hook") + .add_attribute("hook", addr)) +} + +pub fn execute_remove_hook( + deps: DepsMut, + _env: Env, + info: MessageInfo, + addr: String, +) -> Result { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + let hook = deps.api.addr_validate(&addr)?; + HOOKS.remove_hook(deps.storage, hook)?; + Ok(Response::new() + .add_attribute("action", "remove_hook") + .add_attribute("hook", addr)) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { @@ -289,6 +338,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ListStakers { start_after, limit } => { query_list_stakers(deps, start_after, limit) } + QueryMsg::GetDenom {} => query_denom(deps), + QueryMsg::IsActive {} => query_is_active(deps), + QueryMsg::ActiveThreshold {} => query_active_threshold(deps), + QueryMsg::GetHooks {} => to_binary(&query_hooks(deps)?), } } @@ -328,6 +381,13 @@ pub fn query_dao(deps: Deps) -> StdResult { to_binary(&dao) } +pub fn query_denom(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + to_binary(&DenomResponse { + denom: config.denom, + }) +} + pub fn query_claims(deps: Deps, address: String) -> StdResult { CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?) } @@ -360,6 +420,80 @@ pub fn query_list_stakers( to_binary(&ListStakersResponse { stakers }) } +pub fn query_is_active(deps: Deps) -> StdResult { + let threshold = ACTIVE_THRESHOLD.may_load(deps.storage)?; + if let Some(threshold) = threshold { + let denom = CONFIG.load(deps.storage)?.denom; + let actual_power = STAKED_TOTAL.may_load(deps.storage)?.unwrap_or_default(); + match threshold { + ActiveThreshold::AbsoluteCount { count } => to_binary(&IsActiveResponse { + active: actual_power >= count, + }), + ActiveThreshold::Percentage { percent } => { + // percent is bounded between [0, 100]. decimal + // represents percents in u128 terms as p * + // 10^15. this bounds percent between [0, 10^17]. + // + // total_potential_power is bounded between [0, 2^128] + // as it tracks the balances of a cw20 token which has + // a max supply of 2^128. + // + // with our precision factor being 10^9: + // + // total_power <= 2^128 * 10^9 <= 2^256 + // + // so we're good to put that in a u256. + // + // multiply_ratio promotes to a u512 under the hood, + // so it won't overflow, multiplying by a percent less + // than 100 is gonna make something the same size or + // smaller, applied + 10^9 <= 2^128 * 10^9 + 10^9 <= + // 2^256, so the top of the round won't overflow, and + // rounding is rounding down, so the whole thing can + // be safely unwrapped at the end of the day thank you + // for coming to my ted talk. + let total_potential_power: cosmwasm_std::SupplyResponse = + deps.querier + .query(&cosmwasm_std::QueryRequest::Bank(BankQuery::Supply { + denom, + }))?; + let total_power = total_potential_power + .amount + .amount + .full_mul(PRECISION_FACTOR); + // under the hood decimals are `atomics / 10^decimal_places`. + // cosmwasm doesn't give us a Decimal * Uint256 + // implementation so we take the decimal apart and + // multiply by the fraction. + let applied = total_power.multiply_ratio( + percent.atomics(), + Uint256::from(10u64).pow(percent.decimal_places()), + ); + let rounded = (applied + Uint256::from(PRECISION_FACTOR) - Uint256::from(1u128)) + / Uint256::from(PRECISION_FACTOR); + let count: Uint128 = rounded.try_into().unwrap(); + to_binary(&IsActiveResponse { + active: actual_power >= count, + }) + } + } + } else { + to_binary(&IsActiveResponse { active: true }) + } +} + +pub fn query_active_threshold(deps: Deps) -> StdResult { + to_binary(&ActiveThresholdResponse { + active_threshold: ACTIVE_THRESHOLD.may_load(deps.storage)?, + }) +} + +pub fn query_hooks(deps: Deps) -> StdResult { + Ok(GetHooksResponse { + hooks: HOOKS.query_hooks(deps)?.hooks, + }) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { // Set contract to version to latest diff --git a/contracts/voting/dao-voting-native-staked/src/error.rs b/contracts/voting/dao-voting-native-staked/src/error.rs index dc8cf9f83..b87342fb4 100644 --- a/contracts/voting/dao-voting-native-staked/src/error.rs +++ b/contracts/voting/dao-voting-native-staked/src/error.rs @@ -2,7 +2,7 @@ use cosmwasm_std::StdError; use cw_utils::PaymentError; use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), @@ -10,6 +10,9 @@ pub enum ContractError { #[error("{0}")] PaymentError(#[from] PaymentError), + #[error(transparent)] + HookError(#[from] cw_hooks::HookError), + #[error("Unauthorized")] Unauthorized {}, @@ -22,12 +25,18 @@ pub enum ContractError { #[error("Too many outstanding claims. Claim some tokens before unstaking more.")] TooManyClaims {}, - #[error("Only owner can change owner")] - OnlyOwnerCanChangeOwner {}, + #[error("Absolute count threshold cannot be greater than the total token supply")] + InvalidAbsoluteCount {}, + + #[error("Active threshold percentage must be greater than 0 and less than 1")] + InvalidActivePercentage {}, #[error("Can only unstake less than or equal to the amount you have staked")] InvalidUnstakeAmount {}, + #[error("Active threshold count must be greater than zero")] + ZeroActiveCount {}, + #[error("Amount being unstaked must be non-zero")] ZeroUnstake {}, } diff --git a/contracts/voting/dao-voting-native-staked/src/hooks.rs b/contracts/voting/dao-voting-native-staked/src/hooks.rs new file mode 100644 index 000000000..a04d3b043 --- /dev/null +++ b/contracts/voting/dao-voting-native-staked/src/hooks.rs @@ -0,0 +1,51 @@ +use crate::state::HOOKS; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_binary, Addr, StdResult, Storage, SubMsg, Uint128, WasmMsg}; + +#[cw_serde] +pub enum StakeChangedHookMsg { + Stake { addr: Addr, amount: Uint128 }, + Unstake { addr: Addr, amount: Uint128 }, +} + +pub fn stake_hook_msgs( + storage: &dyn Storage, + addr: Addr, + amount: Uint128, +) -> StdResult> { + let msg = to_binary(&StakeChangedExecuteMsg::StakeChangeHook( + StakeChangedHookMsg::Stake { addr, amount }, + ))?; + HOOKS.prepare_hooks(storage, |a| { + let execute = WasmMsg::Execute { + contract_addr: a.to_string(), + msg: msg.clone(), + funds: vec![], + }; + Ok(SubMsg::new(execute)) + }) +} + +pub fn unstake_hook_msgs( + storage: &dyn Storage, + addr: Addr, + amount: Uint128, +) -> StdResult> { + let msg = to_binary(&StakeChangedExecuteMsg::StakeChangeHook( + StakeChangedHookMsg::Unstake { addr, amount }, + ))?; + HOOKS.prepare_hooks(storage, |a| { + let execute = WasmMsg::Execute { + contract_addr: a.to_string(), + msg: msg.clone(), + funds: vec![], + }; + Ok(SubMsg::new(execute)) + }) +} + +// This is just a helper to properly serialize the above message +#[cw_serde] +enum StakeChangedExecuteMsg { + StakeChangeHook(StakeChangedHookMsg), +} diff --git a/contracts/voting/dao-voting-native-staked/src/lib.rs b/contracts/voting/dao-voting-native-staked/src/lib.rs index d1800adbc..6c512e72b 100644 --- a/contracts/voting/dao-voting-native-staked/src/lib.rs +++ b/contracts/voting/dao-voting-native-staked/src/lib.rs @@ -2,6 +2,7 @@ pub mod contract; mod error; +pub mod hooks; pub mod msg; pub mod state; diff --git a/contracts/voting/dao-voting-native-staked/src/msg.rs b/contracts/voting/dao-voting-native-staked/src/msg.rs index bf836ca12..a8b562524 100644 --- a/contracts/voting/dao-voting-native-staked/src/msg.rs +++ b/contracts/voting/dao-voting-native-staked/src/msg.rs @@ -1,36 +1,44 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint128; use cw_utils::Duration; -use dao_dao_macros::voting_module_query; -use dao_interface::state::Admin; +use dao_dao_macros::{active_query, voting_module_query}; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; #[cw_serde] pub struct InstantiateMsg { - // Owner can update all configs including changing the owner. This will generally be a DAO. - pub owner: Option, - // Manager can update all configs except changing the owner. This will generally be an operations multisig for a DAO. - pub manager: Option, - // Token denom e.g. ujuno, or some ibc denom + /// Token denom e.g. ujuno, or some ibc denom pub denom: String, // How long until the tokens become liquid again pub unstaking_duration: Option, + /// The number or percentage of tokens that must be staked + /// for the DAO to be active + pub active_threshold: Option, } #[cw_serde] pub enum ExecuteMsg { + /// Stakes tokens with the contract to get voting power in the DAO Stake {}, - Unstake { - amount: Uint128, - }, - UpdateConfig { - owner: Option, - manager: Option, - duration: Option, - }, + /// Unstakes tokens so that they begin unbonding + Unstake { amount: Uint128 }, + /// Updates the contract configuration + UpdateConfig { duration: Option }, + /// Claims unstaked tokens that have completed the unbonding period Claim {}, + /// Sets the active threshold to a new value. Only the + /// instantiator of this contract (a DAO most likely) may call this + /// method. + UpdateActiveThreshold { + new_threshold: Option, + }, + /// Adds a hook that fires on staking / unstaking + AddHook { addr: String }, + /// Removes a hook that fires on staking / unstaking + RemoveHook { addr: String }, } #[voting_module_query] +#[active_query] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { @@ -38,11 +46,17 @@ pub enum QueryMsg { GetConfig {}, #[returns(cw_controllers::ClaimsResponse)] Claims { address: String }, + #[returns(DenomResponse)] + GetDenom {}, #[returns(ListStakersResponse)] ListStakers { start_after: Option, limit: Option, }, + #[returns(ActiveThresholdResponse)] + ActiveThreshold {}, + #[returns(GetHooksResponse)] + GetHooks {}, } #[cw_serde] @@ -58,3 +72,13 @@ pub struct StakerBalanceResponse { pub address: String, pub balance: Uint128, } + +#[cw_serde] +pub struct DenomResponse { + pub denom: String, +} + +#[cw_serde] +pub struct GetHooksResponse { + pub hooks: Vec, +} diff --git a/contracts/voting/dao-voting-native-staked/src/state.rs b/contracts/voting/dao-voting-native-staked/src/state.rs index 65849e89e..3e46d0281 100644 --- a/contracts/voting/dao-voting-native-staked/src/state.rs +++ b/contracts/voting/dao-voting-native-staked/src/state.rs @@ -1,19 +1,24 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; use cw_controllers::Claims; +use cw_hooks::Hooks; use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; use cw_utils::Duration; +use dao_voting::threshold::ActiveThreshold; #[cw_serde] pub struct Config { - pub owner: Option, - pub manager: Option, pub denom: String, pub unstaking_duration: Option, } +/// The configuration of this voting contract pub const CONFIG: Item = Item::new("config"); + +/// The address of the DAO that instantiated this contract pub const DAO: Item = Item::new("dao"); + +/// Keeps track of staked balances by address over time pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( "staked_balances", "staked_balance__checkpoints", @@ -21,6 +26,7 @@ pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( Strategy::EveryBlock, ); +/// Keeps track of staked total over time pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( "total_staked", "total_staked__checkpoints", @@ -32,3 +38,9 @@ pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( pub const MAX_CLAIMS: u64 = 100; pub const CLAIMS: Claims = Claims::new("claims"); + +/// The minimum amount of staked tokens for the DAO to be active +pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); + +/// Hooks to contracts that will receive staking and unstaking messages +pub const HOOKS: Hooks = Hooks::new("hooks"); diff --git a/contracts/voting/dao-voting-native-staked/src/tests.rs b/contracts/voting/dao-voting-native-staked/src/tests.rs index b6973ff33..75a3b5672 100644 --- a/contracts/voting/dao-voting-native-staked/src/tests.rs +++ b/contracts/voting/dao-voting-native-staked/src/tests.rs @@ -1,25 +1,28 @@ use crate::contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}; +use crate::error::ContractError; use crate::msg::{ - ExecuteMsg, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, + DenomResponse, ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, + QueryMsg, StakerBalanceResponse, }; use crate::state::Config; use cosmwasm_std::testing::{mock_dependencies, mock_env}; -use cosmwasm_std::{coins, Addr, Coin, Empty, Uint128}; +use cosmwasm_std::{coins, Addr, Coin, Decimal, Empty, Uint128}; use cw_controllers::ClaimsResponse; use cw_multi_test::{ custom_app, next_block, App, AppResponse, Contract, ContractWrapper, Executor, }; use cw_utils::Duration; -use dao_interface::state::Admin; use dao_interface::voting::{ - InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, + InfoResponse, IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, }; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; const DAO_ADDR: &str = "dao"; const ADDR1: &str = "addr1"; const ADDR2: &str = "addr2"; const DENOM: &str = "ujuno"; const INVALID_DENOM: &str = "uinvalid"; +const ODD_DENOM: &str = "uodd"; fn staking_contract() -> Box> { let contract = ContractWrapper::new( @@ -61,6 +64,10 @@ fn mock_app() -> App { denom: INVALID_DENOM.to_string(), amount: Uint128::new(10000), }, + Coin { + denom: ODD_DENOM.to_string(), + amount: Uint128::new(5), + }, ], ) .unwrap(); @@ -139,18 +146,12 @@ fn update_config( app: &mut App, staking_addr: Addr, sender: &str, - owner: Option, - manager: Option, duration: Option, ) -> anyhow::Result { app.execute_contract( Addr::unchecked(sender), staking_addr, - &ExecuteMsg::UpdateConfig { - owner, - manager, - duration, - }, + &ExecuteMsg::UpdateConfig { duration }, &[], ) } @@ -204,12 +205,9 @@ fn test_instantiate() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -218,63 +216,49 @@ fn test_instantiate() { &mut app, staking_id, InstantiateMsg { - owner: None, - manager: None, denom: DENOM.to_string(), unstaking_duration: None, + active_threshold: None, }, ); } #[test] -fn test_instantiate_dao_owner() { +#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] +fn test_instantiate_invalid_unstaking_duration_height() { let mut app = mock_app(); let staking_id = app.store_code(staking_contract()); - // Populated fields - let addr = instantiate_staking( + + // Populated fields with height + instantiate_staking( &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), - unstaking_duration: Some(Duration::Height(5)), + unstaking_duration: Some(Duration::Height(0)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), }, ); - - let config = get_config(&mut app, addr); - - assert_eq!(config.owner, Some(Addr::unchecked(DAO_ADDR))) } #[test] #[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] -fn test_instantiate_invalid_unstaking_duration() { +fn test_instantiate_invalid_unstaking_duration_time() { let mut app = mock_app(); let staking_id = app.store_code(staking_contract()); - // Populated fields - let _addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::Address { - addr: DAO_ADDR.to_string(), - }), - manager: Some(ADDR1.to_string()), - denom: DENOM.to_string(), - unstaking_duration: Some(Duration::Height(0)), - }, - ); - // Non populated fields - let _addr = instantiate_staking( + // Populated fields with height + instantiate_staking( &mut app, staking_id, InstantiateMsg { - owner: None, - manager: None, denom: DENOM.to_string(), - unstaking_duration: None, + unstaking_duration: Some(Duration::Time(0)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), }, ); } @@ -288,10 +272,9 @@ fn test_stake_invalid_denom() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -307,10 +290,9 @@ fn test_stake_valid_denom() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -328,10 +310,9 @@ fn test_unstake_none_staked() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -347,10 +328,9 @@ fn test_unstake_zero_tokens() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -366,10 +346,9 @@ fn test_unstake_invalid_balance() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -389,10 +368,9 @@ fn test_unstake() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -424,10 +402,9 @@ fn test_unstake_no_unstaking_duration() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: None, + active_threshold: None, }, ); @@ -461,10 +438,9 @@ fn test_claim_no_claims() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -480,10 +456,9 @@ fn test_claim_claim_not_reached() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -507,10 +482,9 @@ fn test_claim() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -558,167 +532,98 @@ fn test_update_config_invalid_sender() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); // From ADDR2, so not owner or manager - update_config( - &mut app, - addr, - ADDR2, - Some(ADDR1.to_string()), - Some(DAO_ADDR.to_string()), - Some(Duration::Height(10)), - ) - .unwrap(); + update_config(&mut app, addr, ADDR1, Some(Duration::Height(10))).unwrap(); } #[test] -#[should_panic(expected = "Only owner can change owner")] -fn test_update_config_non_owner_changes_owner() { +fn test_update_config_as_dao() { let mut app = mock_app(); let staking_id = app.store_code(staking_contract()); let addr = instantiate_staking( &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), - denom: DENOM.to_string(), - unstaking_duration: Some(Duration::Height(5)), - }, - ); - - // ADDR1 is the manager so cannot change the owner - update_config(&mut app, addr, ADDR1, Some(ADDR2.to_string()), None, None).unwrap(); -} - -#[test] -fn test_update_config_as_owner() { - let mut app = mock_app(); - let staking_id = app.store_code(staking_contract()); - let addr = instantiate_staking( - &mut app, - staking_id, - InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); // Swap owner and manager, change duration - update_config( - &mut app, - addr.clone(), - DAO_ADDR, - Some(ADDR1.to_string()), - Some(DAO_ADDR.to_string()), - Some(Duration::Height(10)), - ) - .unwrap(); + update_config(&mut app, addr.clone(), DAO_ADDR, Some(Duration::Height(10))).unwrap(); let config = get_config(&mut app, addr); assert_eq!( Config { - owner: Some(Addr::unchecked(ADDR1)), - manager: Some(Addr::unchecked(DAO_ADDR)), - unstaking_duration: Some(Duration::Height(10)), denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(10)), }, config ); } #[test] -fn test_update_config_as_manager() { +#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] +fn test_update_config_invalid_duration() { let mut app = mock_app(); let staking_id = app.store_code(staking_contract()); let addr = instantiate_staking( &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); // Change duration and manager as manager cannot change owner - update_config( - &mut app, - addr.clone(), - ADDR1, - Some(DAO_ADDR.to_string()), - Some(ADDR2.to_string()), - Some(Duration::Height(10)), - ) - .unwrap(); - - let config = get_config(&mut app, addr); - assert_eq!( - Config { - owner: Some(Addr::unchecked(DAO_ADDR)), - manager: Some(Addr::unchecked(ADDR2)), - unstaking_duration: Some(Duration::Height(10)), - denom: DENOM.to_string(), - }, - config - ); + update_config(&mut app, addr, DAO_ADDR, Some(Duration::Height(0))).unwrap(); } #[test] -#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] -fn test_update_config_invalid_duration() { +fn test_query_dao() { let mut app = mock_app(); let staking_id = app.store_code(staking_contract()); let addr = instantiate_staking( &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); - // Change duration and manager as manager cannot change owner - update_config( - &mut app, - addr, - ADDR1, - Some(DAO_ADDR.to_string()), - Some(ADDR2.to_string()), - Some(Duration::Height(0)), - ) - .unwrap(); + let msg = QueryMsg::Dao {}; + let dao: Addr = app.wrap().query_wasm_smart(addr, &msg).unwrap(); + assert_eq!(dao, Addr::unchecked(DAO_ADDR)); } #[test] -fn test_query_dao() { +fn test_query_denom() { let mut app = mock_app(); let staking_id = app.store_code(staking_contract()); let addr = instantiate_staking( &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); - let msg = QueryMsg::Dao {}; - let dao: Addr = app.wrap().query_wasm_smart(addr, &msg).unwrap(); - assert_eq!(dao, Addr::unchecked(DAO_ADDR)); + let msg = QueryMsg::GetDenom {}; + let denom: DenomResponse = app.wrap().query_wasm_smart(addr, &msg).unwrap(); + assert_eq!(denom.denom, DENOM.to_string()); } #[test] @@ -729,10 +634,9 @@ fn test_query_info() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -749,10 +653,9 @@ fn test_query_claims() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -785,10 +688,9 @@ fn test_query_get_config() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -796,8 +698,6 @@ fn test_query_get_config() { assert_eq!( config, Config { - owner: Some(Addr::unchecked(DAO_ADDR)), - manager: Some(Addr::unchecked(ADDR1)), unstaking_duration: Some(Duration::Height(5)), denom: DENOM.to_string(), } @@ -812,10 +712,9 @@ fn test_voting_power_queries() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -918,10 +817,9 @@ fn test_query_list_stakers() { &mut app, staking_id, InstantiateMsg { - owner: Some(Admin::CoreModule {}), - manager: Some(ADDR1.to_string()), denom: DENOM.to_string(), unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -994,6 +892,374 @@ fn test_query_list_stakers() { assert_eq!(stakers, ListStakersResponse { stakers: vec![] }); } +#[test] +#[should_panic(expected = "Active threshold count must be greater than zero")] +fn test_instantiate_zero_active_threshold_count() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::zero(), + }), + }, + ); +} + +#[test] +fn test_active_threshold_absolute_count() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 100 tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(20), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 6000 tokens, now active + stake_tokens(&mut app, addr.clone(), ADDR1, 6000, DENOM).unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent_rounds_up() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: ODD_DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 2 tokens, should not be active. + stake_tokens(&mut app, addr.clone(), ADDR1, 2, ODD_DENOM).unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 1 more token, should now be active. + stake_tokens(&mut app, addr.clone(), ADDR1, 1, ODD_DENOM).unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_none() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Active as no threshold + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_update_active_threshold() { + let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!(resp.active_threshold, None); + + let msg = ExecuteMsg::UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }; + + // Expect failure as sender is not the DAO + app.execute_contract(Addr::unchecked(ADDR1), addr.clone(), &msg, &[]) + .unwrap_err(); + + // Expect success as sender is the DAO + app.execute_contract(Addr::unchecked(DAO_ADDR), addr.clone(), &msg, &[]) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!( + resp.active_threshold, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100) + }) + ); + + // Can't set threshold to invalid value + let msg = ExecuteMsg::UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(120), + }), + }; + let err: ContractError = app + .execute_contract(Addr::unchecked(DAO_ADDR), addr.clone(), &msg, &[]) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::InvalidActivePercentage {}); + + // Remove threshold + let msg = ExecuteMsg::UpdateActiveThreshold { + new_threshold: None, + }; + app.execute_contract(Addr::unchecked(DAO_ADDR), addr.clone(), &msg, &[]) + .unwrap(); + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!(resp.active_threshold, None); +} + +#[test] +#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +fn test_active_threshold_percentage_gt_100() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(120), + }), + }, + ); +} + +#[test] +#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +fn test_active_threshold_percentage_lte_0() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(0), + }), + }, + ); +} + +#[test] +#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] +fn test_active_threshold_absolute_count_invalid() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(301), + }), + }, + ); +} + +#[test] +fn test_add_remove_hooks() { + let mut app = App::default(); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + denom: DENOM.to_string(), + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // No hooks exist. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, Vec::::new()); + + // Non-owner can't add hook + let err: ContractError = app + .execute_contract( + Addr::unchecked(ADDR2), + addr.clone(), + &ExecuteMsg::AddHook { + addr: "hook".to_string(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Add a hook. + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::AddHook { + addr: "hook".to_string(), + }, + &[], + ) + .unwrap(); + + // One hook exists. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, vec!["hook".to_string()]); + + // Non-owner can't remove hook + let err: ContractError = app + .execute_contract( + Addr::unchecked(ADDR2), + addr.clone(), + &ExecuteMsg::RemoveHook { + addr: "hook".to_string(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Remove hook. + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::RemoveHook { + addr: "hook".to_string(), + }, + &[], + ) + .unwrap(); + + // No hook exists. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, Vec::::new()); +} + #[test] pub fn test_migrate_update_version() { let mut deps = mock_dependencies(); diff --git a/contracts/voting/dao-voting-token-factory-staked/.cargo/config b/contracts/voting/dao-voting-token-factory-staked/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/voting/dao-voting-token-factory-staked/Cargo.toml b/contracts/voting/dao-voting-token-factory-staked/Cargo.toml new file mode 100644 index 000000000..d66450ba0 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "dao-voting-token-factory-staked" +authors = ["Callum Anderson ", "Noah Saso ", "Jake Hartnell "] +description = "A DAO DAO voting module based on staked token factory or native tokens. Only works with chains that support Token Factory." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] +# use test tube feature to enable test-tube integration tests, for example +# cargo test --features "test-tube" +test-tube = [] +# when writing tests you may wish to enable test-tube as a default feature +# default = ["test-tube"] + +[dependencies] +cosmwasm-std = { workspace = true, features = ["cosmwasm_1_1"] } +cosmwasm-schema = { workspace = true } +cosmwasm-storage = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +cw-utils = { workspace = true } +cw-controllers = { workspace = true } +cw-hooks = { workspace = true } +thiserror = { workspace = true } +dao-dao-macros = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } +cw-paginate-storage = { workspace = true } +cw-tokenfactory-issuer = { workspace = true, features = ["library"] } +token-bindings = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +# TODO use upstream when PR merged and new release tagged: https://github.com/CosmWasm/cw-multi-test/pull/51 +cw-multi-test = { git = "https://github.com/JakeHartnell/cw-multi-test.git", branch = "bank-supply-support" } +cw-tokenfactory-issuer = { workspace = true } +dao-testing = { workspace = true, features = ["test-tube"] } +osmosis-std = { workpsace = true } +osmosis-test-tube = { workspace = true } +serde = { workspace = true } +token-bindings-test = { workspace = true } diff --git a/contracts/voting/dao-voting-token-factory-staked/README.md b/contracts/voting/dao-voting-token-factory-staked/README.md new file mode 100644 index 000000000..ad427dc0b --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/README.md @@ -0,0 +1,76 @@ +# `dao_voting_token_factory_staked` + +Simple native or Token Factory based token voting / staking contract which assumes the native denom provided is not used for staking for securing the network e.g. IBC denoms or secondary tokens (ION). Staked balances may be queried at an arbitrary height. This contract implements the interface needed to be a DAO DAO [voting module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module). + +`dao_voting_token_factory_staked` leverages the `cw_tokenfactory_issuer` contract for tokenfactory functionality. When instantiated, `dao_voting_token_factory_staked` creates a new `cw_tokenfactory_issuer` contract to manage the new Token, with the DAO as admin and owner (these can be renounced or updated by vote of the DAO). + +NOTE: This contract requires having the Token Factory module on your chain, which allows the creation of new native tokens. If your chain does not have this module, use `dao-voting-native-staked` instead. + +## Instantiation +When instantiating a new `dao_voting_token_factory_staked` contract there are two required fields: +- `token_issuer_code_id`: must be set to a valid Code ID for the `cw_tokenfactory_issuer` contract. +- `token_info`: you have the option to leverage an `existing` token or creating a `new` one. + +There are a few optional fields: +- `unstaking_duration`: can be set to `height` or `time` (in seconds), this is the amount of time that must elapse before a user can claim fully unstaked tokens. If not set, they are instantly claimable. +- `active_theshold`: the amount of tokens that must be staked for the DAO to be active. This may be either an `absolute_count` or a `percentage`. + +### Create a New Token +Creating a token has a few additional optional fields: +- `metadata`: information about the token. See [Cosmos SDK Coin metadata documentation](https://docs.cosmos.network/main/architecture/adr-024-coin-metadata) for more info on coin metadata. +- `initial_dao_balance`: the initial balance created for the DAO. + +Example insantiation mesggage: +``` json +{ + "token_issuer_code_id": , + "token_info": { + "new": { + "subdenom": "meow", + "metadata": { + "description": "Meow!", + "additional_denom_units": [ + { + "denom": "roar", + "exponent": 6, + "aliases": [] + } + ], + "display": "meow", + "name": "Cat Token", + "symbol": "MEOW" + }, + "initial_balances": [ + { + "amount": "100000000", + "address": "
" + } + ], + "initial_dao_balance": "100000000000" + } + }, + "unstaking_duration": { + "time": 100000 + }, + "active_threshold": { + "percentage": { + "percent": "0.1" + } + } +} +``` + +### Use Existing +Example insantiation mesggage: + +``` json +{ + "token_issuer_code_id": , + "token_info": { + "new": { + "subdenom": "factory/{address}/{denom}", + } +} +``` + +When leveraging an existing token, cetain `cw_tokenfactory_issuer` features will not work until admin of the Token is transferred over to the `cw_tokenfactory_issuer` contract. diff --git a/contracts/voting/dao-voting-token-factory-staked/examples/schema.rs b/contracts/voting/dao-voting-token-factory-staked/examples/schema.rs new file mode 100644 index 000000000..2aa85dbff --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/examples/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use dao_voting_token_factory_staked::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/voting/dao-voting-token-factory-staked/schema/dao-voting-token-factory-staked.json b/contracts/voting/dao-voting-token-factory-staked/schema/dao-voting-token-factory-staked.json new file mode 100644 index 000000000..624c8be17 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/schema/dao-voting-token-factory-staked.json @@ -0,0 +1,1183 @@ +{ + "contract_name": "dao-voting-token-factory-staked", + "contract_version": "2.2.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "token_info", + "token_issuer_code_id" + ], + "properties": { + "active_threshold": { + "description": "The number or percentage of tokens that must be staked for the DAO to be active", + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + }, + "token_info": { + "description": "New or existing native token to use for voting power.", + "allOf": [ + { + "$ref": "#/definitions/TokenInfo" + } + ] + }, + "token_issuer_code_id": { + "description": "The code id of the cw-tokenfactory-issuer contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unstaking_duration": { + "description": "How long until the tokens become liquid again", + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "DenomUnit": { + "description": "DenomUnit represents a struct that describes a given denomination unit of the basic token.", + "type": "object", + "required": [ + "aliases", + "denom", + "exponent" + ], + "properties": { + "aliases": { + "description": "aliases is a list of string aliases for the given denom", + "type": "array", + "items": { + "type": "string" + } + }, + "denom": { + "description": "denom represents the string name of the given denom unit (e.g uatom).", + "type": "string" + }, + "exponent": { + "description": "exponent represents power of 10 exponent that one must raise the base_denom to in order to equal the given DenomUnit's denom 1 denom = 1^exponent base_denom (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with exponent = 6, thus: 1 atom = 10^6 uatom).", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "oneOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Time in seconds", + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "InitialBalance": { + "type": "object", + "required": [ + "address", + "amount" + ], + "properties": { + "address": { + "type": "string" + }, + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "NewDenomMetadata": { + "type": "object", + "required": [ + "description", + "display", + "name", + "symbol" + ], + "properties": { + "additional_denom_units": { + "description": "Used define additional units of the token (e.g. \"tiger\") These must have an exponent larger than 0.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/DenomUnit" + } + }, + "description": { + "description": "The description of the token", + "type": "string" + }, + "display": { + "description": "The unit commonly used in communication (e.g. \"cat\")", + "type": "string" + }, + "name": { + "description": "The name of the token (e.g. \"Cat Coin\")", + "type": "string" + }, + "symbol": { + "description": "The ticker symbol of the token (e.g. \"CAT\")", + "type": "string" + } + }, + "additionalProperties": false + }, + "NewTokenInfo": { + "type": "object", + "required": [ + "initial_balances", + "subdenom" + ], + "properties": { + "initial_balances": { + "description": "The initial balances to set for the token, cannot be empty.", + "type": "array", + "items": { + "$ref": "#/definitions/InitialBalance" + } + }, + "initial_dao_balance": { + "description": "Optional balance to mint for the DAO.", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "description": "Optional metadata for the token, this can additionally be set later.", + "anyOf": [ + { + "$ref": "#/definitions/NewDenomMetadata" + }, + { + "type": "null" + } + ] + }, + "subdenom": { + "description": "The subdenom of the token to create, will also be used as an alias for the denom. The Token Factory denom will have the format of factory/{contract_address}/{subdenom}", + "type": "string" + } + }, + "additionalProperties": false + }, + "TokenInfo": { + "oneOf": [ + { + "description": "Uses an existing Token Factory token and creates a new issuer contract. Full setup, such as transferring ownership or setting up MsgSetBeforeSendHook, must be done manually. Note, for chain controlled denoms or IBC tokens use dao-voting-native-staked.", + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "description": "Token factory denom", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Creates a new Token Factory token via the issue contract with the DAO automatically setup as admin and owner.", + "type": "object", + "required": [ + "new" + ], + "properties": { + "new": { + "$ref": "#/definitions/NewTokenInfo" + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Stakes tokens with the contract to get voting power in the DAO", + "type": "object", + "required": [ + "stake" + ], + "properties": { + "stake": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Unstakes tokens so that they begin unbonding", + "type": "object", + "required": [ + "unstake" + ], + "properties": { + "unstake": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Updates the contract configuration", + "type": "object", + "required": [ + "update_config" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "duration": { + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Claims unstaked tokens that have completed the unbonding period", + "type": "object", + "required": [ + "claim" + ], + "properties": { + "claim": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets the active threshold to a new value. Only the instantiator of this contract (a DAO most likely) may call this method.", + "type": "object", + "required": [ + "update_active_threshold" + ], + "properties": { + "update_active_threshold": { + "type": "object", + "properties": { + "new_threshold": { + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Adds a hook that fires on staking / unstaking", + "type": "object", + "required": [ + "add_hook" + ], + "properties": { + "add_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes a hook that fires on staking / unstaking", + "type": "object", + "required": [ + "remove_hook" + ], + "properties": { + "remove_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "oneOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Time in seconds", + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "get_config" + ], + "properties": { + "get_config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "claims" + ], + "properties": { + "claims": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "list_stakers" + ], + "properties": { + "list_stakers": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "active_threshold" + ], + "properties": { + "active_threshold": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "get_hooks" + ], + "properties": { + "get_hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "is_active" + ], + "properties": { + "is_active": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the voting power for an address at a given height.", + "type": "object", + "required": [ + "voting_power_at_height" + ], + "properties": { + "voting_power_at_height": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total voting power at a given block heigh.", + "type": "object", + "required": [ + "total_power_at_height" + ], + "properties": { + "total_power_at_height": { + "type": "object", + "properties": { + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the address of the DAO this module belongs to.", + "type": "object", + "required": [ + "dao" + ], + "properties": { + "dao": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract version info.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "token_contract" + ], + "properties": { + "token_contract": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "active_threshold": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ActiveThresholdResponse", + "type": "object", + "properties": { + "active_threshold": { + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "claims": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ClaimsResponse", + "type": "object", + "required": [ + "claims" + ], + "properties": { + "claims": { + "type": "array", + "items": { + "$ref": "#/definitions/Claim" + } + } + }, + "additionalProperties": false, + "definitions": { + "Claim": { + "type": "object", + "required": [ + "amount", + "release_at" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "release_at": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "dao": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Addr", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "denom": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DenomResponse", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "get_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "properties": { + "unstaking_duration": { + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "oneOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Time in seconds", + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + } + } + }, + "get_hooks": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GetHooksResponse", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InfoResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "$ref": "#/definitions/ContractVersion" + } + }, + "additionalProperties": false, + "definitions": { + "ContractVersion": { + "type": "object", + "required": [ + "contract", + "version" + ], + "properties": { + "contract": { + "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing", + "type": "string" + }, + "version": { + "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "is_active": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, + "list_stakers": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ListStakersResponse", + "type": "object", + "required": [ + "stakers" + ], + "properties": { + "stakers": { + "type": "array", + "items": { + "$ref": "#/definitions/StakerBalanceResponse" + } + } + }, + "additionalProperties": false, + "definitions": { + "StakerBalanceResponse": { + "type": "object", + "required": [ + "address", + "balance" + ], + "properties": { + "address": { + "type": "string" + }, + "balance": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "token_contract": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Addr", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "total_power_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "voting_power_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VotingPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/contract.rs b/contracts/voting/dao-voting-token-factory-staked/src/contract.rs new file mode 100644 index 000000000..60f8b8cb3 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/contract.rs @@ -0,0 +1,711 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +use cosmwasm_std::{ + coins, to_binary, BankMsg, BankQuery, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, + MessageInfo, Order, Reply, Response, StdResult, SubMsg, Uint128, Uint256, WasmMsg, +}; +use cw2::set_contract_version; +use cw_controllers::ClaimsResponse; +use cw_storage_plus::Bound; +use cw_tokenfactory_issuer::msg::{ + DenomUnit, ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, Metadata, +}; +use cw_utils::{maybe_addr, must_pay, parse_reply_instantiate_data, Duration}; +use dao_interface::voting::{ + IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; + +use crate::error::ContractError; +use crate::hooks::{stake_hook_msgs, unstake_hook_msgs}; +use crate::msg::{ + DenomResponse, ExecuteMsg, GetHooksResponse, InitialBalance, InstantiateMsg, + ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, TokenInfo, +}; +use crate::state::{ + Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, DENOM, HOOKS, MAX_CLAIMS, STAKED_BALANCES, + STAKED_TOTAL, TOKEN_INSTANTIATION_INFO, TOKEN_ISSUER_CONTRACT, +}; + +pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-token-factory-staked"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Settings for query pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +const INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID: u64 = 0; + +// We multiply by this when calculating needed power for being active +// when using active threshold with percent +const PRECISION_FACTOR: u128 = 10u128.pow(9); + +fn validate_duration(duration: Option) -> Result<(), ContractError> { + if let Some(unstaking_duration) = duration { + match unstaking_duration { + Duration::Height(height) => { + if height == 0 { + return Err(ContractError::InvalidUnstakingDuration {}); + } + } + Duration::Time(time) => { + if time == 0 { + return Err(ContractError::InvalidUnstakingDuration {}); + } + } + } + } + Ok(()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result, ContractError> { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + validate_duration(msg.unstaking_duration)?; + + let config = Config { + unstaking_duration: msg.unstaking_duration, + }; + + CONFIG.save(deps.storage, &config)?; + DAO.save(deps.storage, &info.sender)?; + + if let Some(active_threshold) = msg.active_threshold.as_ref() { + if let ActiveThreshold::Percentage { percent } = active_threshold { + if *percent > Decimal::percent(100) || *percent <= Decimal::percent(0) { + return Err(ContractError::InvalidActivePercentage {}); + } + } + ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?; + } + + // Save new token info for use in reply + TOKEN_INSTANTIATION_INFO.save(deps.storage, &msg.token_info)?; + + match msg.token_info { + TokenInfo::Existing { denom } => { + if let Some(ActiveThreshold::AbsoluteCount { count }) = msg.active_threshold { + assert_valid_absolute_count_threshold(deps.as_ref(), &denom, count)?; + } + + DENOM.save(deps.storage, &denom)?; + + // Instantiate cw-token-factory-issuer contract + // DAO (sender) is set as admin + let issuer_instantiate_msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + admin: Some(info.sender.to_string()), + code_id: msg.token_issuer_code_id, + msg: to_binary(&IssuerInstantiateMsg::ExistingToken { + denom: denom.clone(), + })?, + funds: info.funds, + label: "cw-tokenfactory-issuer".to_string(), + }, + INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, + ); + + Ok(Response::::new() + .add_attribute("action", "instantiate") + .add_attribute("token", "existing_token") + .add_attribute("token_denom", denom) + .add_submessage(issuer_instantiate_msg)) + } + TokenInfo::New(token) => { + // Tnstantiate cw-token-factory-issuer contract + // DAO (sender) is set as contract admin + let issuer_instantiate_msg = SubMsg::reply_always( + WasmMsg::Instantiate { + admin: Some(info.sender.to_string()), + code_id: msg.token_issuer_code_id, + msg: to_binary(&IssuerInstantiateMsg::NewToken { + subdenom: token.subdenom.clone(), + })?, + funds: info.funds, + label: "cw-tokenfactory-issuer".to_string(), + }, + INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID, + ); + + Ok(Response::::new() + .add_attribute("action", "instantiate") + .add_attribute("token", "new_token") + .add_submessage(issuer_instantiate_msg)) + } + } +} + +pub fn assert_valid_absolute_count_threshold( + deps: Deps, + token_denom: &str, + count: Uint128, +) -> Result<(), ContractError> { + if count.is_zero() { + return Err(ContractError::ZeroActiveCount {}); + } + let supply: Coin = deps.querier.query_supply(token_denom.to_string())?; + if count > supply.amount { + return Err(ContractError::InvalidAbsoluteCount {}); + } + Ok(()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + match msg { + ExecuteMsg::Stake {} => execute_stake(deps, env, info), + ExecuteMsg::Unstake { amount } => execute_unstake(deps, env, info, amount), + ExecuteMsg::UpdateConfig { duration } => execute_update_config(deps, info, duration), + ExecuteMsg::Claim {} => execute_claim(deps, env, info), + ExecuteMsg::UpdateActiveThreshold { new_threshold } => { + execute_update_active_threshold(deps, env, info, new_threshold) + } + ExecuteMsg::AddHook { addr } => execute_add_hook(deps, env, info, addr), + ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, env, info, addr), + } +} + +pub fn execute_stake( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result, ContractError> { + let denom = DENOM.load(deps.storage)?; + let amount = must_pay(&info, &denom)?; + + STAKED_BALANCES.update( + deps.storage, + &info.sender, + env.block.height, + |balance| -> StdResult { Ok(balance.unwrap_or_default().checked_add(amount)?) }, + )?; + STAKED_TOTAL.update( + deps.storage, + env.block.height, + |total| -> StdResult { Ok(total.unwrap_or_default().checked_add(amount)?) }, + )?; + let hook_msgs = stake_hook_msgs(deps.storage, info.sender.clone(), amount)?; + + Ok(Response::::new() + .add_submessages(hook_msgs) + .add_attribute("action", "stake") + .add_attribute("amount", amount.to_string()) + .add_attribute("from", info.sender)) +} + +pub fn execute_unstake( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, +) -> Result, ContractError> { + if amount.is_zero() { + return Err(ContractError::ZeroUnstake {}); + } + + STAKED_BALANCES.update( + deps.storage, + &info.sender, + env.block.height, + |balance| -> Result { + balance + .unwrap_or_default() + .checked_sub(amount) + .map_err(|_e| ContractError::InvalidUnstakeAmount {}) + }, + )?; + STAKED_TOTAL.update( + deps.storage, + env.block.height, + |total| -> Result { + total + .unwrap_or_default() + .checked_sub(amount) + .map_err(|_e| ContractError::InvalidUnstakeAmount {}) + }, + )?; + let hook_msgs = unstake_hook_msgs(deps.storage, info.sender.clone(), amount)?; + + let config = CONFIG.load(deps.storage)?; + let denom = DENOM.load(deps.storage)?; + match config.unstaking_duration { + None => { + let msg = CosmosMsg::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: coins(amount.u128(), denom), + }); + Ok(Response::::new() + .add_message(msg) + .add_submessages(hook_msgs) + .add_attribute("action", "unstake") + .add_attribute("from", info.sender) + .add_attribute("amount", amount) + .add_attribute("claim_duration", "None")) + } + Some(duration) => { + let outstanding_claims = CLAIMS.query_claims(deps.as_ref(), &info.sender)?.claims; + if outstanding_claims.len() >= MAX_CLAIMS as usize { + return Err(ContractError::TooManyClaims {}); + } + + CLAIMS.create_claim( + deps.storage, + &info.sender, + amount, + duration.after(&env.block), + )?; + Ok(Response::::new() + .add_submessages(hook_msgs) + .add_attribute("action", "unstake") + .add_attribute("from", info.sender) + .add_attribute("amount", amount) + .add_attribute("claim_duration", format!("{duration}"))) + } + } +} + +pub fn execute_update_config( + deps: DepsMut, + info: MessageInfo, + duration: Option, +) -> Result, ContractError> { + let mut config: Config = CONFIG.load(deps.storage)?; + + // Only the DAO can update the config + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + validate_duration(duration)?; + + config.unstaking_duration = duration; + + CONFIG.save(deps.storage, &config)?; + Ok(Response::::new().add_attribute("action", "update_config")) +} + +pub fn execute_claim( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result, ContractError> { + let release = CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, None)?; + if release.is_zero() { + return Err(ContractError::NothingToClaim {}); + } + + let denom = DENOM.load(deps.storage)?; + let msg = CosmosMsg::::Bank(BankMsg::Send { + to_address: info.sender.to_string(), + amount: coins(release.u128(), denom), + }); + + Ok(Response::::new() + .add_message(msg) + .add_attribute("action", "claim") + .add_attribute("from", info.sender) + .add_attribute("amount", release)) +} + +pub fn execute_update_active_threshold( + deps: DepsMut, + _env: Env, + info: MessageInfo, + new_active_threshold: Option, +) -> Result, ContractError> { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + if let Some(active_threshold) = new_active_threshold { + match active_threshold { + ActiveThreshold::Percentage { percent } => { + if percent > Decimal::percent(100) || percent.is_zero() { + return Err(ContractError::InvalidActivePercentage {}); + } + } + ActiveThreshold::AbsoluteCount { count } => { + let denom = DENOM.load(deps.storage)?; + assert_valid_absolute_count_threshold(deps.as_ref(), &denom, count)?; + } + } + ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; + } else { + ACTIVE_THRESHOLD.remove(deps.storage); + } + + Ok(Response::::new().add_attribute("action", "update_active_threshold")) +} + +pub fn execute_add_hook( + deps: DepsMut, + _env: Env, + info: MessageInfo, + addr: String, +) -> Result, ContractError> { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + let hook = deps.api.addr_validate(&addr)?; + HOOKS.add_hook(deps.storage, hook)?; + Ok(Response::::new() + .add_attribute("action", "add_hook") + .add_attribute("hook", addr)) +} + +pub fn execute_remove_hook( + deps: DepsMut, + _env: Env, + info: MessageInfo, + addr: String, +) -> Result, ContractError> { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + let hook = deps.api.addr_validate(&addr)?; + HOOKS.remove_hook(deps.storage, hook)?; + Ok(Response::::new() + .add_attribute("action", "remove_hook") + .add_attribute("hook", addr)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::VotingPowerAtHeight { address, height } => { + to_binary(&query_voting_power_at_height(deps, env, address, height)?) + } + QueryMsg::TotalPowerAtHeight { height } => { + to_binary(&query_total_power_at_height(deps, env, height)?) + } + QueryMsg::Info {} => query_info(deps), + QueryMsg::Dao {} => query_dao(deps), + QueryMsg::Claims { address } => to_binary(&query_claims(deps, address)?), + QueryMsg::GetConfig {} => to_binary(&CONFIG.load(deps.storage)?), + QueryMsg::Denom {} => to_binary(&DenomResponse { + denom: DENOM.load(deps.storage)?, + }), + QueryMsg::ListStakers { start_after, limit } => { + query_list_stakers(deps, start_after, limit) + } + QueryMsg::IsActive {} => query_is_active(deps), + QueryMsg::ActiveThreshold {} => query_active_threshold(deps), + QueryMsg::GetHooks {} => to_binary(&query_hooks(deps)?), + QueryMsg::TokenContract {} => to_binary(&TOKEN_ISSUER_CONTRACT.load(deps.storage)?), + } +} + +pub fn query_voting_power_at_height( + deps: Deps, + env: Env, + address: String, + height: Option, +) -> StdResult { + let height = height.unwrap_or(env.block.height); + let address = deps.api.addr_validate(&address)?; + let power = STAKED_BALANCES + .may_load_at_height(deps.storage, &address, height)? + .unwrap_or_default(); + Ok(VotingPowerAtHeightResponse { power, height }) +} + +pub fn query_total_power_at_height( + deps: Deps, + env: Env, + height: Option, +) -> StdResult { + let height = height.unwrap_or(env.block.height); + let power = STAKED_TOTAL + .may_load_at_height(deps.storage, height)? + .unwrap_or_default(); + Ok(TotalPowerAtHeightResponse { power, height }) +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = cw2::get_contract_version(deps.storage)?; + to_binary(&dao_interface::voting::InfoResponse { info }) +} + +pub fn query_dao(deps: Deps) -> StdResult { + let dao = DAO.load(deps.storage)?; + to_binary(&dao) +} + +pub fn query_claims(deps: Deps, address: String) -> StdResult { + CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?) +} + +pub fn query_list_stakers( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let addr = maybe_addr(deps.api, start_after)?; + let start = addr.as_ref().map(Bound::exclusive); + + let stakers = STAKED_BALANCES + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + item.map(|(address, balance)| StakerBalanceResponse { + address: address.into_string(), + balance, + }) + }) + .collect::>()?; + + to_binary(&ListStakersResponse { stakers }) +} + +pub fn query_is_active(deps: Deps) -> StdResult { + let threshold = ACTIVE_THRESHOLD.may_load(deps.storage)?; + if let Some(threshold) = threshold { + let denom = DENOM.load(deps.storage)?; + let actual_power = STAKED_TOTAL.may_load(deps.storage)?.unwrap_or_default(); + match threshold { + ActiveThreshold::AbsoluteCount { count } => to_binary(&IsActiveResponse { + active: actual_power >= count, + }), + ActiveThreshold::Percentage { percent } => { + // percent is bounded between [0, 100]. decimal + // represents percents in u128 terms as p * + // 10^15. this bounds percent between [0, 10^17]. + // + // total_potential_power is bounded between [0, 2^128] + // as it tracks the balances of a cw20 token which has + // a max supply of 2^128. + // + // with our precision factor being 10^9: + // + // total_power <= 2^128 * 10^9 <= 2^256 + // + // so we're good to put that in a u256. + // + // multiply_ratio promotes to a u512 under the hood, + // so it won't overflow, multiplying by a percent less + // than 100 is gonna make something the same size or + // smaller, applied + 10^9 <= 2^128 * 10^9 + 10^9 <= + // 2^256, so the top of the round won't overflow, and + // rounding is rounding down, so the whole thing can + // be safely unwrapped at the end of the day thank you + // for coming to my ted talk. + let total_potential_power: cosmwasm_std::SupplyResponse = + deps.querier + .query(&cosmwasm_std::QueryRequest::Bank(BankQuery::Supply { + denom, + }))?; + let total_power = total_potential_power + .amount + .amount + .full_mul(PRECISION_FACTOR); + // under the hood decimals are `atomics / 10^decimal_places`. + // cosmwasm doesn't give us a Decimal * Uint256 + // implementation so we take the decimal apart and + // multiply by the fraction. + let applied = total_power.multiply_ratio( + percent.atomics(), + Uint256::from(10u64).pow(percent.decimal_places()), + ); + let rounded = (applied + Uint256::from(PRECISION_FACTOR) - Uint256::from(1u128)) + / Uint256::from(PRECISION_FACTOR); + let count: Uint128 = rounded.try_into().unwrap(); + to_binary(&IsActiveResponse { + active: actual_power >= count, + }) + } + } + } else { + to_binary(&IsActiveResponse { active: true }) + } +} + +pub fn query_active_threshold(deps: Deps) -> StdResult { + to_binary(&ActiveThresholdResponse { + active_threshold: ACTIVE_THRESHOLD.may_load(deps.storage)?, + }) +} + +pub fn query_hooks(deps: Deps) -> StdResult { + Ok(GetHooksResponse { + hooks: HOOKS.query_hooks(deps)?.hooks, + }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate( + deps: DepsMut, + _env: Env, + _msg: MigrateMsg, +) -> Result, ContractError> { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::::default()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply( + deps: DepsMut, + env: Env, + msg: Reply, +) -> Result, ContractError> { + match msg.id { + INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID => { + // Parse and save address of cw-tokenfactory-issuer + let issuer_addr = parse_reply_instantiate_data(msg)?.contract_address; + TOKEN_ISSUER_CONTRACT.save(deps.storage, &deps.api.addr_validate(&issuer_addr)?)?; + + // Load info for new token and remove temporary data + let token_info = TOKEN_INSTANTIATION_INFO.load(deps.storage)?; + TOKEN_INSTANTIATION_INFO.remove(deps.storage); + + match token_info { + TokenInfo::Existing { .. } => { + // Not much to do here. + Ok( + Response::new() + .add_attribute("cw-tokenfactory-issuer-address", issuer_addr), + ) + } + TokenInfo::New(token) => { + // Load the DAO address + let dao = DAO.load(deps.storage)?; + + // Format the denom and save it + let denom = format!("factory/{}/{}", &issuer_addr, token.subdenom); + + DENOM.save(deps.storage, &denom)?; + + // Check supply is greater than zero, iterate through initial + // balances and sum them, add DAO balance as well. + let initial_supply = token + .initial_balances + .iter() + .fold(Uint128::zero(), |previous, new_balance| { + previous + new_balance.amount + }); + let total_supply = + initial_supply + token.initial_dao_balance.unwrap_or_default(); + + // Cannot instantiate with no initial token owners because it would + // immediately lock the DAO. + if initial_supply.is_zero() { + return Err(ContractError::InitialBalancesError {}); + } + + // Msgs to be executed to finalize setup + let mut msgs: Vec = vec![]; + + // Grant an allowance to mint the initial supply + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::SetMinterAllowance { + address: env.contract.address.to_string(), + allowance: total_supply, + })?, + funds: vec![], + }); + + // If metadata, set it by calling the contract + if let Some(metadata) = token.metadata { + // The first denom_unit must be the same as the tf and base denom. + // It must have an exponent of 0. This the smallest unit of the token. + // For more info: // https://docs.cosmos.network/main/architecture/adr-024-coin-metadata + let mut denom_units = vec![DenomUnit { + denom: denom.clone(), + exponent: 0, + aliases: vec![token.subdenom], + }]; + + // Caller can optionally define additional units + if let Some(mut additional_units) = metadata.additional_denom_units { + denom_units.append(&mut additional_units); + } + + // Sort denom units by exponent, must be in ascending order + denom_units.sort_by(|a, b| a.exponent.cmp(&b.exponent)); + + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::SetDenomMetadata { + metadata: Metadata { + description: metadata.description, + denom_units, + base: denom.clone(), + display: metadata.display, + name: metadata.name, + symbol: metadata.symbol, + }, + })?, + funds: vec![], + }); + } + + // Call issuer contract to mint tokens for initial balances + token + .initial_balances + .iter() + .for_each(|b: &InitialBalance| { + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::Mint { + to_address: b.address.clone(), + amount: b.amount, + }) + .unwrap_or_default(), + funds: vec![], + }); + }); + + // Add initial DAO balance to initial_balances if nonzero. + if let Some(initial_dao_balance) = token.initial_dao_balance { + if !initial_dao_balance.is_zero() { + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::Mint { + to_address: dao.to_string().clone(), + amount: initial_dao_balance, + })?, + funds: vec![], + }); + } + } + + // Update issuer contract owner to be the DAO + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::ChangeContractOwner { + new_owner: dao.to_string(), + })?, + funds: vec![], + }); + + Ok(Response::new() + .add_attribute("cw-tokenfactory-issuer-address", issuer_addr) + .add_attribute("denom", denom) + .add_messages(msgs)) + } + } + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/error.rs b/contracts/voting/dao-voting-token-factory-staked/src/error.rs new file mode 100644 index 000000000..2a5425e0b --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/error.rs @@ -0,0 +1,51 @@ +use cosmwasm_std::StdError; +use cw_utils::{ParseReplyError, PaymentError}; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + PaymentError(#[from] PaymentError), + + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + + #[error(transparent)] + HookError(#[from] cw_hooks::HookError), + + #[error("Absolute count threshold cannot be greater than the total token supply")] + InvalidAbsoluteCount {}, + + #[error("Active threshold percentage must be greater than 0 and less than 1")] + InvalidActivePercentage {}, + + #[error("Initial governance token balances must not be empty")] + InitialBalancesError {}, + + #[error("Can only unstake less than or equal to the amount you have staked")] + InvalidUnstakeAmount {}, + + #[error("Invalid unstaking duration, unstaking duration cannot be 0")] + InvalidUnstakingDuration {}, + + #[error("Nothing to claim")] + NothingToClaim {}, + + #[error("Too many outstanding claims. Claim some tokens before unstaking more.")] + TooManyClaims {}, + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Active threshold count must be greater than zero")] + ZeroActiveCount {}, + + #[error("Amount being unstaked must be non-zero")] + ZeroUnstake {}, +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/hooks.rs b/contracts/voting/dao-voting-token-factory-staked/src/hooks.rs new file mode 100644 index 000000000..b5941ce84 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/hooks.rs @@ -0,0 +1,52 @@ +use crate::state::HOOKS; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_binary, Addr, StdResult, Storage, SubMsg, Uint128, WasmMsg}; +use token_bindings::TokenFactoryMsg; + +#[cw_serde] +pub enum StakeChangedHookMsg { + Stake { addr: Addr, amount: Uint128 }, + Unstake { addr: Addr, amount: Uint128 }, +} + +pub fn stake_hook_msgs( + storage: &dyn Storage, + addr: Addr, + amount: Uint128, +) -> StdResult>> { + let msg = to_binary(&StakeChangedExecuteMsg::StakeChangeHook( + StakeChangedHookMsg::Stake { addr, amount }, + ))?; + HOOKS.prepare_hooks_custom_msg(storage, |a| { + let execute = WasmMsg::Execute { + contract_addr: a.to_string(), + msg: msg.clone(), + funds: vec![], + }; + Ok(SubMsg::::new(execute)) + }) +} + +pub fn unstake_hook_msgs( + storage: &dyn Storage, + addr: Addr, + amount: Uint128, +) -> StdResult>> { + let msg = to_binary(&StakeChangedExecuteMsg::StakeChangeHook( + StakeChangedHookMsg::Unstake { addr, amount }, + ))?; + HOOKS.prepare_hooks_custom_msg(storage, |a| { + let execute = WasmMsg::Execute { + contract_addr: a.to_string(), + msg: msg.clone(), + funds: vec![], + }; + Ok(SubMsg::::new(execute)) + }) +} + +// This is just a helper to properly serialize the above message +#[cw_serde] +enum StakeChangedExecuteMsg { + StakeChangeHook(StakeChangedHookMsg), +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/lib.rs b/contracts/voting/dao-voting-token-factory-staked/src/lib.rs new file mode 100644 index 000000000..6c512e72b --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/lib.rs @@ -0,0 +1,12 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod hooks; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::ContractError; diff --git a/contracts/voting/dao-voting-token-factory-staked/src/msg.rs b/contracts/voting/dao-voting-token-factory-staked/src/msg.rs new file mode 100644 index 000000000..122b18148 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/msg.rs @@ -0,0 +1,138 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Uint128; +use cw_tokenfactory_issuer::msg::DenomUnit; +use cw_utils::Duration; +use dao_dao_macros::{active_query, token_query, voting_module_query}; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; + +#[cw_serde] +pub struct InitialBalance { + pub amount: Uint128, + pub address: String, +} + +#[cw_serde] +pub struct NewDenomMetadata { + /// The name of the token (e.g. "Cat Coin") + pub name: String, + /// The description of the token + pub description: String, + /// The ticker symbol of the token (e.g. "CAT") + pub symbol: String, + /// The unit commonly used in communication (e.g. "cat") + pub display: String, + /// Used define additional units of the token (e.g. "tiger") + /// These must have an exponent larger than 0. + pub additional_denom_units: Option>, +} + +#[cw_serde] +pub struct NewTokenInfo { + /// The subdenom of the token to create, will also be used as an alias + /// for the denom. The Token Factory denom will have the format of + /// factory/{contract_address}/{subdenom} + pub subdenom: String, + /// Optional metadata for the token, this can additionally be set later. + pub metadata: Option, + /// The initial balances to set for the token, cannot be empty. + pub initial_balances: Vec, + /// Optional balance to mint for the DAO. + pub initial_dao_balance: Option, +} + +#[cw_serde] +pub enum TokenInfo { + /// Uses an existing Token Factory token and creates a new issuer contract. + /// Full setup, such as transferring ownership or setting up MsgSetBeforeSendHook, + /// must be done manually. + /// Note, for chain controlled denoms or IBC tokens use dao-voting-native-staked. + Existing { + /// Token factory denom + denom: String, + }, + /// Creates a new Token Factory token via the issue contract with the DAO automatically + /// setup as admin and owner. + New(NewTokenInfo), +} + +#[cw_serde] +pub struct InstantiateMsg { + /// The code id of the cw-tokenfactory-issuer contract + pub token_issuer_code_id: u64, + /// New or existing native token to use for voting power. + pub token_info: TokenInfo, + /// How long until the tokens become liquid again + pub unstaking_duration: Option, + /// The number or percentage of tokens that must be staked + /// for the DAO to be active + pub active_threshold: Option, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Stakes tokens with the contract to get voting power in the DAO + Stake {}, + /// Unstakes tokens so that they begin unbonding + Unstake { amount: Uint128 }, + /// Updates the contract configuration + UpdateConfig { duration: Option }, + /// Claims unstaked tokens that have completed the unbonding period + Claim {}, + /// Sets the active threshold to a new value. Only the + /// instantiator of this contract (a DAO most likely) may call this + /// method. + UpdateActiveThreshold { + new_threshold: Option, + }, + /// Adds a hook that fires on staking / unstaking + AddHook { addr: String }, + /// Removes a hook that fires on staking / unstaking + RemoveHook { addr: String }, +} + +#[active_query] +#[voting_module_query] +#[token_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(crate::state::Config)] + GetConfig {}, + #[returns(DenomResponse)] + Denom {}, + #[returns(cw_controllers::ClaimsResponse)] + Claims { address: String }, + #[returns(ListStakersResponse)] + ListStakers { + start_after: Option, + limit: Option, + }, + #[returns(ActiveThresholdResponse)] + ActiveThreshold {}, + #[returns(GetHooksResponse)] + GetHooks {}, +} + +#[cw_serde] +pub struct MigrateMsg {} + +#[cw_serde] +pub struct ListStakersResponse { + pub stakers: Vec, +} + +#[cw_serde] +pub struct StakerBalanceResponse { + pub address: String, + pub balance: Uint128, +} + +#[cw_serde] +pub struct DenomResponse { + pub denom: String, +} + +#[cw_serde] +pub struct GetHooksResponse { + pub hooks: Vec, +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/state.rs b/contracts/voting/dao-voting-token-factory-staked/src/state.rs new file mode 100644 index 000000000..6fb8bc4a0 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/state.rs @@ -0,0 +1,56 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_controllers::Claims; +use cw_hooks::Hooks; +use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; +use cw_utils::Duration; +use dao_voting::threshold::ActiveThreshold; + +use crate::msg::TokenInfo; + +#[cw_serde] +pub struct Config { + pub unstaking_duration: Option, +} + +/// The configuration of this voting contract +pub const CONFIG: Item = Item::new("config"); + +/// The address of the DAO this voting contract is connected to +pub const DAO: Item = Item::new("dao"); + +/// The native denom associated with this contract +pub const DENOM: Item = Item::new("denom"); + +/// Keeps track of staked balances by address over time +pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( + "staked_balances", + "staked_balance__checkpoints", + "staked_balance__changelog", + Strategy::EveryBlock, +); + +/// Keeps track of staked total over time +pub const STAKED_TOTAL: SnapshotItem = SnapshotItem::new( + "total_staked", + "total_staked__checkpoints", + "total_staked__changelog", + Strategy::EveryBlock, +); + +/// The maximum number of claims that may be outstanding. +pub const MAX_CLAIMS: u64 = 100; + +pub const CLAIMS: Claims = Claims::new("claims"); + +/// The minimum amount of staked tokens for the DAO to be active +pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); + +/// Hooks to contracts that will receive staking and unstaking messages +pub const HOOKS: Hooks = Hooks::new("hooks"); + +/// Temporarily holds token_instantiation_info when creating a new Token Factory denom +pub const TOKEN_INSTANTIATION_INFO: Item = Item::new("token_instantiation_info"); + +/// The address of the cw-tokenfactory-issuer contract +pub const TOKEN_ISSUER_CONTRACT: Item = Item::new("token_issuer_contract"); diff --git a/contracts/voting/dao-voting-token-factory-staked/src/tests/mod.rs b/contracts/voting/dao-voting-token-factory-staked/src/tests/mod.rs new file mode 100644 index 000000000..7d9dc1531 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/tests/mod.rs @@ -0,0 +1,10 @@ +// Tests for the crate, using cw-multi-test +// Most coverage lives here +mod multitest; + +// Integrationg tests using an actual chain binary, requires +// the "test-tube" feature to be enabled +// cargo test --features test-tube +#[cfg(test)] +#[cfg(feature = "test-tube")] +mod test_tube; diff --git a/contracts/voting/dao-voting-token-factory-staked/src/tests/multitest/mod.rs b/contracts/voting/dao-voting-token-factory-staked/src/tests/multitest/mod.rs new file mode 100644 index 000000000..eb46f700f --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/tests/multitest/mod.rs @@ -0,0 +1,1370 @@ +use crate::contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}; +use crate::msg::{ + ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, + StakerBalanceResponse, TokenInfo, +}; +use crate::state::Config; +use cosmwasm_std::testing::{mock_env, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{coins, Addr, Coin, Decimal, OwnedDeps, Uint128}; +use cw_controllers::ClaimsResponse; +use cw_multi_test::{ + next_block, AppResponse, BankSudo, Contract, ContractWrapper, Executor, SudoMsg, +}; +use cw_utils::Duration; +use dao_interface::voting::{ + InfoResponse, IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; +use std::marker::PhantomData; +use token_bindings::{TokenFactoryMsg, TokenFactoryQuery}; +use token_bindings_test::TokenFactoryApp as App; + +const DAO_ADDR: &str = "dao"; +const ADDR1: &str = "addr1"; +const ADDR2: &str = "addr2"; +const DENOM: &str = "ujuno"; +const INVALID_DENOM: &str = "uinvalid"; +const ODD_DENOM: &str = "uodd"; + +fn issuer_contract() -> Box> { + let contract = ContractWrapper::new( + cw_tokenfactory_issuer::contract::execute, + cw_tokenfactory_issuer::contract::instantiate, + cw_tokenfactory_issuer::contract::query, + ) + .with_reply(cw_tokenfactory_issuer::contract::reply); + Box::new(contract) +} + +fn staking_contract() -> Box> { + let contract = ContractWrapper::new( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + ) + .with_reply(crate::contract::reply); + Box::new(contract) +} + +fn mock_app() -> App { + let mut app = App::new(); + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: DAO_ADDR.to_string(), + amount: vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + ], + })) + .unwrap(); + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: ADDR1.to_string(), + amount: vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: ODD_DENOM.to_string(), + amount: Uint128::new(5), + }, + ], + })) + .unwrap(); + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: ADDR2.to_string(), + amount: vec![ + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(10000), + }, + Coin { + denom: INVALID_DENOM.to_string(), + amount: Uint128::new(10000), + }, + ], + })) + .unwrap(); + app +} + +fn instantiate_staking(app: &mut App, staking_id: u64, msg: InstantiateMsg) -> Addr { + app.instantiate_contract( + staking_id, + Addr::unchecked(DAO_ADDR), + &msg, + &[], + "Staking", + None, + ) + .unwrap() +} + +fn stake_tokens( + app: &mut App, + staking_addr: Addr, + sender: &str, + amount: u128, + denom: &str, +) -> anyhow::Result { + app.execute_contract( + Addr::unchecked(sender), + staking_addr, + &ExecuteMsg::Stake {}, + &coins(amount, denom), + ) +} + +fn unstake_tokens( + app: &mut App, + staking_addr: Addr, + sender: &str, + amount: u128, +) -> anyhow::Result { + app.execute_contract( + Addr::unchecked(sender), + staking_addr, + &ExecuteMsg::Unstake { + amount: Uint128::new(amount), + }, + &[], + ) +} + +fn claim(app: &mut App, staking_addr: Addr, sender: &str) -> anyhow::Result { + app.execute_contract( + Addr::unchecked(sender), + staking_addr, + &ExecuteMsg::Claim {}, + &[], + ) +} + +fn update_config( + app: &mut App, + staking_addr: Addr, + sender: &str, + duration: Option, +) -> anyhow::Result { + app.execute_contract( + Addr::unchecked(sender), + staking_addr, + &ExecuteMsg::UpdateConfig { duration }, + &[], + ) +} + +fn get_voting_power_at_height( + app: &mut App, + staking_addr: Addr, + address: String, + height: Option, +) -> VotingPowerAtHeightResponse { + app.wrap() + .query_wasm_smart( + staking_addr, + &QueryMsg::VotingPowerAtHeight { address, height }, + ) + .unwrap() +} + +fn get_total_power_at_height( + app: &mut App, + staking_addr: Addr, + height: Option, +) -> TotalPowerAtHeightResponse { + app.wrap() + .query_wasm_smart(staking_addr, &QueryMsg::TotalPowerAtHeight { height }) + .unwrap() +} + +fn get_config(app: &mut App, staking_addr: Addr) -> Config { + app.wrap() + .query_wasm_smart(staking_addr, &QueryMsg::GetConfig {}) + .unwrap() +} + +fn get_claims(app: &mut App, staking_addr: Addr, address: String) -> ClaimsResponse { + app.wrap() + .query_wasm_smart(staking_addr, &QueryMsg::Claims { address }) + .unwrap() +} + +fn get_balance(app: &mut App, address: &str, denom: &str) -> Uint128 { + app.wrap().query_balance(address, denom).unwrap().amount +} + +#[test] +fn test_instantiate_existing() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + // Populated fields + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Non populated fields + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: None, + active_threshold: None, + }, + ); + + let token_contract: Addr = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::TokenContract {}) + .unwrap(); + assert_eq!(token_contract, Addr::unchecked("contract3")); +} + +#[test] +#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] +fn test_instantiate_invalid_unstaking_duration_height() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + + // Populated fields + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(0)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), + }, + ); +} + +#[test] +#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] +fn test_instantiate_invalid_unstaking_duration_time() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + + // Populated fields with height + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Time(0)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), + }, + ); +} + +#[test] +#[should_panic(expected = "Must send reserve token 'ujuno'")] +fn test_stake_invalid_denom() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Try and stake an invalid denom + stake_tokens(&mut app, addr, ADDR1, 100, INVALID_DENOM).unwrap(); +} + +#[test] +fn test_stake_valid_denom() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Try and stake an valid denom + stake_tokens(&mut app, addr, ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); +} + +#[test] +#[should_panic(expected = "Can only unstake less than or equal to the amount you have staked")] +fn test_unstake_none_staked() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + unstake_tokens(&mut app, addr, ADDR1, 100).unwrap(); +} + +#[test] +#[should_panic(expected = "Amount being unstaked must be non-zero")] +fn test_unstake_zero_tokens() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + unstake_tokens(&mut app, addr, ADDR1, 0).unwrap(); +} + +#[test] +#[should_panic(expected = "Can only unstake less than or equal to the amount you have staked")] +fn test_unstake_invalid_balance() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Try and unstake too many + unstake_tokens(&mut app, addr, ADDR1, 200).unwrap(); +} + +#[test] +fn test_unstake() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake some + unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap(); + + // Query claims + let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + assert_eq!(claims.claims.len(), 1); + app.update_block(next_block); + + // Unstake the rest + unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); + + // Query claims + let claims = get_claims(&mut app, addr, ADDR1.to_string()); + assert_eq!(claims.claims.len(), 2); +} + +#[test] +fn test_unstake_no_unstaking_duration() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: None, + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake some tokens + unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap(); + + app.update_block(next_block); + + let balance = get_balance(&mut app, ADDR1, DENOM); + // 10000 (initial bal) - 100 (staked) + 75 (unstaked) = 9975 + assert_eq!(balance, Uint128::new(9975)); + + // Unstake the rest + unstake_tokens(&mut app, addr, ADDR1, 25).unwrap(); + + let balance = get_balance(&mut app, ADDR1, DENOM); + // 10000 (initial bal) - 100 (staked) + 75 (unstaked 1) + 25 (unstaked 2) = 10000 + assert_eq!(balance, Uint128::new(10000)) +} + +#[test] +#[should_panic(expected = "Nothing to claim")] +fn test_claim_no_claims() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + claim(&mut app, addr, ADDR1).unwrap(); +} + +#[test] +#[should_panic(expected = "Nothing to claim")] +fn test_claim_claim_not_reached() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake them to create the claims + unstake_tokens(&mut app, addr.clone(), ADDR1, 100).unwrap(); + app.update_block(next_block); + + // We have a claim but it isnt reached yet so this will still fail + claim(&mut app, addr, ADDR1).unwrap(); +} + +#[test] +fn test_claim() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake some to create the claims + unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap(); + app.update_block(|b| { + b.height += 5; + b.time = b.time.plus_seconds(25); + }); + + // Claim + claim(&mut app, addr.clone(), ADDR1).unwrap(); + + // Query balance + let balance = get_balance(&mut app, ADDR1, DENOM); + // 10000 (initial bal) - 100 (staked) + 75 (unstaked) = 9975 + assert_eq!(balance, Uint128::new(9975)); + + // Unstake the rest + unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); + app.update_block(|b| { + b.height += 10; + b.time = b.time.plus_seconds(50); + }); + + // Claim + claim(&mut app, addr, ADDR1).unwrap(); + + // Query balance + let balance = get_balance(&mut app, ADDR1, DENOM); + // 10000 (initial bal) - 100 (staked) + 75 (unstaked 1) + 25 (unstaked 2) = 10000 + assert_eq!(balance, Uint128::new(10000)); +} + +#[test] +#[should_panic(expected = "Unauthorized")] +fn test_update_config_invalid_sender() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // From ADDR2, so not owner or manager + update_config(&mut app, addr, ADDR2, Some(Duration::Height(10))).unwrap(); +} + +#[test] +fn test_update_config_as_owner() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Swap owner and manager, change duration + update_config(&mut app, addr.clone(), DAO_ADDR, Some(Duration::Height(10))).unwrap(); + + let config = get_config(&mut app, addr); + assert_eq!( + Config { + unstaking_duration: Some(Duration::Height(10)), + }, + config + ); +} + +#[test] +#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")] +fn test_update_config_invalid_duration() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Change duration and manager as manager cannot change owner + update_config(&mut app, addr, DAO_ADDR, Some(Duration::Height(0))).unwrap(); +} + +#[test] +fn test_query_dao() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let msg = QueryMsg::Dao {}; + let dao: Addr = app.wrap().query_wasm_smart(addr, &msg).unwrap(); + assert_eq!(dao, Addr::unchecked(DAO_ADDR)); +} + +#[test] +fn test_query_info() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let msg = QueryMsg::Info {}; + let resp: InfoResponse = app.wrap().query_wasm_smart(addr, &msg).unwrap(); + assert_eq!( + resp.info.contract, + "crates.io:dao-voting-token-factory-staked" + ); +} + +#[test] +fn test_query_token_contract() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let msg = QueryMsg::TokenContract {}; + let res: Addr = app.wrap().query_wasm_smart(addr, &msg).unwrap(); + assert_eq!(res, Addr::unchecked("contract1")); +} + +#[test] +fn test_query_claims() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + assert_eq!(claims.claims.len(), 0); + + // Stake some tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Unstake some tokens + unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); + app.update_block(next_block); + + let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string()); + assert_eq!(claims.claims.len(), 1); + + unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap(); + app.update_block(next_block); + + let claims = get_claims(&mut app, addr, ADDR1.to_string()); + assert_eq!(claims.claims.len(), 2); +} + +#[test] +fn test_query_get_config() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let config = get_config(&mut app, addr); + assert_eq!( + config, + Config { + unstaking_duration: Some(Duration::Height(5)), + } + ) +} + +#[test] +fn test_voting_power_queries() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Total power is 0 + let resp = get_total_power_at_height(&mut app, addr.clone(), None); + assert!(resp.power.is_zero()); + + // ADDR1 has no power, none staked + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + assert!(resp.power.is_zero()); + + // ADDR1 stakes + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Total power is 100 + let resp = get_total_power_at_height(&mut app, addr.clone(), None); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR1 has 100 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR2 still has 0 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), None); + assert!(resp.power.is_zero()); + + // ADDR2 stakes + stake_tokens(&mut app, addr.clone(), ADDR2, 50, DENOM).unwrap(); + app.update_block(next_block); + let prev_height = app.block_info().height - 1; + + // Query the previous height, total 100, ADDR1 100, ADDR2 0 + // Total power is 100 + let resp = get_total_power_at_height(&mut app, addr.clone(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR1 has 100 power + let resp = + get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR2 still has 0 power + let resp = + get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), Some(prev_height)); + assert!(resp.power.is_zero()); + + // For current height, total 150, ADDR1 100, ADDR2 50 + // Total power is 150 + let resp = get_total_power_at_height(&mut app, addr.clone(), None); + assert_eq!(resp.power, Uint128::new(150)); + + // ADDR1 has 100 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR2 now has 50 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), None); + assert_eq!(resp.power, Uint128::new(50)); + + // ADDR1 unstakes half + unstake_tokens(&mut app, addr.clone(), ADDR1, 50).unwrap(); + app.update_block(next_block); + let prev_height = app.block_info().height - 1; + + // Query the previous height, total 150, ADDR1 100, ADDR2 50 + // Total power is 100 + let resp = get_total_power_at_height(&mut app, addr.clone(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(150)); + + // ADDR1 has 100 power + let resp = + get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR2 still has 0 power + let resp = + get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), Some(prev_height)); + assert_eq!(resp.power, Uint128::new(50)); + + // For current height, total 100, ADDR1 50, ADDR2 50 + // Total power is 100 + let resp = get_total_power_at_height(&mut app, addr.clone(), None); + assert_eq!(resp.power, Uint128::new(100)); + + // ADDR1 has 50 power + let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None); + assert_eq!(resp.power, Uint128::new(50)); + + // ADDR2 now has 50 power + let resp = get_voting_power_at_height(&mut app, addr, ADDR2.to_string(), None); + assert_eq!(resp.power, Uint128::new(50)); +} + +#[test] +fn test_query_list_stakers() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // ADDR1 stakes + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + + // ADDR2 stakes + stake_tokens(&mut app, addr.clone(), ADDR2, 50, DENOM).unwrap(); + + // check entire result set + let stakers: ListStakersResponse = app + .wrap() + .query_wasm_smart( + addr.clone(), + &QueryMsg::ListStakers { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + let test_res = ListStakersResponse { + stakers: vec![ + StakerBalanceResponse { + address: ADDR1.to_string(), + balance: Uint128::new(100), + }, + StakerBalanceResponse { + address: ADDR2.to_string(), + balance: Uint128::new(50), + }, + ], + }; + + assert_eq!(stakers, test_res); + + // skipped 1, check result + let stakers: ListStakersResponse = app + .wrap() + .query_wasm_smart( + addr.clone(), + &QueryMsg::ListStakers { + start_after: Some(ADDR1.to_string()), + limit: None, + }, + ) + .unwrap(); + + let test_res = ListStakersResponse { + stakers: vec![StakerBalanceResponse { + address: ADDR2.to_string(), + balance: Uint128::new(50), + }], + }; + + assert_eq!(stakers, test_res); + + // skipped 2, check result. should be nothing + let stakers: ListStakersResponse = app + .wrap() + .query_wasm_smart( + addr, + &QueryMsg::ListStakers { + start_after: Some(ADDR2.to_string()), + limit: None, + }, + ) + .unwrap(); + + assert_eq!(stakers, ListStakersResponse { stakers: vec![] }); +} + +#[test] +#[should_panic(expected = "Active threshold count must be greater than zero")] +fn test_instantiate_zero_active_threshold_count() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::zero(), + }), + }, + ); +} + +#[test] +fn test_active_threshold_absolute_count() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 100 tokens + stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(20), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 6000 tokens, now active + stake_tokens(&mut app, addr.clone(), ADDR1, 6000, DENOM).unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent_rounds_up() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: ODD_DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50), + }), + }, + ); + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 2 tokens, should not be active. + stake_tokens(&mut app, addr.clone(), ADDR1, 2, ODD_DENOM).unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 1 more token, should now be active. + stake_tokens(&mut app, addr.clone(), ADDR1, 1, ODD_DENOM).unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_none() { + let mut app = App::default(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Active as no threshold + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_update_active_threshold() { + let mut app = mock_app(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!(resp.active_threshold, None); + + let msg = ExecuteMsg::UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }; + + // Expect failure as sender is not the DAO + app.execute_contract(Addr::unchecked(ADDR1), addr.clone(), &msg, &[]) + .unwrap_err(); + + // Expect success as sender is the DAO + app.execute_contract(Addr::unchecked(DAO_ADDR), addr.clone(), &msg, &[]) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!( + resp.active_threshold, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100) + }) + ); +} + +#[test] +#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +fn test_active_threshold_percentage_gt_100() { + let mut app = App::default(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(120), + }), + }, + ); +} + +#[test] +#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +fn test_active_threshold_percentage_lte_0() { + let mut app = App::default(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(0), + }), + }, + ); +} + +#[test] +#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] +fn test_active_threshold_absolute_count_invalid() { + let mut app = App::default(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(30001), + }), + }, + ); +} + +#[test] +fn test_add_remove_hooks() { + let mut app = App::default(); + let issuer_id = app.store_code(issuer_contract()); + let staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // No hooks exist. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, Vec::::new()); + + // Add a hook. + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::AddHook { + addr: "hook".to_string(), + }, + &[], + ) + .unwrap(); + + // One hook exists. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr.clone(), &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, vec!["hook".to_string()]); + + // Remove hook. + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::RemoveHook { + addr: "hook".to_string(), + }, + &[], + ) + .unwrap(); + + // No hook exists. + let resp: GetHooksResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::GetHooks {}) + .unwrap(); + assert_eq!(resp.hooks, Vec::::new()); +} + +#[test] +pub fn test_migrate_update_version() { + let mut deps = OwnedDeps { + storage: MockStorage::default(), + api: MockApi::default(), + querier: MockQuerier::default(), + custom_query_type: PhantomData::, + }; + cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); + migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); + let version = cw2::get_contract_version(&deps.storage).unwrap(); + assert_eq!(version.version, CONTRACT_VERSION); + assert_eq!(version.contract, CONTRACT_NAME); +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/integration_tests.rs b/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/integration_tests.rs new file mode 100644 index 000000000..08573a542 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/integration_tests.rs @@ -0,0 +1,244 @@ +use cosmwasm_std::{Coin, Uint128}; +use cw_tokenfactory_issuer::msg::DenomUnit; +use osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest; +use osmosis_test_tube::{Account, OsmosisTestApp}; + +use crate::{ + msg::{ExecuteMsg, InitialBalance, InstantiateMsg, NewDenomMetadata, NewTokenInfo, TokenInfo}, + tests::test_tube::test_env::TfDaoVotingContract, + ContractError, +}; + +use super::test_env::{TestEnv, TestEnvBuilder}; + +#[test] +fn test_stake_unstake_new_denom() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + vp_contract, + accounts, + .. + } = env.default_setup(&app); + + let denom = vp_contract.query_denom().unwrap().denom; + + // Stake 100 tokens + let stake_msg = ExecuteMsg::Stake {}; + vp_contract + .execute(&stake_msg, &[Coin::new(100, denom)], &accounts[0]) + .unwrap(); + + app.increase_time(1); + + // Query voting power + let voting_power = vp_contract.query_vp(&accounts[0].address(), None).unwrap(); + assert_eq!(voting_power.power, Uint128::new(100)); + + // DAO is active (default threshold is absolute count of 75) + let active = vp_contract.query_active().unwrap().active; + assert!(active); + + // Unstake 50 tokens + let unstake_msg = ExecuteMsg::Unstake { + amount: Uint128::new(50), + }; + vp_contract + .execute(&unstake_msg, &[], &accounts[0]) + .unwrap(); + app.increase_time(1); + let voting_power = vp_contract.query_vp(&accounts[0].address(), None).unwrap(); + assert_eq!(voting_power.power, Uint128::new(50)); + + // DAO is not active + let active = vp_contract.query_active().unwrap().active; + assert!(!active); + + // Can't claim before unstaking period (2 seconds) + vp_contract + .execute(&ExecuteMsg::Claim {}, &[], &accounts[0]) + .unwrap_err(); + + // Pass time, unstaking duration is set to 2 seconds + app.increase_time(5); + vp_contract + .execute(&ExecuteMsg::Claim {}, &[], &accounts[0]) + .unwrap(); +} + +#[test] +fn test_instantiate_no_dao_balance() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new().default_setup(&app); + let tf_issuer_id = env.get_tf_issuer_code_id(); + + let dao = app + .init_account(&[Coin::new(100000000000, "uosmo")]) + .unwrap(); + let dao_addr = &dao.address(); + + let vp_contract = env + .instantiate( + &InstantiateMsg { + token_issuer_code_id: tf_issuer_id, + token_info: TokenInfo::New(NewTokenInfo { + subdenom: "ucat".to_string(), + metadata: Some(NewDenomMetadata { + description: "Awesome token, get it meow!".to_string(), + additional_denom_units: Some(vec![DenomUnit { + denom: "cat".to_string(), + exponent: 6, + aliases: vec![], + }]), + display: "cat".to_string(), + name: "Cat Token".to_string(), + symbol: "CAT".to_string(), + }), + initial_balances: vec![InitialBalance { + amount: Uint128::new(100), + address: env.accounts[0].address(), + }], + initial_dao_balance: None, + }), + unstaking_duration: None, + active_threshold: None, + }, + dao, + ) + .unwrap(); + + let denom = vp_contract.query_denom().unwrap().denom; + + // Check balances + // Account 0 + let bal = env + .bank() + .query_balance(&QueryBalanceRequest { + address: env.accounts[0].address(), + denom: denom.clone(), + }) + .unwrap(); + assert_eq!(bal.balance.unwrap().amount, Uint128::new(100).to_string()); + + // DAO + let bal = env + .bank() + .query_balance(&QueryBalanceRequest { + address: dao_addr.to_string(), + denom, + }) + .unwrap(); + assert_eq!(bal.balance.unwrap().amount, Uint128::zero().to_string()); +} + +#[test] +fn test_instantiate_no_metadata() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new().default_setup(&app); + let tf_issuer_id = env.get_tf_issuer_code_id(); + + let dao = app + .init_account(&[Coin::new(100000000000, "uosmo")]) + .unwrap(); + + env.instantiate( + &InstantiateMsg { + token_issuer_code_id: tf_issuer_id, + token_info: TokenInfo::New(NewTokenInfo { + subdenom: "ucat".to_string(), + metadata: None, + initial_balances: vec![InitialBalance { + amount: Uint128::new(100), + address: env.accounts[0].address(), + }], + initial_dao_balance: None, + }), + unstaking_duration: None, + active_threshold: None, + }, + dao, + ) + .unwrap(); +} + +#[test] +fn test_instantiate_invalid_metadata_fails() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new().default_setup(&app); + let tf_issuer_id = env.get_tf_issuer_code_id(); + + let dao = app + .init_account(&[Coin::new(100000000000, "uosmo")]) + .unwrap(); + + env.instantiate( + &InstantiateMsg { + token_issuer_code_id: tf_issuer_id, + token_info: TokenInfo::New(NewTokenInfo { + subdenom: "cat".to_string(), + metadata: Some(NewDenomMetadata { + description: "Awesome token, get it meow!".to_string(), + additional_denom_units: Some(vec![DenomUnit { + denom: "cat".to_string(), + // Exponent 0 is automatically set + exponent: 0, + aliases: vec![], + }]), + display: "cat".to_string(), + name: "Cat Token".to_string(), + symbol: "CAT".to_string(), + }), + initial_balances: vec![InitialBalance { + amount: Uint128::new(100), + address: env.accounts[0].address(), + }], + initial_dao_balance: None, + }), + unstaking_duration: None, + active_threshold: None, + }, + dao, + ) + .unwrap_err(); +} + +#[test] +fn test_instantiate_no_initial_balances_fails() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new().default_setup(&app); + let tf_issuer_id = env.get_tf_issuer_code_id(); + let dao = app + .init_account(&[Coin::new(10000000000000, "uosmo")]) + .unwrap(); + + let err = env + .instantiate( + &InstantiateMsg { + token_issuer_code_id: tf_issuer_id, + token_info: TokenInfo::New(NewTokenInfo { + subdenom: "ucat".to_string(), + metadata: Some(NewDenomMetadata { + description: "Awesome token, get it meow!".to_string(), + additional_denom_units: Some(vec![DenomUnit { + denom: "cat".to_string(), + exponent: 6, + aliases: vec![], + }]), + display: "cat".to_string(), + name: "Cat Token".to_string(), + symbol: "CAT".to_string(), + }), + initial_balances: vec![], + initial_dao_balance: Some(Uint128::new(100000)), + }), + unstaking_duration: None, + active_threshold: None, + }, + dao, + ) + .unwrap_err(); + assert_eq!( + err, + TfDaoVotingContract::execute_submessage_error(ContractError::InitialBalancesError {}) + ); +} diff --git a/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/mod.rs b/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/mod.rs new file mode 100644 index 000000000..fe51e9fb6 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/mod.rs @@ -0,0 +1,6 @@ +// Ignore integration tests for code coverage since there will be problems with dynamic linking libosmosistesttube +// and also, tarpaulin will not be able read coverage out of wasm binary anyway +#![cfg(not(tarpaulin))] + +mod integration_tests; +mod test_env; diff --git a/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/test_env.rs b/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/test_env.rs new file mode 100644 index 000000000..26b706de5 --- /dev/null +++ b/contracts/voting/dao-voting-token-factory-staked/src/tests/test_tube/test_env.rs @@ -0,0 +1,370 @@ +// The code is used in tests but reported as dead code +// see https://github.com/rust-lang/rust/issues/46379 +#![allow(dead_code)] + +use crate::{ + msg::{ExecuteMsg, InitialBalance, InstantiateMsg, NewTokenInfo, QueryMsg, TokenInfo}, + ContractError, +}; + +use cosmwasm_std::{Coin, Uint128}; +use cw_tokenfactory_issuer::msg::{DenomResponse, DenomUnit}; +use cw_utils::Duration; +use dao_interface::voting::{IsActiveResponse, VotingPowerAtHeightResponse}; +use dao_testing::test_tube::cw_tokenfactory_issuer::TokenfactoryIssuer; +use dao_voting::threshold::ActiveThreshold; +use osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, cosmwasm::wasm::v1::MsgExecuteContractResponse, +}; +use osmosis_test_tube::{ + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::path::PathBuf; + +pub const DENOM: &str = "ucat"; +pub const JUNO: &str = "ujuno"; + +pub struct TestEnv<'a> { + pub app: &'a OsmosisTestApp, + pub vp_contract: TfDaoVotingContract<'a>, + pub tf_issuer: TokenfactoryIssuer<'a>, + pub accounts: Vec, +} + +impl<'a> TestEnv<'a> { + pub fn instantiate( + &self, + msg: &InstantiateMsg, + signer: SigningAccount, + ) -> Result { + TfDaoVotingContract::<'a>::instantiate(self.app, self.vp_contract.code_id, msg, &signer) + } + + pub fn get_tf_issuer_code_id(&self) -> u64 { + self.tf_issuer.code_id + } + + pub fn bank(&self) -> Bank<'_, OsmosisTestApp> { + Bank::new(self.app) + } + + pub fn assert_account_balances( + &self, + account: SigningAccount, + expected_balances: Vec, + ignore_denoms: Vec<&str>, + ) { + let account_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: account.address(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .filter(|coin| !ignore_denoms.contains(&coin.denom.as_str())) + .collect(); + + assert_eq!(account_balances, expected_balances); + } + + pub fn assert_contract_balances(&self, expected_balances: &[Coin]) { + let contract_balances: Vec = Bank::new(self.app) + .query_all_balances(&QueryAllBalancesRequest { + address: self.vp_contract.contract_addr.clone(), + pagination: None, + }) + .unwrap() + .balances + .into_iter() + .map(|coin| Coin::new(coin.amount.parse().unwrap(), coin.denom)) + .collect(); + + assert_eq!(contract_balances, expected_balances); + } +} + +pub struct TestEnvBuilder { + pub accounts: Vec, + pub instantiate_msg: Option, +} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self { + accounts: vec![], + instantiate_msg: None, + } + } + + pub fn default_setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + let initial_balances: Vec = accounts + .iter() + .map(|acc| InitialBalance { + address: acc.address(), + amount: Uint128::new(100), + }) + .collect(); + + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + + let vp_contract = TfDaoVotingContract::deploy( + app, + &InstantiateMsg { + token_issuer_code_id: issuer_id, + token_info: TokenInfo::New(NewTokenInfo { + subdenom: DENOM.to_string(), + metadata: Some(crate::msg::NewDenomMetadata { + description: "Awesome token, get it meow!".to_string(), + additional_denom_units: Some(vec![DenomUnit { + denom: "cat".to_string(), + exponent: 6, + aliases: vec![], + }]), + display: "cat".to_string(), + name: "Cat Token".to_string(), + symbol: "CAT".to_string(), + }), + initial_balances, + initial_dao_balance: Some(Uint128::new(900)), + }), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }, + &accounts[0], + ) + .unwrap(); + + let issuer_addr = + TfDaoVotingContract::query(&vp_contract, &QueryMsg::TokenContract {}).unwrap(); + + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + + TestEnv { + app, + vp_contract, + tf_issuer, + accounts, + } + } + + pub fn build(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = self.accounts; + + let vp_contract = TfDaoVotingContract::deploy( + app, + self.instantiate_msg + .as_ref() + .expect("instantiate msg not set"), + &accounts[0], + ) + .unwrap(); + + let issuer_addr = + TfDaoVotingContract::query(&vp_contract, &QueryMsg::TokenContract {}).unwrap(); + + let tf_issuer = TokenfactoryIssuer::new_with_values( + app, + self.instantiate_msg + .expect("instantiate msg not set") + .token_issuer_code_id, + issuer_addr, + ) + .unwrap(); + + TestEnv { + app, + vp_contract, + tf_issuer, + accounts, + } + } + + pub fn upload_issuer(self, app: &'_ OsmosisTestApp, signer: &SigningAccount) -> u64 { + TokenfactoryIssuer::upload(app, signer).unwrap() + } + + pub fn set_accounts(mut self, accounts: Vec) -> Self { + self.accounts = accounts; + self + } + + pub fn with_account(mut self, account: SigningAccount) -> Self { + self.accounts.push(account); + self + } + + pub fn with_instantiate_msg(mut self, msg: InstantiateMsg) -> Self { + self.instantiate_msg = Some(msg); + self + } +} + +#[derive(Debug)] +pub struct TfDaoVotingContract<'a> { + pub app: &'a OsmosisTestApp, + pub contract_addr: String, + pub code_id: u64, +} + +impl<'a> TfDaoVotingContract<'a> { + pub fn deploy( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn instantiate( + app: &'a OsmosisTestApp, + code_id: u64, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn execute( + &self, + msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, msg, funds, signer) + } + + pub fn query(&self, msg: &QueryMsg) -> RunnerResult + where + T: ?Sized + DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, msg) + } + + pub fn query_active(&self) -> RunnerResult { + self.query(&QueryMsg::IsActive {}) + } + + pub fn query_denom(&self) -> RunnerResult { + self.query(&QueryMsg::Denom {}) + } + + pub fn query_vp( + &self, + address: &str, + height: Option, + ) -> RunnerResult { + self.query(&QueryMsg::VotingPowerAtHeight { + address: address.to_string(), + height, + }) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_token_factory_staked.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_token_factory_staked-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } + + pub fn execute_submessage_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: dispatch: submessages: reply: {}: execute wasm contract failed", + err + ), + } + } +} + +pub fn assert_contract_err(expected: ContractError, actual: RunnerError) { + match actual { + RunnerError::ExecuteError { msg } => { + if !msg.contains(&expected.to_string()) { + panic!( + "assertion failed:\n\n must contain \t: \"{}\",\n actual \t: \"{}\"\n", + expected, msg + ) + } + } + _ => panic!("unexpected error, expect execute error but got: {}", actual), + }; +} diff --git a/justfile b/justfile index 2fa8cbcfb..1aff4f8ad 100644 --- a/justfile +++ b/justfile @@ -19,10 +19,16 @@ gen-schema: integration-test: deploy-local workspace-optimize RUST_LOG=info CONFIG={{orc_config}} cargo integration-test +test-tube: + cargo test --features "test-tube" + +test-tube-dev: workspace-optimize + cargo test --features "test-tube" + integration-test-dev test_name="": SKIP_CONTRACT_STORE=true RUST_LOG=info CONFIG='{{`pwd`}}/ci/configs/cosm-orc/local.yaml' cargo integration-test {{test_name}} -bootstrap-dev: deploy-local workspace-optimize-arm +bootstrap-dev: deploy-local workspace-optimize RUST_LOG=info CONFIG={{orc_config}} cargo run bootstrap-env deploy-local: download-deps @@ -39,7 +45,7 @@ deploy-local: download-deps -p 26657:26657 \ -p 9090:9090 \ --mount type=volume,source=junod_data,target=/root \ - ghcr.io/cosmoscontracts/juno:v11.0.0 /opt/setup_and_run.sh {{test_addrs}} + ghcr.io/cosmoscontracts/juno:v15.0.0 /opt/setup_and_run.sh {{test_addrs}} download-deps: mkdir -p artifacts target @@ -47,20 +53,20 @@ download-deps: wget https://github.com/CosmWasm/cw-plus/releases/latest/download/cw4_group.wasm -O artifacts/cw4_group.wasm wget https://github.com/CosmWasm/cw-nfts/releases/latest/download/cw721_base.wasm -O artifacts/cw721_base.wasm -optimize: - cargo install cw-optimizoor || true - cargo cw-optimizoor Cargo.toml - workspace-optimize: - docker run --rm -v "$(pwd)":/code \ - --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - --platform linux/amd64 \ - cosmwasm/workspace-optimizer:0.12.13 - -workspace-optimize-arm: - docker run --rm -v "$(pwd)":/code \ - --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - --platform linux/arm64 \ - cosmwasm/workspace-optimizer-arm64:0.12.13 + #!/bin/bash + if [[ $(uname -m) == 'arm64' ]]; then docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + --platform linux/arm64 \ + cosmwasm/workspace-optimizer-arm64:0.13.0; \ + elif [[ $(uname -m) == 'aarch64' ]]; then docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + --platform linux/arm64 \ + cosmwasm/workspace-optimizer-arm64:0.13.0; \ + elif [[ $(uname -m) == 'x86_64' ]]; then docker run --rm -v "$(pwd)":/code \ + --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + --platform linux/amd64 \ + cosmwasm/workspace-optimizer:0.13.0; fi diff --git a/packages/cw-hooks/src/lib.rs b/packages/cw-hooks/src/lib.rs index 334b88e44..fdbf43577 100644 --- a/packages/cw-hooks/src/lib.rs +++ b/packages/cw-hooks/src/lib.rs @@ -75,6 +75,19 @@ impl<'a> Hooks<'a> { .collect() } + pub fn prepare_hooks_custom_msg StdResult>, T>( + &self, + storage: &dyn Storage, + prep: F, + ) -> StdResult>> { + self.0 + .may_load(storage)? + .unwrap_or_default() + .into_iter() + .map(prep) + .collect::>, _>>() + } + pub fn hook_count(&self, storage: &dyn Storage) -> StdResult { // The WASM VM (as of version 1) is 32 bit and sets limits for // memory accordingly: @@ -94,7 +107,7 @@ impl<'a> Hooks<'a> { #[cfg(test)] mod tests { use super::*; - use cosmwasm_std::{coins, testing::mock_dependencies, BankMsg}; + use cosmwasm_std::{coins, testing::mock_dependencies, BankMsg, Empty}; // Shorthand for an unchecked address. macro_rules! addr { @@ -108,6 +121,20 @@ mod tests { let mut deps = mock_dependencies(); let storage = &mut deps.storage; let hooks = Hooks::new("hooks"); + + // Prepare hooks doesn't through error if no hooks added + let msgs = hooks + .prepare_hooks(storage, |a| { + Ok(SubMsg::reply_always( + BankMsg::Burn { + amount: coins(a.as_str().len() as u128, "uekez"), + }, + 2, + )) + }) + .unwrap(); + assert_eq!(msgs, vec![]); + hooks.add_hook(storage, addr!("ekez")).unwrap(); hooks.add_hook(storage, addr!("meow")).unwrap(); @@ -138,8 +165,40 @@ mod tests { )] ); - let HooksResponse { hooks: the_hooks } = hooks.query_hooks(deps.as_ref()).unwrap(); + // Test prepare hooks with custom messages. + // In a real world scenario, you would be using something like + // TokenFactoryMsg. + let msgs = hooks + .prepare_hooks_custom_msg(storage, |a| { + Ok(SubMsg::::reply_always( + BankMsg::Burn { + amount: coins(a.as_str().len() as u128, "uekez"), + }, + 2, + )) + }) + .unwrap(); + + assert_eq!( + msgs, + vec![SubMsg::::reply_always( + BankMsg::Burn { + amount: coins(4, "uekez"), + }, + 2, + )] + ); + // Query hooks returns all hooks added + let HooksResponse { hooks: the_hooks } = hooks.query_hooks(deps.as_ref()).unwrap(); assert_eq!(the_hooks, vec![addr!("meow")]); + + // Remove last hook + hooks.remove_hook(&mut deps.storage, addr!("meow")).unwrap(); + + // Query hooks returns empty vector if no hooks added + let HooksResponse { hooks: the_hooks } = hooks.query_hooks(deps.as_ref()).unwrap(); + let no_hooks: Vec = vec![]; + assert_eq!(the_hooks, no_hooks); } } diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index d6d7d66a0..d7a9d0c10 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -1,12 +1,17 @@ [package] name = "dao-testing" -authors = ["ekez ekez@withoutdoing.com"] +authors = ["ekez ekez@withoutdoing.com", "Jake Hartnell "] description = "Testing helper functions and interfaces for testing DAO modules." edition = { workspace = true } license = { workspace = true } repository = { workspace = true } version = { workspace = true } +[features] +# use test tube feature to enable test-tube integration tests, for example +# cargo test --features "test-tube" +test-tube = [] + # This crate depends on multi-test and rand. These are not features in # wasm builds of cosmwasm. Despite this crate only being used as a dev # dependency, because it is part of the workspace it will always be @@ -23,7 +28,11 @@ cw20 = { workspace = true } cw20-base = { workspace = true } cw4 = { workspace = true } cw4-group = { workspace = true } +osmosis-std = { workspace = true } +osmosis-test-tube = { workspace = true } rand = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } cw-core-v1 = { workspace = true, features = ["library"] } cw-hooks = { workspace = true } @@ -32,6 +41,7 @@ cw-vesting = { workspace = true } cw20-stake = { workspace = true } cw721-base = { workspace = true } cw721-roles = { workspace = true } +cw-tokenfactory-issuer = { workspace = true } dao-dao-core = { workspace = true, features = ["library"] } dao-interface = { workspace = true } dao-pre-propose-multiple = { workspace = true } @@ -45,5 +55,7 @@ dao-voting-cw4 = { workspace = true } dao-voting-cw721-staked = { workspace = true } dao-voting-cw721-roles = { workspace = true } dao-voting-native-staked = { workspace = true } +dao-voting-token-factory-staked = { workspace = true } voting-v1 = { workspace = true } stake-cw20-v03 = { workspace = true } +token-bindings = { workpsace = true } diff --git a/packages/dao-testing/src/lib.rs b/packages/dao-testing/src/lib.rs index 56891f636..eeb38e678 100644 --- a/packages/dao-testing/src/lib.rs +++ b/packages/dao-testing/src/lib.rs @@ -11,3 +11,6 @@ pub mod contracts; #[cfg(not(target_arch = "wasm32"))] pub use tests::*; + +#[cfg(not(target_arch = "wasm32"))] +pub mod test_tube; diff --git a/packages/dao-testing/src/test_tube/cw_tokenfactory_issuer.rs b/packages/dao-testing/src/test_tube/cw_tokenfactory_issuer.rs new file mode 100644 index 000000000..27ed1dcf0 --- /dev/null +++ b/packages/dao-testing/src/test_tube/cw_tokenfactory_issuer.rs @@ -0,0 +1,155 @@ +use cosmwasm_std::Coin; +use cw_tokenfactory_issuer::{ + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::{ + MsgExecuteContractResponse, MsgMigrateContract, MsgMigrateContractResponse, + }, + Account, Module, OsmosisTestApp, Runner, RunnerError, RunnerExecuteResult, SigningAccount, + Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct TokenfactoryIssuer<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> TokenfactoryIssuer<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let token_creation_fee = Coin::new(10000000, "uosmo"); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + pub fn migrate( + &self, + testdata: &str, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let wasm_byte_code = + std::fs::read(manifest_path.join("tests").join("testdata").join(testdata)).unwrap(); + + let code_id = wasm.store_code(&wasm_byte_code, None, signer)?.data.code_id; + self.app.execute( + MsgMigrateContract { + sender: signer.address(), + contract: self.contract_addr.clone(), + code_id, + msg: serde_json::to_vec(&MigrateMsg {}).unwrap(), + }, + "/cosmwasm.wasm.v1.MsgMigrateContract", + signer, + ) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("cw_tokenfactory_issuer.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("cw_tokenfactory_issuer-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/mod.rs b/packages/dao-testing/src/test_tube/mod.rs new file mode 100644 index 000000000..f87c833bc --- /dev/null +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -0,0 +1,9 @@ +// Ignore integration tests for code coverage since there will be problems with dynamic linking libosmosistesttube +// and also, tarpaulin will not be able read coverage out of wasm binary anyway +#![cfg(not(tarpaulin))] + +// Integrationg tests using an actual chain binary, requires +// the "test-tube" feature to be enabled +// cargo test --features test-tube +#[cfg(feature = "test-tube")] +pub mod cw_tokenfactory_issuer; diff --git a/packages/dao-voting/src/threshold.rs b/packages/dao-voting/src/threshold.rs index 40fd779af..f22fb9849 100644 --- a/packages/dao-voting/src/threshold.rs +++ b/packages/dao-voting/src/threshold.rs @@ -18,6 +18,11 @@ pub enum ActiveThreshold { Percentage { percent: Decimal }, } +#[cw_serde] +pub struct ActiveThresholdResponse { + pub active_threshold: Option, +} + #[derive(Error, Debug, PartialEq, Eq)] pub enum ThresholdError { #[error("Required threshold cannot be zero")]