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 c41e83979..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 +nightly 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 5f75eca90..fca7b1d82 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -21,7 +21,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly + toolchain: nightly-2023-02-02 target: wasm32-unknown-unknown override: true diff --git a/.github/workflows/release-contracts.yml b/.github/workflows/release-contracts.yml new file mode 100644 index 000000000..b6a6bd359 --- /dev/null +++ b/.github/workflows/release-contracts.yml @@ -0,0 +1,51 @@ +name: Release contracts + +permissions: + contents: write + +on: + push: + tags: + - 'v*' + branches: + - main + - ci/release-contracts + +jobs: + release: + runs-on: ubuntu-latest + container: cosmwasm/workspace-optimizer:0.12.13 + steps: + - uses: actions/checkout@v3 + + # tar is required for cargo cache + - run: apk add --no-cache tar + + - name: Set up cargo cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Compile contracts + timeout-minutes: 30 + run: optimize_workspace.sh . + + - name: Upload contracts + uses: actions/upload-artifact@v3 + with: + name: contracts + path: artifacts/ + + - name: release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: artifacts/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test_tube.yml b/.github/workflows/test_tube.yml new file mode 100644 index 000000000..d36a1b66f --- /dev/null +++ b/.github/workflows/test_tube.yml @@ -0,0 +1,71 @@ +name: Test Tube Tests + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Integration tests + runs-on: ubuntu-latest + env: + GAS_OUT_DIR: gas_reports + GAS_LIMIT: 100000000 + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install latest nightly toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2023-02-02 + target: wasm32-unknown-unknown + override: true + + - name: Setup Go + uses: actions/setup-go@v4 + + - name: Clone libwasmv (needed for test-tube) + uses: actions/checkout@v2 + with: + repository: CosmWasm/wasmvm + path: ./wasmvm + ref: v1.4.1 + + - name: Install libwasmv + run: cd ./wasmvm && make build-rust && cd ../ + + - name: Rust Dependencies Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + artifacts/ + key: ${{ runner.os }}-cargo-with-artifacts-${{ hashFiles('**/Cargo.lock') }} + + - name: Set latest just version + run: echo "JUST_VERSION=$(cargo search just -q | sed -n -e '/^just[[:space:]]/p' | cut -d '"' -f 2)" >> $GITHUB_ENV + + - name: Get cached just + uses: actions/cache@v3 + with: + path: ~/.cargo/bin/just + key: ${{ runner.os }}-just-${{ env.JUST_VERSION }} + + - name: Install just + run: cargo install just || true + + - name: Run download deps + run: just download-deps + + - name: Run workspace optimize + run: just workspace-optimize + + - name: Run Test Tube Integration Tests + run: just test-tube diff --git a/Cargo.lock b/Cargo.lock index af58a5a58..6565c11db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "5a824f2aa7e75a0c98c5a504fceb80649e9c35265d44525b5f94de4771a395cd" dependencies = [ "getrandom", "once_cell", @@ -30,18 +30,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[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,29 +68,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] [[package]] name = "async-trait" -version = "0.1.71" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", -] - -[[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", + "syn 2.0.39", ] [[package]] @@ -101,13 +90,13 @@ 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", - "bitflags", + "bitflags 1.3.2", "bytes", "futures-util", "http", @@ -146,9 +135,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -165,18 +154,59 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + [[package]] name = "base64ct" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + +[[package]] +name = "bindgen" +version = "0.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +dependencies = [ + "bitflags 2.4.1", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.39", + "which", +] + [[package]] name = "bip32" version = "0.4.0" @@ -185,12 +215,12 @@ checksum = "b30ed1d6f8437a487a266c8293aeb95b61a23261273e3e02912cdb8b68bf798b" dependencies = [ "bs58", "hmac", - "k256", + "k256 0.11.6", "once_cell", "pbkdf2", "rand_core 0.6.4", "ripemd", - "sha2 0.10.7", + "sha2 0.10.8", "subtle", "zeroize", ] @@ -201,6 +231,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + [[package]] name = "block-buffer" version = "0.9.0" @@ -219,6 +255,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bnum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" + [[package]] name = "bootstrap-env" version = "0.2.0" @@ -227,14 +269,14 @@ dependencies = [ "cosm-orc", "cosmwasm-std", "cw-admin-factory", - "cw-utils 0.16.0", - "cw20 0.16.0", - "cw20-stake 2.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "cw20-stake 2.3.0", "dao-dao-core", "dao-interface", "dao-pre-propose-single", "dao-proposal-single", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-staked", "env_logger", "serde", @@ -253,30 +295,42 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cexpr" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] [[package]] name = "cfg-if" @@ -284,11 +338,31 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "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 = "config" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" dependencies = [ "async-trait", "json5", @@ -305,15 +379,15 @@ 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" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -321,9 +395,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cosm-orc" @@ -348,13 +422,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "596064e3608349aa302eb68b2df8ed3a66bbb51d9b470dbd9afff70843e44642" dependencies = [ "async-trait", - "cosmrs", + "cosmrs 0.10.0", "regex", "schemars", "serde", "serde_json", "thiserror", - "tonic", + "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]] @@ -366,19 +451,40 @@ dependencies = [ "prost 0.11.9", "prost-types", "tendermint-proto 0.26.0", - "tonic", + "tonic 0.8.3", ] [[package]] name = "cosmos-sdk-proto" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4776e787b24d9568dd61d3237eeb4eb321d622fb881b858c7b82806420e87d4" +checksum = "73c9d2043a9e617b0d602fbc0a0ecd621568edbf3a9774890a6d562389bd8e1c" dependencies = [ "prost 0.11.9", "prost-types", - "tendermint-proto 0.27.0", - "tonic", + "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 0.14.8", + "eyre", + "getrandom", + "k256 0.11.6", + "rand_core 0.6.4", + "serde", + "serde_json", + "subtle-encoding", + "tendermint 0.23.9", + "tendermint-rpc 0.23.9", + "thiserror", ] [[package]] @@ -389,46 +495,47 @@ checksum = "6fa07096219b1817432b8f1e47c22e928c64bbfd231fc08f0a98f0e7ddd602b7" dependencies = [ "bip32", "cosmos-sdk-proto 0.15.0", - "ecdsa", + "ecdsa 0.14.8", "eyre", "getrandom", - "k256", + "k256 0.11.6", "rand_core 0.6.4", "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.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb64554a91d6a9231127f4355d351130a0b94e663d5d9dc8b3a54ca17d83de49" +checksum = "d8bb3c77c3b7ce472056968c745eb501c440fbc07be5004eba02782c35bfbbe3" dependencies = [ "digest 0.10.7", + "ecdsa 0.16.9", "ed25519-zebra", - "k256", + "k256 0.13.2", "rand_core 0.6.4", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.2.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0fb2ce09f41a3dae1a234d56a9988f9aff4c76441cd50ef1ee9a4f20415b028" +checksum = "fea73e9162e6efde00018d55ed0061e93a108b5d6ec4548b4f8ce3c706249687" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230e5d1cefae5331db8934763c81b9c871db6a2cd899056a5694fa71d292c815" +checksum = "0df41ea55f2946b6b43579659eec048cc2f66e8c8e2e3652fc5e5e476f673856" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -439,9 +546,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43dadf7c23406cb28079d69e6cb922c9c29b9157b0fe887e3b79c783b7d4bcb8" +checksum = "43609e92ce1b9368aa951b334dd354a2d0dd4d484931a5f83ae10e12a26c8ba9" dependencies = [ "proc-macro2", "quote", @@ -450,11 +557,13 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4337eef8dfaf8572fe6b6b415d6ec25f9308c7bb09f2da63789209fb131363be" +checksum = "04d6864742e3a7662d024b51a94ea81c9af21db6faea2f9a6d2232bb97c6e53e" dependencies = [ - "base64", + "base64 0.21.5", + "bech32", + "bnum", "cosmwasm-crypto", "cosmwasm-derive", "derivative", @@ -463,16 +572,16 @@ dependencies = [ "schemars", "serde", "serde-json-wasm", - "sha2 0.10.7", + "sha2 0.10.8", + "static_assertions", "thiserror", - "uint", ] [[package]] name = "cosmwasm-storage" -version = "1.2.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8601d284db8776e39fe99b3416516c5636ca73cef14666b7bb9648ca32c4b89" +checksum = "bd2b4ae72a03e8f56c85df59d172d51d2d7dc9cec6e2bc811e3fb60c588032a4" dependencies = [ "cosmwasm-std", "serde", @@ -480,24 +589,30 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] [[package]] -name = "crunchy" -version = "0.2.2" +name = "crypto-bigint" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] [[package]] name = "crypto-bigint" -version = "0.4.9" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -548,16 +663,16 @@ dependencies = [ [[package]] name = "cw-admin-factory" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20-base 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20-base 1.1.2", "dao-dao-core", "dao-interface", "thiserror", @@ -593,14 +708,14 @@ dependencies = [ [[package]] name = "cw-controllers" -version = "0.16.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24bd6738c3fd59c87d2f84911c1cad1e4f2d1c58ecaa6e52549b4f78f4ed6f07" +checksum = "57de8d3761e46be863e3ac1eba8c8a976362a48c6abf240df1e26c3e421ee9e8" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.16.0", - "cw-utils 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "schemars", "serde", "thiserror", @@ -675,13 +790,13 @@ dependencies = [ [[package]] name = "cw-denom" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw20 0.16.0", - "cw20-base 0.16.0", + "cw20 1.1.2", + "cw20-base 1.1.2", "thiserror", ] @@ -692,13 +807,13 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-paginate-storage 2.2.0", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw20-stake 2.2.0", + "cw-paginate-storage 2.3.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.3.0", "dao-dao-core", "dao-interface", "dao-voting-cw20-staked", @@ -707,30 +822,30 @@ dependencies = [ [[package]] name = "cw-hooks" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", + "cw-storage-plus 1.2.0", "thiserror", ] [[package]] name = "cw-multi-test" -version = "0.16.5" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "127c7bb95853b8e828bdab97065c81cb5ddc20f7339180b61b2300565aaa99d1" +checksum = "579e2c2f2c0877b839c5cad85e67811074e854a50c1ff3085eb8290b1c27809c" dependencies = [ "anyhow", "cosmwasm-std", - "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "derivative", - "itertools", - "k256", - "prost 0.9.0", + "itertools 0.11.0", + "prost 0.12.3", "schemars", "serde", + "sha2 0.10.8", "thiserror", ] @@ -744,8 +859,8 @@ dependencies = [ "cosmwasm-std", "cw-address-like", "cw-ownable-derive", - "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "thiserror", ] @@ -774,30 +889,30 @@ dependencies = [ [[package]] name = "cw-paginate-storage" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.1.0", + "cw-storage-plus 1.2.0", "serde", ] [[package]] name = "cw-payroll-factory" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", "cw-multi-test", "cw-ownable", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "cw-vesting", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", "thiserror", "wynd-utils", ] @@ -829,7 +944,7 @@ dependencies = [ [[package]] name = "cw-stake-tracker" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -871,9 +986,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" +checksum = "d5ff29294ee99373e2cd5fd21786a3c0ced99a52fec2ca347d565489c61b723c" dependencies = [ "cosmwasm-std", "schemars", @@ -882,17 +997,38 @@ dependencies = [ [[package]] name = "cw-token-swap" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "thiserror", +] + +[[package]] +name = "cw-tokenfactory-issuer" +version = "2.3.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "osmosis-std", + "osmosis-test-tube", + "prost 0.11.9", + "schemars", + "serde", + "serde_json", "thiserror", + "token-bindings", ] [[package]] @@ -936,13 +1072,13 @@ dependencies = [ [[package]] name = "cw-utils" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +checksum = "1c4a657e5caacc3a0d00ee96ca8618745d050b8f757c709babafb81208d4239c" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw2 1.1.0", + "cw2 1.1.2", "schemars", "semver", "serde", @@ -951,7 +1087,7 @@ dependencies = [ [[package]] name = "cw-vesting" -version = "2.2.0" +version = "2.3.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -959,14 +1095,14 @@ dependencies = [ "cw-denom", "cw-multi-test", "cw-ownable", - "cw-paginate-storage 2.2.0", + "cw-paginate-storage 2.3.0", "cw-stake-tracker", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", "cw-wormhole", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", "dao-testing", "serde", "thiserror", @@ -975,11 +1111,11 @@ dependencies = [ [[package]] name = "cw-wormhole" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", + "cw-storage-plus 1.2.0", "serde", ] @@ -1022,14 +1158,15 @@ dependencies = [ [[package]] name = "cw2" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ac2dc7a55ad64173ca1e0a46697c31b7a5c51342f55a1e84a724da4eb99908" +checksum = "c6c120b24fbbf5c3bedebb97f2cc85fbfa1c3287e09223428e7e597b5293c1fa" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", + "cw-storage-plus 1.2.0", "schemars", + "semver", "serde", "thiserror", ] @@ -1060,13 +1197,13 @@ dependencies = [ [[package]] name = "cw20" -version = "0.16.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45a8794a5dd33b66af34caee52a7beceb690856adcc1682b6e3db88b2cdee62" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils 0.16.0", + "cw-utils 1.0.3", "schemars", "serde", ] @@ -1105,16 +1242,15 @@ dependencies = [ [[package]] name = "cw20-base" -version = "0.16.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61d826fa1084d026d0abdb54faa5956972efa3a9053473bfcefb5388960aab69" +checksum = "17ad79e86ea3707229bf78df94e08732e8f713207b4a77b2699755596725e7d9" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.16.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "cw20 1.1.2", "schemars", "semver", "serde", @@ -1142,62 +1278,66 @@ dependencies = [ [[package]] name = "cw20-stake" -version = "2.2.0" +version = "2.3.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", - "cw-controllers 0.16.0", + "cw-controllers 1.1.2", + "cw-hooks", "cw-multi-test", "cw-ownable", - "cw-paginate-storage 2.2.0", - "cw-storage-plus 1.1.0", + "cw-paginate-storage 2.3.0", + "cw-storage-plus 1.2.0", "cw-utils 0.13.4", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", "cw20-stake 0.2.6", + "dao-hooks", + "dao-voting 2.3.0", "thiserror", ] [[package]] name = "cw20-stake-external-rewards" -version = "2.2.0" +version = "2.3.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", - "cw-controllers 0.16.0", + "cw-controllers 1.1.2", "cw-multi-test", "cw-ownable", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", "cw20 0.13.4", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw20-stake 2.2.0", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.3.0", + "dao-hooks", "stake-cw20-external-rewards", "thiserror", ] [[package]] name = "cw20-stake-reward-distributor" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", "cw-ownable", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw20-stake 2.2.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.3.0", "stake-cw20-reward-distributor", "thiserror", ] @@ -1237,15 +1377,17 @@ dependencies = [ [[package]] name = "cw3" -version = "0.16.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4054911cd88826fc7472851d4e799e6ca7246925cf318d6acd21a102486be30c" +checksum = "2967fbd073d4b626dd9e7148e05a84a3bebd9794e71342e12351110ffbb12395" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-utils 0.16.0", + "cw-utils 1.0.3", + "cw20 1.1.2", "schemars", "serde", + "thiserror", ] [[package]] @@ -1262,13 +1404,13 @@ dependencies = [ [[package]] name = "cw4" -version = "0.16.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86a93c2764ddb44038b948d9a9a8aadc9f92f5ef3e61249af2fad16091e43412" +checksum = "24754ff6e45f2a1c60adc409d9b2eb87666012c44021329141ffaab3388fccd2" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 0.16.0", + "cw-storage-plus 1.2.0", "schemars", "serde", ] @@ -1292,17 +1434,17 @@ dependencies = [ [[package]] name = "cw4-group" -version = "0.16.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba1d15bff40b97bde05f36a0d44ac3369a085a4bda6c4673e33a9359513fdd2" +checksum = "9e24a22c3af54c52edf528673b420a67a1648be2c159b8ec778d2fbf543df24b" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-controllers 0.16.0", - "cw-storage-plus 0.16.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw4 0.16.0", + "cw-controllers 1.1.2", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw4 1.1.2", "schemars", "serde", "thiserror", @@ -1352,6 +1494,19 @@ dependencies = [ "serde", ] +[[package]] +name = "cw721" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c4d286625ccadc957fe480dd3bdc54ada19e0e6b5b9325379db3130569e914" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "schemars", + "serde", +] + [[package]] name = "cw721-base" version = "0.16.0" @@ -1369,33 +1524,84 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw721-base" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da518d9f68bfda7d972cbaca2e8fcf04651d0edc3de72b04ae2bcd9289c81614" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.16.0", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw721-controllers" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "thiserror", +] + +[[package]] +name = "cw721-roles" +version = "2.3.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw4 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "dao-cw721-extensions", + "dao-testing", + "dao-voting-cw721-staked", + "serde", "thiserror", ] +[[package]] +name = "dao-cw721-extensions" +version = "2.3.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw4 1.1.2", +] + [[package]] name = "dao-dao-core" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-core", "cw-multi-test", - "cw-paginate-storage 2.2.0", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw721 0.16.0", - "cw721-base", + "cw-paginate-storage 2.3.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", "dao-dao-macros", "dao-interface", "dao-proposal-sudo", @@ -1405,34 +1611,47 @@ dependencies = [ [[package]] name = "dao-dao-macros" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-hooks", "dao-interface", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "proc-macro2", "quote", "syn 1.0.109", ] [[package]] -name = "dao-interface" -version = "2.2.0" +name = "dao-hooks" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-hooks", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw721 0.16.0", + "cw4 1.1.2", + "dao-pre-propose-base", + "dao-voting 2.3.0", +] + +[[package]] +name = "dao-interface" +version = "2.3.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-hooks", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw721 0.18.0", + "osmosis-std", ] [[package]] name = "dao-migrator" -version = "2.2.0" +version = "2.3.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -1441,15 +1660,15 @@ dependencies = [ "cw-core-interface 0.1.0 (git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0)", "cw-multi-test", "cw-proposal-single", - "cw-storage-plus 1.1.0", + "cw-storage-plus 1.2.0", "cw-utils 0.13.4", - "cw-utils 0.16.0", - "cw2 0.16.0", + "cw-utils 1.0.3", + "cw2 1.1.2", "cw20 0.13.4", - "cw20 0.16.0", - "cw20-base 0.16.0", + "cw20 1.1.2", + "cw20-base 1.1.2", "cw20-stake 0.2.6", - "cw20-stake 2.2.0", + "cw20-stake 2.3.0", "cw20-staked-balance-voting", "cw4 0.13.4", "cw4-voting", @@ -1458,7 +1677,7 @@ dependencies = [ "dao-proposal-single", "dao-testing", "dao-voting 0.1.0", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-staked", "dao-voting-cw4", "thiserror", @@ -1466,26 +1685,26 @@ dependencies = [ [[package]] name = "dao-pre-propose-approval-single" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", "cw-multi-test", - "cw-paginate-storage 2.2.0", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw4-group 0.16.0", + "cw-paginate-storage 2.3.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw4-group 1.1.2", "dao-dao-core", + "dao-hooks", "dao-interface", "dao-pre-propose-base", - "dao-proposal-hooks", "dao-proposal-single", "dao-testing", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-staked", "dao-voting-cw4", "thiserror", @@ -1493,195 +1712,184 @@ dependencies = [ [[package]] name = "dao-pre-propose-approver" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw4-group 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw4-group 1.1.2", "dao-dao-core", + "dao-hooks", "dao-interface", "dao-pre-propose-approval-single", "dao-pre-propose-base", - "dao-proposal-hooks", "dao-proposal-single", "dao-testing", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-staked", "dao-voting-cw4", ] [[package]] name = "dao-pre-propose-base" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", "cw-hooks", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", "dao-interface", - "dao-proposal-hooks", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "serde", "thiserror", ] [[package]] name = "dao-pre-propose-multiple" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", "cw-multi-test", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw4-group 0.16.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw4-group 1.1.2", "dao-dao-core", + "dao-hooks", "dao-interface", "dao-pre-propose-base", - "dao-proposal-hooks", "dao-proposal-multiple", "dao-testing", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-staked", "dao-voting-cw4", ] [[package]] name = "dao-pre-propose-single" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", "cw-hooks", "cw-multi-test", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw4-group 0.16.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw4-group 1.1.2", "dao-dao-core", + "dao-hooks", "dao-interface", "dao-pre-propose-base", - "dao-proposal-hooks", "dao-proposal-single", "dao-testing", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-staked", "dao-voting-cw4", ] [[package]] name = "dao-proposal-condorcet" -version = "2.2.0" +version = "2.3.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw4 0.16.0", - "cw4-group 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw4 1.1.2", + "cw4-group 1.1.2", "dao-dao-core", "dao-dao-macros", "dao-interface", "dao-testing", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw4", "thiserror", ] [[package]] name = "dao-proposal-hook-counter" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-hooks", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", "dao-dao-core", + "dao-hooks", "dao-interface", - "dao-proposal-hooks", "dao-proposal-single", - "dao-vote-hooks", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-balance", "thiserror", ] -[[package]] -name = "dao-proposal-hooks" -version = "2.2.0" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-hooks", - "dao-voting 2.2.0", -] - [[package]] name = "dao-proposal-multiple" -version = "2.2.0" +version = "2.3.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "cw-denom", "cw-hooks", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw20-stake 2.2.0", - "cw3 0.16.0", - "cw4 0.16.0", - "cw4-group 0.16.0", - "cw721-base", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.3.0", + "cw3 1.1.2", + "cw4 1.1.2", + "cw4-group 1.1.2", + "cw721-base 0.18.0", "dao-dao-macros", + "dao-hooks", "dao-interface", "dao-pre-propose-base", "dao-pre-propose-multiple", - "dao-proposal-hooks", "dao-testing", - "dao-vote-hooks", "dao-voting 0.1.0", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-balance", "dao-voting-cw20-staked", "dao-voting-cw4", "dao-voting-cw721-staked", - "dao-voting-native-staked", + "dao-voting-token-staked", "rand", "thiserror", ] [[package]] name = "dao-proposal-single" -version = "2.2.0" +version = "2.3.0" dependencies = [ + "anyhow", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -1690,53 +1898,73 @@ dependencies = [ "cw-hooks", "cw-multi-test", "cw-proposal-single", - "cw-storage-plus 1.1.0", + "cw-storage-plus 1.2.0", "cw-utils 0.13.4", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw20-stake 2.2.0", - "cw3 0.16.0", - "cw4 0.16.0", - "cw4-group 0.16.0", - "cw721-base", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.3.0", + "cw3 1.1.2", + "cw4 1.1.2", + "cw4-group 1.1.2", + "cw721-base 0.18.0", "dao-dao-core", "dao-dao-macros", + "dao-hooks", "dao-interface", "dao-pre-propose-base", "dao-pre-propose-single", - "dao-proposal-hooks", "dao-testing", - "dao-vote-hooks", "dao-voting 0.1.0", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-balance", "dao-voting-cw20-staked", "dao-voting-cw4", "dao-voting-cw721-staked", - "dao-voting-native-staked", + "dao-voting-token-staked", "thiserror", ] [[package]] name = "dao-proposal-sudo" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw2 0.16.0", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "dao-dao-macros", + "dao-interface", + "thiserror", +] + +[[package]] +name = "dao-test-custom-factory" +version = "2.3.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-tokenfactory-issuer", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", "dao-dao-macros", "dao-interface", + "dao-voting 2.3.0", "thiserror", ] [[package]] name = "dao-testing" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1744,40 +1972,39 @@ dependencies = [ "cw-hooks", "cw-multi-test", "cw-proposal-single", - "cw-utils 0.16.0", + "cw-tokenfactory-issuer", + "cw-utils 1.0.3", "cw-vesting", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw20-stake 2.2.0", - "cw4 0.16.0", - "cw4-group 0.16.0", - "cw721-base", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.3.0", + "cw4 1.1.2", + "cw4-group 1.1.2", + "cw721-base 0.18.0", + "cw721-roles", "dao-dao-core", "dao-interface", "dao-pre-propose-multiple", "dao-pre-propose-single", "dao-proposal-condorcet", "dao-proposal-single", + "dao-test-custom-factory", "dao-voting 0.1.0", - "dao-voting 2.2.0", + "dao-voting 2.3.0", "dao-voting-cw20-balance", "dao-voting-cw20-staked", "dao-voting-cw4", + "dao-voting-cw721-roles", "dao-voting-cw721-staked", - "dao-voting-native-staked", + "dao-voting-token-staked", + "osmosis-std", + "osmosis-test-tube", "rand", + "serde", + "serde_json", "stake-cw20", -] - -[[package]] -name = "dao-vote-hooks" -version = "2.2.0" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-hooks", - "dao-voting 2.2.0", + "token-bindings", ] [[package]] @@ -1794,14 +2021,14 @@ dependencies = [ [[package]] name = "dao-voting" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw20 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", "dao-dao-macros", "dao-interface", "thiserror", @@ -1809,16 +2036,16 @@ dependencies = [ [[package]] name = "dao-voting-cw20-balance" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", "dao-dao-macros", "dao-interface", "thiserror", @@ -1826,79 +2053,126 @@ dependencies = [ [[package]] name = "dao-voting-cw20-staked" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw20-stake 2.2.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.3.0", "dao-dao-macros", "dao-interface", + "dao-voting 2.3.0", "thiserror", ] [[package]] name = "dao-voting-cw4" -version = "2.2.0" +version = "2.3.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw4 0.16.0", - "cw4-group 0.16.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw4 1.1.2", + "cw4-group 1.1.2", + "dao-dao-macros", + "dao-interface", + "thiserror", +] + +[[package]] +name = "dao-voting-cw721-roles" +version = "2.3.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-ownable", + "cw-paginate-storage 2.3.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw4 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", + "cw721-controllers", + "cw721-roles", + "dao-cw721-extensions", "dao-dao-macros", "dao-interface", + "dao-testing", "thiserror", ] [[package]] name = "dao-voting-cw721-staked" -version = "2.2.0" +version = "2.3.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-controllers 0.16.0", + "cw-controllers 1.1.2", + "cw-hooks", "cw-multi-test", - "cw-paginate-storage 2.2.0", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw721 0.16.0", - "cw721-base", + "cw-paginate-storage 2.3.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721 0.18.0", + "cw721-base 0.18.0", "cw721-controllers", "dao-dao-macros", + "dao-hooks", "dao-interface", + "dao-proposal-hook-counter", + "dao-proposal-single", + "dao-test-custom-factory", "dao-testing", + "dao-voting 2.3.0", + "osmosis-std", + "osmosis-test-tube", + "serde", "thiserror", ] [[package]] -name = "dao-voting-native-staked" -version = "2.2.0" +name = "dao-voting-token-staked" +version = "2.3.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", - "cw-controllers 0.16.0", + "cw-controllers 1.1.2", + "cw-hooks", "cw-multi-test", - "cw-paginate-storage 2.2.0", - "cw-storage-plus 1.1.0", - "cw-utils 0.16.0", - "cw2 0.16.0", + "cw-ownable", + "cw-paginate-storage 2.3.0", + "cw-storage-plus 1.2.0", + "cw-tokenfactory-issuer", + "cw-utils 1.0.3", + "cw2 1.1.2", "dao-dao-macros", + "dao-hooks", "dao-interface", + "dao-proposal-hook-counter", + "dao-proposal-single", + "dao-test-custom-factory", + "dao-testing", + "dao-voting 2.3.0", + "osmosis-std", + "osmosis-test-tube", + "serde", "thiserror", ] @@ -1912,6 +2186,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1939,6 +2223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -1951,9 +2236,9 @@ checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" [[package]] name = "ecdsa" @@ -1961,10 +2246,24 @@ version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" dependencies = [ - "der", - "elliptic-curve", - "rfc6979", - "signature", + "der 0.6.1", + "elliptic-curve 0.12.3", + "rfc6979 0.3.1", + "signature 1.6.4", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.8", + "digest 0.10.7", + "elliptic-curve 0.13.8", + "rfc6979 0.4.0", + "signature 2.2.0", + "spki 0.7.3", ] [[package]] @@ -1973,7 +2272,7 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ - "signature", + "signature 1.6.4", ] [[package]] @@ -2005,9 +2304,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" @@ -2015,28 +2314,47 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" dependencies = [ - "base16ct", - "crypto-bigint", - "der", + "base16ct 0.1.1", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest 0.10.7", + "ff 0.12.1", + "generic-array", + "group 0.12.1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1 0.3.0", + "subtle", + "zeroize", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct 0.2.0", + "crypto-bigint 0.5.5", "digest 0.10.7", - "ff", + "ff 0.13.0", "generic-array", - "group", - "pkcs8", + "group 0.13.0", + "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1", + "sec1 0.7.3", "subtle", "zeroize", ] [[package]] name = "env_logger" -version = "0.9.3" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ - "atty", "humantime", + "is-terminal", "log", "regex", "termcolor", @@ -2044,24 +2362,34 @@ 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.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" dependencies = [ "serde", ] +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "eyre" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +checksum = "80f656be11ddf91bd709454d15d5bd896fbaf4cc3314e69349e4d1569f5b46cd" dependencies = [ "indenter", "once_cell", @@ -2077,6 +2405,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "flex-error" version = "0.4.4" @@ -2095,9 +2433,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -2110,9 +2448,9 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -2125,9 +2463,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -2135,15 +2473,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -2152,38 +2490,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -2205,13 +2543,14 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "js-sys", @@ -2222,9 +2561,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glob" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "group" @@ -2232,16 +2577,27 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" dependencies = [ - "ff", + "ff 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", "rand_core 0.6.4", "subtle", ] [[package]] name = "h2" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -2249,7 +2605,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -2267,18 +2623,17 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64", - "bitflags", + "base64 0.21.5", "bytes", "headers-core", "http", @@ -2298,18 +2653,9 @@ dependencies = [ [[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" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -2326,11 +2672,20 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -2356,9 +2711,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" @@ -2383,7 +2738,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -2441,9 +2796,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2480,12 +2835,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.3", ] [[package]] @@ -2496,20 +2851,22 @@ dependencies = [ "assert_matches", "cosm-orc", "cosm-tome", - "cosmos-sdk-proto 0.16.0", + "cosmos-sdk-proto 0.19.0", "cosmwasm-std", - "cw-utils 0.16.0", + "cw-utils 1.0.3", "cw-vesting", - "cw20 0.16.0", - "cw20-base 0.16.0", - "cw20-stake 2.2.0", - "cw721 0.16.0", - "cw721-base", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.3.0", + "cw721 0.18.0", + "cw721-base 0.18.0", + "cw721-roles", "dao-dao-core", "dao-interface", "dao-pre-propose-single", "dao-proposal-single", - "dao-voting 2.2.0", + "dao-test-custom-factory", + "dao-voting 2.3.0", "dao-voting-cw20-staked", "dao-voting-cw721-staked", "env_logger", @@ -2520,6 +2877,17 @@ dependencies = [ "test-context", ] +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2529,17 +2897,26 @@ 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" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -2562,12 +2939,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" dependencies = [ "cfg-if", - "ecdsa", - "elliptic-curve", - "sha2 0.10.7", + "ecdsa 0.14.8", + "elliptic-curve 0.12.3", + "sha2 0.10.8", "sha3", ] +[[package]] +name = "k256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +dependencies = [ + "cfg-if", + "ecdsa 0.16.9", + "elliptic-curve 0.13.8", + "once_cell", + "sha2 0.10.8", + "signature 2.2.0", +] + [[package]] name = "keccak" version = "0.1.4" @@ -2584,34 +2975,56 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] -name = "libc" -version = "0.2.147" +name = "lazycell" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] -name = "linked-hash-map" -version = "0.5.6" +name = "libc" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] -name = "log" -version = "0.4.19" +name = "libloading" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" - +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + [[package]] name = "matchit" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "mime" @@ -2636,13 +3049,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2668,9 +3081,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -2681,15 +3094,24 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.2", + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ "libc", ] [[package]] name = "object" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -2722,11 +3144,58 @@ dependencies = [ "hashbrown 0.12.3", ] +[[package]] +name = "osmosis-std" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7aa053bc3fad557ac90a0377688b400c395e2537f0f1de3293a15cad2e970" +dependencies = [ + "chrono", + "cosmwasm-std", + "osmosis-std-derive", + "prost 0.11.9", + "prost-types", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "osmosis-std-derive" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ebdfd1bc8ed04db596e110c6baa9b174b04f6ed1ec22c666ddc5cb3fa91bd7" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "prost-types", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "osmosis-test-tube" +version = "20.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1534a419d9e2c27b0b4869e68496b92abca93464b82efbdd1f1b43467f2938" +dependencies = [ + "base64 0.21.5", + "bindgen", + "cosmrs 0.9.0", + "cosmwasm-std", + "osmosis-std", + "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" @@ -2743,6 +3212,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" @@ -2772,25 +3247,26 @@ checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.0" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" dependencies = [ + "memchr", "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.7.0" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" dependencies = [ "pest", "pest_generator", @@ -2798,53 +3274,53 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.7.0" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] [[package]] name = "pest_meta" -version = "2.7.0" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" dependencies = [ "once_cell", "pest", - "sha2 0.10.7", + "sha2 0.10.8", ] [[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.23", + "syn 2.0.39", ] [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2858,8 +3334,18 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ - "der", - "spki", + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.8", + "spki 0.7.3", ] [[package]] @@ -2868,11 +3354,21 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" +dependencies = [ + "proc-macro2", + "syn 2.0.39", +] + [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" dependencies = [ "unicode-ident", ] @@ -2891,32 +3387,32 @@ dependencies = [ [[package]] name = "prost" -version = "0.9.0" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", - "prost-derive 0.9.0", + "prost-derive 0.11.9", ] [[package]] name = "prost" -version = "0.11.9" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", - "prost-derive 0.11.9", + "prost-derive 0.12.3", ] [[package]] name = "prost-derive" -version = "0.9.0" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -2924,15 +3420,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.11.9" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools", + "itertools 0.11.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] @@ -2946,9 +3442,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", ] @@ -2991,9 +3487,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.0" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", @@ -3003,9 +3499,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", @@ -3014,9 +3510,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "rfc6979" @@ -3024,11 +3520,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ - "crypto-bigint", + "crypto-bigint 0.4.9", "hmac", "zeroize", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.16.20" @@ -3070,8 +3576,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" dependencies = [ - "base64", - "bitflags", + "base64 0.13.1", + "bitflags 1.3.2", "serde", ] @@ -3091,13 +3597,32 @@ 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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9470c4bf8246c8daf25f9598dca807fb6510347b1e1cfa55749113850c79d88a" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64", + "base64 0.13.1", "log", "ring", "sct", @@ -3118,15 +3643,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" @@ -3143,14 +3668,14 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "schemars" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "dyn-clone", "schemars_derive", @@ -3160,9 +3685,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", @@ -3186,21 +3711,35 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ - "base16ct", - "der", + "base16ct 0.1.1", + "der 0.6.1", "generic-array", - "pkcs8", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct 0.2.0", + "der 0.7.8", + "generic-array", + "pkcs8 0.10.2", "subtle", "zeroize", ] [[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", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -3209,9 +3748,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", @@ -3219,19 +3758,28 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.17" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.167" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daf513456463b42aa1d94cff7e0c24d682b429f020b9afa4f5ba5c40a22b237" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 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" @@ -3243,22 +3791,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.167" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b69b106b68bc8054f0e974e70d19984040f8a5cf9215ca82626ea4853f82c4b9" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] [[package]] @@ -3274,9 +3822,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.100" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -3285,22 +3833,22 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731" +checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] [[package]] name = "serde_yaml" -version = "0.9.22" +version = "0.9.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "452e67b9c20c37fa79df53201dc03839651086ed9bbe92b3ca585ca9fdaa7d85" +checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -3309,9 +3857,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -3333,9 +3881,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -3352,6 +3900,12 @@ dependencies = [ "keccak", ] +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + [[package]] name = "signature" version = "1.6.4" @@ -3362,25 +3916,45 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -3394,7 +3968,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", - "der", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.8", ] [[package]] @@ -3489,9 +4073,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.23" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -3504,6 +4088,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 0.11.6", + "num-traits", + "once_cell", + "prost 0.11.9", + "prost-types", + "ripemd160", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.9.9", + "signature 1.6.4", + "subtle", + "subtle-encoding", + "tendermint-proto 0.23.9", + "time", + "zeroize", +] + [[package]] name = "tendermint" version = "0.26.0" @@ -3516,7 +4131,7 @@ dependencies = [ "ed25519-dalek", "flex-error", "futures", - "k256", + "k256 0.11.6", "num-traits", "once_cell", "prost 0.11.9", @@ -3527,7 +4142,7 @@ dependencies = [ "serde_json", "serde_repr", "sha2 0.9.9", - "signature", + "signature 1.6.4", "subtle", "subtle-encoding", "tendermint-proto 0.26.0", @@ -3535,6 +4150,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" @@ -3544,11 +4173,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" @@ -3569,9 +4216,9 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.27.0" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5895470f28c530f8ae8c4071bf8190304ce00bd131d25e81730453124a3375c" +checksum = "c0cec054567d16d85e8c3f6a3139963d1a66d9d3051ed545d31562550e9bcc3d" dependencies = [ "bytes", "flex-error", @@ -3585,6 +4232,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" @@ -3607,8 +4287,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", @@ -3621,9 +4301,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] @@ -3649,51 +4329,58 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "test-tube" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e79c7af10967dd3383ee5aae3810637cc3f2fd040f87f862c02151db060628" +dependencies = [ + "base64 0.13.1", + "cosmrs 0.9.0", + "cosmwasm-std", + "osmosis-std", + "prost 0.11.9", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] [[package]] name = "time" -version = "0.3.22" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +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.9" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" -dependencies = [ - "time-core", -] +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tinyvec" @@ -3710,22 +4397,33 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "token-bindings" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be1c893c90d2993320d9722516ece705460f464616313a62edadb9e71df4502" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "schemars", + "serde", +] + [[package]] name = "tokio" -version = "1.29.1" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3740,13 +4438,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] [[package]] @@ -3773,9 +4471,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3803,7 +4501,7 @@ dependencies = [ "async-stream", "async-trait", "axum", - "base64", + "base64 0.13.1", "bytes", "futures-core", "futures-util", @@ -3826,6 +4524,34 @@ dependencies = [ "tracing-futures", ] +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "axum", + "base64 0.21.5", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost 0.11.9", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -3860,11 +4586,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -3872,20 +4597,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -3908,9 +4633,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" @@ -3918,18 +4643,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" @@ -3938,9 +4651,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -3953,9 +4666,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" @@ -3965,9 +4678,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -4000,9 +4713,9 @@ dependencies = [ [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -4025,9 +4738,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4035,24 +4748,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4060,28 +4773,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -4106,6 +4819,18 @@ dependencies = [ "webpki", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + [[package]] name = "winapi" version = "0.3.9" @@ -4124,9 +4849,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -4143,65 +4868,131 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] name = "wynd-utils" @@ -4226,9 +5017,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] @@ -4241,5 +5032,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.39", ] diff --git a/Cargo.toml b/Cargo.toml index aeea5ed91..f87c6a177 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,109 +1,128 @@ [workspace] +exclude = ["ci/configs/", "wasmvm/libwasmvm"] members = [ - "contracts/dao-dao-core", - "contracts/proposal/*", - "contracts/pre-propose/*", - "contracts/staking/*", - "contracts/voting/*", - "packages/*", - "test-contracts/*", - "ci/*", - "contracts/external/*" - ] -exclude = ["ci/configs/"] + "contracts/dao-dao-core", + "contracts/external/*", + "contracts/proposal/*", + "contracts/pre-propose/*", + "contracts/staking/*", + "contracts/test/*", + "contracts/voting/*", + "packages/*", + "ci/*", +] [workspace.package] edition = "2021" license = "BSD-3-Clause" repository = "https://github.com/DA0-DA0/dao-contracts" -version = "2.2.0" - -[profile.release.package.stake-cw20-external-rewards] -codegen-units = 1 -incremental = false +version = "2.3.0" [profile.release] codegen-units = 1 -opt-level = 3 debug = false -rpath = false -lto = true debug-assertions = false -panic = 'abort' incremental = false +lto = true +opt-level = 3 +panic = 'abort' +rpath = false # Please do not disable these. Doing so will cause overflow checks in # all workspace members to stop working. Overflows should be errors. overflow-checks = true [workspace.dependencies] -anyhow = { version = "1.0.51"} -cosmwasm-schema = { version = "1.1" } -cosmwasm-std = { version = "1.1", features = ["ibc3"] } -cosmwasm-storage = { version = "1.1" } -cw-controllers = "0.16" -cw-multi-test = { version = "0.16" } -cw-storage-plus = { version = "1.1" } -cw-utils = "0.16" -cw2 = "0.16" -cw20 = "0.16" -cw20-base = "0.16" -cw3 = "0.16" -cw4 = "0.16" -cw4-group = "0.16" -cw721 = "0.16" -cw721-base = "0.16" +anyhow = {version = "1.0"} +assert_matches = "1.5" +cosm-orc = {version = "4.0"} +cosm-tome = "0.2" +cosmos-sdk-proto = "0.19" +cosmwasm-schema = {version = "1.2"} +cosmwasm-std = {version = "1.5.0", features = ["ibc3"]} +cosmwasm-storage = {version = "1.2"} +cw-controllers = "1.1" +cw-multi-test = "0.18" +cw-storage-plus = {version = "1.1"} +cw-utils = "1.0" +cw2 = "1.1" +cw20 = "1.1" +cw20-base = "1.1" +cw3 = "1.1" +cw4 = "1.1" +cw4-group = "1.1" +cw721 = "0.18" +cw721-base = "0.18" +env_logger = "0.10" +once_cell = "1.18" +osmosis-std = "0.20.1" +osmosis-test-tube = "20.1.1" proc-macro2 = "1.0" +prost = "0.11" quote = "1.0" rand = "0.8" -serde = { version = "1.0", default-features = false, features = ["derive"]} -syn = { version = "1.0", features = ["derive"] } -thiserror = { version = "1.0.30" } -wynd-utils = "0.4.1" +schemars = "0.8" +serde = {version = "1.0", default-features = false, features = ["derive"]} +serde_json = "1.0" +serde_yaml = "0.9" +sg-multi-test = "3.1.0" +sg-std = "3.1.0" +sg721 = "3.1.0" +sg721-base = "3.1.0" +syn = {version = "1.0", features = ["derive"]} +test-context = "0.1" +thiserror = {version = "1.0"} +token-bindings = "0.11.0" +wynd-utils = "0.4" # One commit ahead of version 0.3.0. Allows initialization with an # optional owner. -cw-ownable = "0.5.0" +cw-ownable = "0.5" -cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.2.0" } -cw-denom = { path = "./packages/cw-denom", version = "2.2.0" } -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-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" } -cw721-controllers = { path = "./packages/cw721-controllers", version = "2.2.0" } -dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.2.0" } -dao-interface = { path = "./packages/dao-interface", version = "2.2.0" } -dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.2.0" } -dao-pre-propose-approval-single = { path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "2.2.0" } -dao-pre-propose-approver = { path = "./contracts/pre-propose/dao-pre-propose-approver", version = "2.2.0" } -dao-pre-propose-base = { path = "./packages/dao-pre-propose-base", version = "2.2.0" } -dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-multiple", version = "2.2.0" } -dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.2.0" } -dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.2.0" } -dao-proposal-hooks = { path = "./packages/dao-proposal-hooks", version = "2.2.0" } -dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.2.0" } -dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.2.0" } -dao-proposal-sudo = { path = "./test-contracts/dao-proposal-sudo", version = "2.2.0" } -dao-testing = { path = "./packages/dao-testing", version = "2.2.0" } -dao-vote-hooks = { path = "./packages/dao-vote-hooks", version = "2.2.0" } -dao-voting = { path = "./packages/dao-voting", version = "2.2.0" } -dao-voting-cw20-balance = { path = "./test-contracts/dao-voting-cw20-balance", version = "2.2.0" } -dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.2.0" } -dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.2.0" } -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" } +cw-admin-factory = {path = "./contracts/external/cw-admin-factory", version = "2.3.0"} +cw-denom = {path = "./packages/cw-denom", version = "2.3.0"} +cw-hooks = {path = "./packages/cw-hooks", version = "2.3.0"} +cw-paginate-storage = {path = "./packages/cw-paginate-storage", version = "2.3.0"} +cw-payroll-factory = {path = "./contracts/external/cw-payroll-factory", version = "2.3.0"} +cw-stake-tracker = {path = "./packages/cw-stake-tracker", version = "2.3.0"} +cw-tokenfactory-issuer = {path = "./contracts/external/cw-tokenfactory-issuer", version = "2.3.0"} +cw-vesting = {path = "./contracts/external/cw-vesting", version = "2.3.0"} +cw-wormhole = {path = "./packages/cw-wormhole", version = "2.3.0"} +cw20-stake = {path = "./contracts/staking/cw20-stake", version = "2.3.0"} +cw721-controllers = {path = "./packages/cw721-controllers", version = "2.3.0"} +cw721-roles = {path = "./contracts/external/cw721-roles", version = "2.3.0"} +dao-cw721-extensions = {path = "./packages/dao-cw721-extensions", version = "2.3.0"} +dao-dao-core = {path = "./contracts/dao-dao-core", version = "2.3.0"} +dao-dao-macros = {path = "./packages/dao-dao-macros", version = "2.3.0"} +dao-hooks = {path = "./packages/dao-hooks", version = "2.3.0"} +dao-interface = {path = "./packages/dao-interface", version = "2.3.0"} +dao-pre-propose-approval-single = {path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "2.3.0"} +dao-pre-propose-approver = {path = "./contracts/pre-propose/dao-pre-propose-approver", version = "2.3.0"} +dao-pre-propose-base = {path = "./packages/dao-pre-propose-base", version = "2.3.0"} +dao-pre-propose-multiple = {path = "./contracts/pre-propose/dao-pre-propose-multiple", version = "2.3.0"} +dao-pre-propose-single = {path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.3.0"} +dao-proposal-condorcet = {path = "./contracts/proposal/dao-proposal-condorcet", version = "2.3.0"} +dao-proposal-hook-counter = {path = "./contracts/test/dao-proposal-hook-counter", version = "2.3.0"} +dao-proposal-multiple = {path = "./contracts/proposal/dao-proposal-multiple", version = "2.3.0"} +dao-proposal-single = {path = "./contracts/proposal/dao-proposal-single", version = "2.3.0"} +dao-proposal-sudo = {path = "./contracts/test/dao-proposal-sudo", version = "2.3.0"} +dao-test-custom-factory = {path = "./contracts/test/dao-test-custom-factory", version = "2.3.0"} +dao-testing = {path = "./packages/dao-testing", version = "2.3.0"} +dao-voting = {path = "./packages/dao-voting", version = "2.3.0"} +dao-voting-cw20-balance = {path = "./contracts/test/dao-voting-cw20-balance", version = "2.3.0"} +dao-voting-cw20-staked = {path = "./contracts/voting/dao-voting-cw20-staked", version = "2.3.0"} +dao-voting-cw4 = {path = "./contracts/voting/dao-voting-cw4", version = "2.3.0"} +dao-voting-cw721-roles = {path = "./contracts/voting/dao-voting-cw721-roles", version = "2.3.0"} +dao-voting-cw721-staked = {path = "./contracts/voting/dao-voting-cw721-staked", version = "2.3.0"} +dao-voting-token-staked = {path = "./contracts/voting/dao-voting-token-staked", version = "2.3.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" } cw20-staked-balance-voting-v1 = { package = "cw20-staked-balance-voting", version = "0.1.0" } 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" } +voting-v1 = { package = "dao-voting", version = "0.1.0" } diff --git a/README.md b/README.md index c02bd21b8..34b82e536 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ # DAO Contracts +[![GitHub tag (with filter)](https://img.shields.io/github/v/tag/DA0-DA0/dao-contracts?label=Latest%20version&logo=github)](https://github.com/DA0-DA0/dao-contracts/releases/latest) +[![GitHub contributors](https://img.shields.io/github/contributors/DA0-DA0/dao-contracts?logo=github)](https://github.com/DA0-DA0/dao-contracts/graphs/contributors) + +[![GitHub commit activity (branch)](https://img.shields.io/github/commit-activity/m/DA0-DA0/dao-contracts?logo=git)](https://github.com/DA0-DA0/dao-contracts/pulse/monthly) [![codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/branch/main/graph/badge.svg?token=SCKOIPYZPV)](https://codecov.io/gh/DA0-DA0/dao-contracts) +[![Discord](https://img.shields.io/discord/895922260047720449?logo=discord&label=Discord)](https://discord.gg/MUBxdbwJDD) +[![X (formerly Twitter) URL](https://img.shields.io/twitter/url?url=https%3A%2F%2Ftwitter.com%2FDA0_DA0&label=DA0_DA0)](https://twitter.com/DA0_DA0) + +[![DAO DAO DAO](https://img.shields.io/badge/DAO%20DAO%20DAO-gray?logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAAHAAAABwCAMAAADxPgR5AAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAR1QTFRF%2F%2F%2F%2F7%2FDw0NDRoaOjgoSFY2VmREdIJSgpFhgaBgkLNTc4sbKz0dHRREZINTc54ODgwMHCwMLCJSgpREZIoqOjNTc5VFZXoqOksbKz0dHRwcHCkpOUsbKyoqKjJikqc3V2g4SFoaGiwMDB7%2FDwoaKjFxkb39%2FfkZKTFhgaY2VnZGZnZGVn7u%2FvgoOEdHZ3BwoM%2Fv7%2BwcLCRUhJNjg5NTc4gYOENTg5cnR2c3V3sLGy7%2B%2Fv0NHR0NDRU1ZXoaOkgoSFY2VmREdIz9DQJSgqoaKiFhkaBgkL3%2BDgv8DBNDc5z8%2FQsLKzVVdY7u7uwMHBkpSUFxobZWZodHV3sbGyc3R2Njg6RUdJoKKiv8HBr7GykZOT3t%2Ff%2F%2F%2F%2FcnR1oaOjFOTQHAAAAA90Uk5T%2Fv7%2B%2Fv7%2B%2Fv7%2B%2Fv7%2B%2Fv7%2B6a2FXwAABrRJREFUaN7Vm%2BtjmzYQwNnWx7ZuUwq1jU0Dbp04LM0CTWu3pWnBdXHjuY9kXV8b5P%2F%2FM4aFAElIIAH5sPsWk%2BjnO%2B4hnS5KJCPe6Us%2FWCyX4SqRcLleBP7mVGoFRRGHvYzXK6asg0vQNXDH58ByaHy7O6DnhysBUWPQCXBHWwnLYtMaeKdHrLjsxwN9CDyoOBiNjFhTSTUvWwHv4qv1Yt1jWlyPe8LISiBuTM3wqtYBu9jvmqAR0LOKLz326t0B3CvMEQB54E7%2B59pINMh0rdauPGChnjhuK%2FdzpDWRAQK1EQ4i1co3yQbuoUAPjaiBZO9yXxcF%2Bpl6IGok0wAt4IsBrTbqpXJg84gM4O%2FIz0DUQqbIrIf1QMR74EWt5KjPJpaAiDeOWssfTCIN9Dvj5cTjKiDiGVHUIdHnA%2B92qB90Vrics8cDgrBbXqbjPmADPbVrXkY0J0xgGvD9LnmR%2B4ByHAx4J413r1NgdATN5jwsA9MCEYKoY5nahFELYNBlQJRd9ZgGgnRr0D0vcjXcqDlQbZ%2Bwq416QgJTj9mLrkSgUR2dAKYKXg0PGdXEgamCD68IGN2HKl5iQKjg4qp4SMVHBVC%2FWgUzFR%2FmQI2toLeJ%2B9vDbrhexBuvvYonGTCNQb10siBPhf1NaxUnCGgxXPSuyjhxXjZXUUWlGALVUgwCziH0cePMMIOFMQXuwLXwpQzuEdtummyPkNtsgRbtMn7Vqdpv4zY%2BBM4pi%2FrV53i%2FRX57tAUCyqJ%2BXefAb2HTSQKEae1x8f7qexWN3qM7h%2BktAcLK%2B4TauVWK3cRXXegqTxPgnIj6nlA7pomKz%2BBLTIBwiQmxk6oVvfFL%2FE55TqQZVQzYpHK6yy3we0XHbaSL9rgaVBb3xRb4g%2BLjPvNCuKnW1GuuKQHm6BPhLp49kSfubf%2FwuqJhFipbNByPomgYCLmN5ydrrS%2Br3fSGMseAVsk5ANW3yeUJ91jP7bS92j69qSyxxFYqSvnfApt60mdXPOqsRH6jFGhjYTinVg34Gb0UGK9rky0MxB8V%2BCvoI1oNrGE%2For2GlSqRnHDcdPvsJwJIWxT%2F9apn2WpInP8PEMsnz%2BlnE1ZFT%2BURRZoQQNxpbH6pDbgOjO%2BSyl0L6OKE0%2BBhseQWPlAb%2BUdFWNC2fEOExRJzx3kp8KcuEdRVZZ8f%2BGj1s%2B3Tn9PUpnPsloTin657NmNsAxjdjntJ9dYY%2FfgBBryRJu9LTmrrJn0b2L7tOlGeBhJA8ZKo60R5GmD2eS4DPBEuvKdF4Di3yC2GLQF0TgV7CvtYdk%2B2GBM8jDUZFd%2BKKRic4JsoapvoywAdobd45hjUNtHCvGYkA%2BQWPlzeqcj0qc8cJsABvtW3pYjHIjsnE9%2Fq7yVA4iVaUsD6c80s77LDxOaA%2FLhmNLFpLXFWOPPr%2FLiWekq20dS6JM6KWpWWr6cQOMJtakgCedd2W3%2BxsFo1xY7caVVCX3ViyxJNTgI4U%2FFa9Rr9AIE%2Bnmz8lbSwLmBfaUQxTjeRTxEw9dP3TVUsN428M40qxs9SH2W2voxVEwn7H26fJqXQe7P50A%2Fp3QbV%2BkJu876ho1a93wirvbBFi9qXc1zFUWc8B5Q6tAi4S6hodQUcEwru4S1oE1dxYnZrUFcttaDRyfAg2xTud8E7B3gMIv%2FJrxGgle0p%2Bmm3ixdoEPtHk7q3GJFH93F3L9ANcP8proIswqjRRVveX%2BRV0GHp7in1lNyobYkX%2BR4qJLbj2HVealT1qBPiRbHFIM83VReWxx3w3D5pXwKIwm%2BWfzB2WvpLdogzPc6l8z5FNBrF4%2FkuxTsHvGt1FH4FETTIOdhxLeU5H%2FmDA2OaKG%2FWvz36UDyuGo04LhGBKYPEJ4xYvPLwBwqGmYsZWhhJjODMKIflADNi%2FyiSRYb44fddn8ljDfBcZMd7wp0%2B1SHJSbvswE%2FzmCNKyHPsAxf%2FdHhoOlxoGJPTYQchlVArgblnBlPy86Hx%2BbwMDbUxNYs21VbcsRX2mJmOAl794tKPhrr%2F9nM2stsL4sGwfOkTlhJAHbAIePWrK9loLrakEoN0SV49zky3kELmODwBiAC3oZB38L%2B50jiTN2JYMe4JDnP%2FUL9M65mer65q1ItqBlrxrLZImBVQ74PGzm9SQCrFrONv71wG1dsQA%2BDmx6oVa4eSqaymLuJ%2Fvn79Nx1KfnO6eRn3yZ7numaARGDsWv%2FsNCoWjYHJu7SEkvdSZJJYdHR%2BZH1yKqG9sdisrcQ%2FB4DdwwTKovbigfCkhiIpv9y6dv3X37JcutQCfzCUGgtR%2FgO61zuwRnnviwAAAABJRU5ErkJggg%3D%3D)](https://daodao.zone/dao/juno10h0hc64jv006rr8qy0zhlu4jsxct8qwa0vtaleayh0ujz0zynf2s2r7v8q) + + This is a collection of smart contracts for building composable, modular, and upgradable DAOs. For a detailed look at how these contracts work, see [our wiki](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design). -Our most recently [audited](https://github.com/oak-security/audit-reports/blob/master/DAO%20DAO/2023-02-06%20Audit%20Report%20-%20DAO%20DAO%202%20v1.0.pdf) release is `v2.0.0`. If you believe you have found a problem, please [let us know](SECURITY.md). - ## Overview Every DAO is made up of three modules: @@ -24,6 +32,48 @@ Each module type has a [standard interface](https://github.com/DA0-DA0/dao-contr The best way to get started is to create a DAO! We maintain an [open source](https://github.com/DA0-DA0/dao-dao-ui) frontend you can find at [daodao.zone](https://daodao.zone). +## Audits + +If you believe you have found a problem, please [let us know](SECURITY.md). + +DAO DAO has been audited by [Oak Security](https://www.oaksecurity.io/) on multiple occasions. You can find all the audit reports [here](https://github.com/oak-security/audit-reports/tree/master/DAO%20DAO). + +`v2.3.0` is the most recent DAO DAO release; only new feautres related to tokenfactory and improved NFT DAOs have been [audited](https://github.com/oak-security/audit-reports/blob/master/DAO%20DAO/2023-10-16%20Audit%20Report%20-%20DAO%20DAO%20Updates%20v1.0.pdf). Our most recently [full audited](https://github.com/oak-security/audit-reports/blob/master/DAO%20DAO/2023-02-06%20Audit%20Report%20-%20DAO%20DAO%202%20v1.0.pdf) release is `v2.0.0`. Vesting and payroll were added and [audited](https://github.com/oak-security/audit-reports/blob/master/DAO%20DAO/2023-03-22%20Audit%20Report%20-%20DAO%20DAO%20Vesting%20and%20Payroll%20Factory%20v1.0.pdf) in `v2.1.0`. + +Audited contracts include: +- [cw-payroll-factory](https://crates.io/crates/cw-payroll-factory) +- [cw-tokenfactory-issuer](https://crates.io/crates/cw-tokenfactory-issuer) +- [cw-token-swap](https://crates.io/crates/cw-token-swap) +- [cw-vesting](https://crates.io/crates/cw-vesting) +- [dao-dao-core](https://crates.io/crates/dao-dao-core) +- [dao-pre-propose-approval-single](https://crates.io/crates/dao-pre-propose-approval-single) +- [dao-pre-propose-approver](https://crates.io/crates/dao-pre-propose-approver) +- [dao-pre-propose-multiple](https://crates.io/crates/dao-pre-propose-multiple) +- [dao-pre-propose-single](https://crates.io/crates/dao-pre-propose-single) +- [dao-proposal-condorcet](https://crates.io/crates/dao-proposal-condorcet) +- [dao-proposal-multiple](https://crates.io/crates/dao-proposal-multiple) +- [dao-proposal-single](https://crates.io/crates/dao-proposal-single) +- [cw20-stake](https://crates.io/crates/cw20-stake) +- [cw20-stake-external-rewards](https://crates.io/crates/cw20-stake-external-rewards) +- [cw20-stake-reward-distributor](https://crates.io/crates/cw20-stake-reward-distributor) +- [dao-voting-cw4](https://crates.io/crates/dao-voting-cw4) +- [dao-voting-cw20-staked](https://crates.io/crates/dao-voting-cw20-staked) +- [dao-voting-cw721-staked](https://crates.io/crates/dao-voting-cw721-staked) +- [dao-voting-token-staked](https://crates.io/crates/dao-voting-token-staked) + +Audited packages include: +- [cw721-controllers](https://crates.io/crates/cw721-controllers) +- [cw-denom](https://crates.io/crates/cw-denom) +- [cw-hooks](https://crates.io/crates/cw-hooks) +- [cw-paginate-storage](https://crates.io/crates/cw-paginate-storage) +- [cw-stake-tracker](https://crates.io/crates/cw-stake-tracker) +- [cw-wormhole](https://crates.io/crates/cw-wormhole) +- [dao-dao-macros](https://crates.io/crates/dao-dao-macros) +- [dao-hooks](https://crates.io/crates/dao-hooks) +- [dao-interface](https://crates.io/crates/dao-interface) +- [dao-pre-propose-based](https://crates.io/crates/dao-pre-propose-based) +- [dao-voting](https://crates.io/crates/dao-voting) + ## Why? Our institutions grew rapidly after 1970, but as time passed their priorities shifted from growth, to protectionism. We're fighting this. We believe The Internet is where the organizations of tomorrow will be built. diff --git a/ci/bootstrap-env/Cargo.toml b/ci/bootstrap-env/Cargo.toml index 0a037220e..5ea498f2b 100644 --- a/ci/bootstrap-env/Cargo.toml +++ b/ci/bootstrap-env/Cargo.toml @@ -5,7 +5,7 @@ edition = { workspace = true } repository = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -cosm-orc = { version = "4.0" } +cosm-orc = { workspace = true } cw20 = { workspace = true } cw-utils = { workspace = true } cosmwasm-std = { workspace = true, features = ["ibc3"] } @@ -19,7 +19,7 @@ dao-interface = { workspace = true } dao-voting = { workspace = true } anyhow = { workspace = true } -env_logger = "0.9.0" +env_logger = { workspace = true } serde = { workspace = true, default-features = false, features = ["derive"] } -serde_json = "1.0" -serde_yaml = "0.9" +serde_json = { workspace = true } +serde_yaml = { workspace = true } diff --git a/ci/bootstrap-env/src/main.rs b/ci/bootstrap-env/src/main.rs index b8bdeb0a5..809b1f5a8 100644 --- a/ci/bootstrap-env/src/main.rs +++ b/ci/bootstrap-env/src/main.rs @@ -1,7 +1,7 @@ use anyhow::Result; use cosm_orc::orchestrator::{Coin, Key, SigningKey}; use cosm_orc::{config::cfg::Config, orchestrator::cosm_orc::CosmOrc}; -use cosmwasm_std::{to_binary, Decimal, Empty, Uint128}; +use cosmwasm_std::{to_json_binary, Decimal, Empty, Uint128}; use cw20::Cw20Coin; use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_voting::{ @@ -55,7 +55,7 @@ fn main() -> Result<()> { automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: orc.contract_map.code_id("dao_voting_cw20_staked")?, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: orc.contract_map.code_id("cw20_base")?, label: "DAO DAO Gov token".to_string(), @@ -73,12 +73,13 @@ fn main() -> Result<()> { }, active_threshold: None, })?, + funds: vec![], admin: Some(Admin::CoreModule {}), label: "DAO DAO Voting Module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: orc.contract_map.code_id("dao_proposal_single")?, - msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { min_voting_period: None, threshold: Threshold::ThresholdQuorum { threshold: PercentageThreshold::Majority {}, @@ -90,7 +91,7 @@ fn main() -> Result<()> { pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: orc.contract_map.code_id("dao_pre_propose_single")?, - msg: to_binary(&dao_pre_propose_single::InstantiateMsg { + msg: to_json_binary(&dao_pre_propose_single::InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: DepositToken::VotingModuleToken {}, amount: Uint128::new(1000000000), @@ -101,12 +102,15 @@ fn main() -> Result<()> { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Pre-Propose Module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, })?, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Proposal Module".to_string(), }], initial_items: None, diff --git a/ci/integration-tests/Cargo.toml b/ci/integration-tests/Cargo.toml index 37221c34c..6291ec502 100644 --- a/ci/integration-tests/Cargo.toml +++ b/ci/integration-tests/Cargo.toml @@ -10,10 +10,11 @@ edition = { workspace = true } # conditionally. As such, we don't compile anything here if we're # targeting wasm. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -cosm-orc = { version = "4.0" } +cosm-orc = { workspace = true } cw20 = { workspace = true } cw20-base = { workspace = true } cw721-base = { workspace = true } +cw721-roles = { workspace = true } cw721 = { workspace = true } cw-utils = { workspace = true } cosmwasm-std = { workspace = true, features = ["ibc3"] } @@ -24,17 +25,18 @@ dao-dao-core = { workspace = true } dao-interface = { workspace = true } dao-pre-propose-single = { workspace = true } dao-proposal-single = { workspace = true } +dao-test-custom-factory = { workspace = true } dao-voting = { workspace = true } dao-voting-cw20-staked = { workspace = true } dao-voting-cw721-staked = { workspace = true } -assert_matches = "1.5" -anyhow = { version = "1.0.51"} -serde = { version = "1.0", default-features = false, features = ["derive"] } -serde_json = "1.0" -once_cell = "1.13.0" -env_logger = "0.9.0" -test-context = "0.1.4" -cosm-tome = "0.2.1" -cosmos-sdk-proto = "0.16" +assert_matches = { workspace = true } +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +once_cell = { workspace = true } +env_logger = { workspace = true } +test-context = { workspace = true } +cosm-tome = { workspace = true } +cosmos-sdk-proto = { workspace = true } rand = { workspace = true } diff --git a/ci/integration-tests/src/helpers/helper.rs b/ci/integration-tests/src/helpers/helper.rs index 3c685c22d..772025e4c 100644 --- a/ci/integration-tests/src/helpers/helper.rs +++ b/ci/integration-tests/src/helpers/helper.rs @@ -1,7 +1,7 @@ use super::chain::Chain; use anyhow::Result; use cosm_orc::orchestrator::SigningKey; -use cosmwasm_std::{to_binary, CosmosMsg, Decimal, Empty, Uint128}; +use cosmwasm_std::{to_json_binary, CosmosMsg, Decimal, Empty, Uint128}; use cw20::Cw20Coin; use cw_utils::Duration; use dao_interface::query::DumpStateResponse; @@ -39,7 +39,7 @@ pub fn create_dao( automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: chain.orc.contract_map.code_id("dao_voting_cw20_staked")?, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: chain.orc.contract_map.code_id("cw20_base")?, label: "DAO DAO Gov token".to_string(), @@ -59,10 +59,11 @@ pub fn create_dao( })?, admin: Some(Admin::CoreModule {}), label: "DAO DAO Voting Module".to_string(), + funds: vec![], }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: chain.orc.contract_map.code_id("dao_proposal_single")?, - msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { min_voting_period: None, threshold: Threshold::ThresholdQuorum { threshold: PercentageThreshold::Majority {}, @@ -75,7 +76,7 @@ pub fn create_dao( pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: chain.orc.contract_map.code_id("dao_pre_propose_single")?, - msg: to_binary(&dao_pre_propose_single::InstantiateMsg { + msg: to_json_binary(&dao_pre_propose_single::InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: DepositToken::VotingModuleToken {}, amount: DEPOSIT_AMOUNT, @@ -86,11 +87,14 @@ pub fn create_dao( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Pre-Propose Module".to_string(), }, }, + veto: None, })?, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Proposal Module".to_string(), }], initial_items: None, @@ -118,15 +122,16 @@ pub fn create_dao( .unwrap(); let ProposalCreationPolicy::Module { addr: pre_propose } = chain - .orc - .query( - "dao_proposal_single", - &dao_proposal_single::msg::QueryMsg::ProposalCreationPolicy {} - ).unwrap() - .data() - .unwrap() + .orc + .query( + "dao_proposal_single", + &dao_proposal_single::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap() + .data() + .unwrap() else { - panic!("expected pre-propose module") + panic!("expected pre-propose module") }; chain .orc @@ -183,7 +188,7 @@ pub fn stake_tokens(chain: &mut Chain, how_many: u128, key: &SigningKey) { &cw20::Cw20ExecuteMsg::Send { contract: chain.orc.contract_map.address("cw20_stake").unwrap(), amount: Uint128::new(how_many), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, key, vec![], diff --git a/ci/integration-tests/src/tests/cw20_stake_test.rs b/ci/integration-tests/src/tests/cw20_stake_test.rs index 13cb0d226..4250441d5 100644 --- a/ci/integration-tests/src/tests/cw20_stake_test.rs +++ b/ci/integration-tests/src/tests/cw20_stake_test.rs @@ -1,5 +1,5 @@ use crate::helpers::{chain::Chain, helper::create_dao}; -use cosmwasm_std::{to_binary, Uint128}; +use cosmwasm_std::{to_json_binary, Uint128}; use cw20_stake::{msg::StakedValueResponse, state::Config}; use dao_interface::voting::VotingPowerAtHeightResponse; use std::time::Duration; @@ -79,7 +79,7 @@ fn execute_stake_tokens(chain: &mut Chain) { &cw20_base::msg::ExecuteMsg::Send { contract: staking_addr, amount: Uint128::new(100), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &user_key, vec![], diff --git a/ci/integration-tests/src/tests/cw_core_test.rs b/ci/integration-tests/src/tests/cw_core_test.rs index 348ac3925..904003d4e 100644 --- a/ci/integration-tests/src/tests/cw_core_test.rs +++ b/ci/integration-tests/src/tests/cw_core_test.rs @@ -3,7 +3,7 @@ use crate::helpers::helper::create_dao; use assert_matches::assert_matches; use cosm_orc::orchestrator::error::CosmwasmError::TxError; use cosm_orc::orchestrator::error::ProcessError; -use cosmwasm_std::{to_binary, Addr, CosmosMsg, Decimal, Uint128}; +use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, Decimal, Uint128}; use cw20_stake::msg::{StakedValueResponse, TotalValueResponse}; use cw_utils::Duration; @@ -40,7 +40,7 @@ fn execute_execute_admin_msgs(chain: &mut Chain) { &dao_interface::msg::ExecuteMsg::ExecuteAdminMsgs { msgs: vec![CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: dao.addr, - msg: to_binary(&dao_interface::msg::ExecuteMsg::Pause { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::Pause { duration: Duration::Time(100), }) .unwrap(), @@ -79,7 +79,7 @@ fn execute_execute_admin_msgs(chain: &mut Chain) { &dao_interface::msg::ExecuteMsg::ExecuteAdminMsgs { msgs: vec![CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: dao.addr, - msg: to_binary(&dao_interface::msg::ExecuteMsg::Pause { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::Pause { duration: Duration::Height(100), }) .unwrap(), @@ -139,7 +139,7 @@ fn execute_items(chain: &mut Chain) { &dao_interface::msg::ExecuteMsg::ExecuteAdminMsgs { msgs: vec![CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: dao.addr.clone(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::SetItem { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::SetItem { key: "meme".to_string(), value: "foobar".to_string(), }) @@ -174,7 +174,7 @@ fn execute_items(chain: &mut Chain) { &dao_interface::msg::ExecuteMsg::ExecuteAdminMsgs { msgs: vec![CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute { contract_addr: dao.addr, - msg: to_binary(&dao_interface::msg::ExecuteMsg::RemoveItem { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::RemoveItem { key: "meme".to_string(), }) .unwrap(), diff --git a/ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs b/ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs index 70ef1b96f..dd13c47fb 100644 --- a/ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs +++ b/ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs @@ -1,7 +1,6 @@ use cosm_orc::orchestrator::{ExecReq, SigningKey}; use cosmwasm_std::{Binary, Empty, Uint128}; use cw_utils::Duration; -use dao_interface::state::Admin; use test_context::test_context; use dao_voting_cw721_staked as module; @@ -38,7 +37,6 @@ pub fn instantiate_cw721_base(chain: &mut Chain, key: &SigningKey, minter: &str) fn setup_test( chain: &mut Chain, - owner: Option, unstaking_duration: Option, key: &SigningKey, minter: &str, @@ -50,9 +48,11 @@ fn setup_test( CONTRACT_NAME, "instantiate_dao_voting_cw721_staked", &module::msg::InstantiateMsg { - owner, - nft_address: cw721.clone(), + nft_contract: module::msg::NftContract::Existing { + address: cw721.clone(), + }, unstaking_duration, + active_threshold: None, }, key, None, @@ -93,12 +93,12 @@ pub fn mint_nft(chain: &mut Chain, sender: &SigningKey, receiver: &str, token_id .execute( CW721_NAME, "mint_nft", - &cw721_base::ExecuteMsg::Mint::(cw721_base::MintMsg { + &cw721_base::ExecuteMsg::Mint:: { token_id: token_id.to_string(), owner: receiver.to_string(), token_uri: None, extension: Empty::default(), - }), + }, sender, vec![], ) @@ -166,7 +166,7 @@ fn cw721_stake_tokens(chain: &mut Chain) { let user_addr = chain.users["user1"].account.address.clone(); let user_key = chain.users["user1"].key.clone(); - let CommonTest { module, .. } = setup_test(chain, None, None, &user_key, &user_addr); + let CommonTest { module, .. } = setup_test(chain, None, &user_key, &user_addr); mint_and_stake_nft(chain, &user_key, &user_addr, &module, "a"); @@ -199,13 +199,8 @@ fn cw721_stake_max_claims_works(chain: &mut Chain) { let user_addr = chain.users["user1"].account.address.clone(); let user_key = chain.users["user1"].key.clone(); - let CommonTest { module, .. } = setup_test( - chain, - None, - Some(Duration::Height(1)), - &user_key, - &user_addr, - ); + let CommonTest { module, .. } = + setup_test(chain, Some(Duration::Height(1)), &user_key, &user_addr); // Create `MAX_CLAIMS` claims. @@ -220,14 +215,12 @@ fn cw721_stake_max_claims_works(chain: &mut Chain) { reqs.push(ExecReq { contract_name: CW721_NAME.to_string(), - msg: Box::new(cw721_base::ExecuteMsg::Mint::( - cw721_base::MintMsg { - token_id: token_id.clone(), - owner: user_addr.to_string(), - token_uri: None, - extension: Empty::default(), - }, - )), + msg: Box::new(cw721_base::ExecuteMsg::Mint:: { + token_id: token_id.clone(), + owner: user_addr.to_string(), + token_uri: None, + extension: Empty::default(), + }), funds: vec![], }); diff --git a/ci/integration-tests/src/tests/proposal_gas_test.rs b/ci/integration-tests/src/tests/proposal_gas_test.rs index 697125c81..3c19c559a 100644 --- a/ci/integration-tests/src/tests/proposal_gas_test.rs +++ b/ci/integration-tests/src/tests/proposal_gas_test.rs @@ -1,5 +1,4 @@ -use cosmwasm_std::{to_binary, CosmosMsg, Empty, WasmMsg}; -use cw721_base::MintMsg; +use cosmwasm_std::{to_json_binary, CosmosMsg, Empty, WasmMsg}; use dao_proposal_single::query::ProposalResponse; use dao_voting::voting::Vote; use test_context::test_context; @@ -16,14 +15,13 @@ fn mint_mint_mint_mint(cw721: &str, owner: &str, mints: u64) -> Vec { .map(|mint| { WasmMsg::Execute { contract_addr: cw721.to_string(), - msg: to_binary(&cw721_base::msg::ExecuteMsg::Mint::( - MintMsg:: { + msg: to_json_binary(&cw721_base::msg::ExecuteMsg::Mint::{ token_id: mint.to_string(), owner: owner.to_string(), token_uri: Some("https://bafkreibufednctf2f2bpduiibgkvpqcw5rtdmhqh2htqx3qbdnji4h55hy.ipfs.nftstorage.link".to_string()), extension: Empty::default(), }, - )) + ) .unwrap(), funds: vec![], } diff --git a/contracts/dao-dao-core/README.md b/contracts/dao-dao-core/README.md index 148b1e0ef..6eb81641d 100644 --- a/contracts/dao-dao-core/README.md +++ b/contracts/dao-dao-core/README.md @@ -1,5 +1,8 @@ # dao-dao-core +[![dao-dao-core on crates.io](https://img.shields.io/crates/v/dao-dao-core.svg?logo=rust)](https://crates.io/crates/dao-dao-core) +[![docs.rs](https://img.shields.io/docsrs/dao-dao-core?logo=docsdotrs)](https://docs.rs/dao-dao-core/latest/dao_dao_core/index.html) + This contract is the core module for all DAO DAO DAOs. It handles management of voting power and proposal modules, executes messages, and holds the DAO's treasury. diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index fec05f159..7bd23a28e 100644 --- a/contracts/dao-dao-core/schema/dao-dao-core.json +++ b/contracts/dao-dao-core/schema/dao-dao-core.json @@ -1,6 +1,6 @@ { "contract_name": "dao-dao-core", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -125,6 +125,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "InitialItem": { "description": "Information about an item to be stored in the items list.", "type": "object", @@ -149,6 +164,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -170,6 +186,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -184,6 +207,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" } } }, @@ -1029,7 +1056,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -1147,7 +1174,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -1159,6 +1186,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -1180,6 +1208,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -1398,7 +1433,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -2014,6 +2049,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "MigrateParams": { "type": "object", "required": [ @@ -2096,6 +2146,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -2117,6 +2168,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2209,6 +2267,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" + }, "V1CodeIds": { "type": "object", "required": [ diff --git a/contracts/dao-dao-core/src/contract.rs b/contracts/dao-dao-core/src/contract.rs index 97f3a7d3e..56cd5c526 100644 --- a/contracts/dao-dao-core/src/contract.rs +++ b/contracts/dao-dao-core/src/contract.rs @@ -1,8 +1,8 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order, - Reply, Response, StdError, StdResult, SubMsg, WasmMsg, + from_json, to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, + Order, Reply, Response, StdError, StdResult, SubMsg, WasmMsg, }; use cw2::{get_contract_version, set_contract_version, ContractVersion}; use cw_paginate_storage::{paginate_map, paginate_map_keys, paginate_map_values}; @@ -579,22 +579,22 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { pub fn query_admin(deps: Deps) -> StdResult { let admin = ADMIN.load(deps.storage)?; - to_binary(&admin) + to_json_binary(&admin) } pub fn query_admin_nomination(deps: Deps) -> StdResult { let nomination = NOMINATED_ADMIN.may_load(deps.storage)?; - to_binary(&AdminNominationResponse { nomination }) + to_json_binary(&AdminNominationResponse { nomination }) } pub fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - to_binary(&config) + to_json_binary(&config) } pub fn query_voting_module(deps: Deps) -> StdResult { let voting_module = VOTING_MODULE.load(deps.storage)?; - to_binary(&voting_module) + to_json_binary(&voting_module) } pub fn query_proposal_modules( @@ -615,7 +615,7 @@ pub fn query_proposal_modules( // // Even if this does lock up one can determine the existing // proposal modules by looking at past transactions on chain. - to_binary(&paginate_map_values( + to_json_binary(&paginate_map_values( deps, &PROPOSAL_MODULES, start_after @@ -645,7 +645,7 @@ pub fn query_active_proposal_modules( let limit = limit.unwrap_or(values.len() as u32); - to_binary::>( + to_json_binary::>( &values .into_iter() .filter(|module: &ProposalModule| module.status == ProposalModuleStatus::Enabled) @@ -668,7 +668,7 @@ fn get_pause_info(deps: Deps, env: Env) -> StdResult { } pub fn query_paused(deps: Deps, env: Env) -> StdResult { - to_binary(&get_pause_info(deps, env)?) + to_json_binary(&get_pause_info(deps, env)?) } pub fn query_dump_state(deps: Deps, env: Env) -> StdResult { @@ -683,7 +683,7 @@ pub fn query_dump_state(deps: Deps, env: Env) -> StdResult { let version = get_contract_version(deps.storage)?; let active_proposal_module_count = ACTIVE_PROPOSAL_MODULE_COUNT.load(deps.storage)?; let total_proposal_module_count = TOTAL_PROPOSAL_MODULE_COUNT.load(deps.storage)?; - to_binary(&DumpStateResponse { + to_json_binary(&DumpStateResponse { admin, config, version, @@ -705,7 +705,7 @@ pub fn query_voting_power_at_height( voting_module, &voting::Query::VotingPowerAtHeight { height, address }, )?; - to_binary(&voting_power) + to_json_binary(&voting_power) } pub fn query_total_power_at_height(deps: Deps, height: Option) -> StdResult { @@ -713,17 +713,17 @@ pub fn query_total_power_at_height(deps: Deps, height: Option) -> StdResult let total_power: voting::TotalPowerAtHeightResponse = deps .querier .query_wasm_smart(voting_module, &voting::Query::TotalPowerAtHeight { height })?; - to_binary(&total_power) + to_json_binary(&total_power) } pub fn query_get_item(deps: Deps, item: String) -> StdResult { let item = ITEMS.may_load(deps.storage, item)?; - to_binary(&GetItemResponse { item }) + to_json_binary(&GetItemResponse { item }) } pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; - to_binary(&dao_interface::voting::InfoResponse { info }) + to_json_binary(&dao_interface::voting::InfoResponse { info }) } pub fn query_list_items( @@ -731,7 +731,7 @@ pub fn query_list_items( start_after: Option, limit: Option, ) -> StdResult { - to_binary(&paginate_map( + to_json_binary(&paginate_map( deps, &ITEMS, start_after, @@ -745,7 +745,7 @@ pub fn query_cw20_list( start_after: Option, limit: Option, ) -> StdResult { - to_binary(&paginate_map_keys( + to_json_binary(&paginate_map_keys( deps, &CW20_LIST, start_after @@ -761,7 +761,7 @@ pub fn query_cw721_list( start_after: Option, limit: Option, ) -> StdResult { - to_binary(&paginate_map_keys( + to_json_binary(&paginate_map_keys( deps, &CW721_LIST, start_after @@ -802,7 +802,7 @@ pub fn query_cw20_balances( }) }) .collect::>>()?; - to_binary(&balances) + to_json_binary(&balances) } pub fn query_list_sub_daos( @@ -830,18 +830,18 @@ pub fn query_list_sub_daos( }) .collect(); - to_binary(&subdaos) + to_json_binary(&subdaos) } pub fn query_dao_uri(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - to_binary(&DaoURIResponse { + to_json_binary(&DaoURIResponse { dao_uri: config.dao_uri, }) } pub fn query_proposal_module_count(deps: Deps) -> StdResult { - to_binary(&ProposalModuleCountResponse { + to_json_binary(&ProposalModuleCountResponse { active_proposal_module_count: ACTIVE_PROPOSAL_MODULE_COUNT.load(deps.storage)?, total_proposal_module_count: TOTAL_PROPOSAL_MODULE_COUNT.load(deps.storage)?, }) @@ -904,12 +904,13 @@ pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result Result from_binary::(&data) + Some(data) => from_json::(&data) .map(|m| m.msgs) .unwrap_or_else(|_| vec![]), None => vec![], @@ -977,7 +978,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result from_binary::(&data) + Some(data) => from_json::(&data) .map(|m| m.msgs) .unwrap_or_else(|_| vec![]), None => vec![], diff --git a/contracts/dao-dao-core/src/tests.rs b/contracts/dao-dao-core/src/tests.rs index f2da5064d..88574113c 100644 --- a/contracts/dao-dao-core/src/tests.rs +++ b/contracts/dao-dao-core/src/tests.rs @@ -1,8 +1,8 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - from_slice, + from_json, testing::{mock_dependencies, mock_env}, - to_binary, Addr, CosmosMsg, Empty, Storage, Uint128, WasmMsg, + to_json_binary, Addr, CosmosMsg, Empty, Storage, Uint128, WasmMsg, }; use cw2::{set_contract_version, ContractVersion}; use cw_multi_test::{App, Contract, ContractWrapper, Executor}; @@ -117,15 +117,17 @@ fn test_instantiate_with_n_gov_modules(n: usize) { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: cw20_id, - msg: to_binary(&cw20_instantiate).unwrap(), + msg: to_json_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: (0..n) .map(|n| ModuleInstantiateInfo { code_id: cw20_id, - msg: to_binary(&cw20_instantiate).unwrap(), + msg: to_json_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: format!("governance module {n}"), }) .collect(), @@ -189,21 +191,24 @@ fn test_instantiate_with_submessage_failure() { let mut governance_modules = (0..3) .map(|n| ModuleInstantiateInfo { code_id: cw20_id, - msg: to_binary(&cw20_instantiate).unwrap(), + msg: to_json_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: format!("governance module {n}"), }) .collect::>(); governance_modules.push(ModuleInstantiateInfo { code_id: cw20_id, - msg: to_binary("bad").unwrap(), + msg: to_json_binary("bad").unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "I have a bad instantiate message".to_string(), }); governance_modules.push(ModuleInstantiateInfo { code_id: cw20_id, - msg: to_binary(&cw20_instantiate).unwrap(), + msg: to_json_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "Everybody knowing that goodness is good makes wickedness." @@ -220,8 +225,9 @@ makes wickedness." automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: cw20_id, - msg: to_binary(&cw20_instantiate).unwrap(), + msg: to_json_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: governance_modules, @@ -250,14 +256,16 @@ fn test_update_config() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }], initial_items: None, @@ -303,7 +311,7 @@ fn test_update_config() { msgs: vec![WasmMsg::Execute { contract_addr: gov_addr.to_string(), funds: vec![], - msg: to_binary(&ExecuteMsg::UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig { config: expected_config.clone(), }) .unwrap(), @@ -347,14 +355,16 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: propmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: propmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -416,8 +426,9 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { let to_add: Vec<_> = (0..add) .map(|n| ModuleInstantiateInfo { code_id: propmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: format!("governance module {n}"), }) .collect(); @@ -436,7 +447,7 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { msgs: vec![WasmMsg::Execute { contract_addr: gov_addr.to_string(), funds: vec![], - msg: to_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) .unwrap(), } .into()], @@ -523,14 +534,16 @@ fn test_removed_modules_can_not_execute() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -564,8 +577,9 @@ fn test_removed_modules_can_not_execute() { let to_add = vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "new governance module".to_string(), }]; @@ -579,7 +593,8 @@ fn test_removed_modules_can_not_execute() { msgs: vec![WasmMsg::Execute { contract_addr: gov_addr.to_string(), funds: vec![], - msg: to_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }).unwrap(), + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) + .unwrap(), } .into()], }, @@ -595,8 +610,9 @@ fn test_removed_modules_can_not_execute() { // earlier. This should fail as we have been removed. let to_add = vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "new governance module".to_string(), }]; let to_disable = vec![new_proposal_module.address.to_string()]; @@ -609,7 +625,7 @@ fn test_removed_modules_can_not_execute() { msgs: vec![WasmMsg::Execute { contract_addr: gov_addr.to_string(), funds: vec![], - msg: to_binary(&ExecuteMsg::UpdateProposalModules { + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add: to_add.clone(), to_disable: to_disable.clone(), }) @@ -651,7 +667,8 @@ fn test_removed_modules_can_not_execute() { msgs: vec![WasmMsg::Execute { contract_addr: gov_addr.to_string(), funds: vec![], - msg: to_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }).unwrap(), + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) + .unwrap(), } .into()], }, @@ -680,14 +697,16 @@ fn test_module_already_disabled() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -732,11 +751,12 @@ fn test_module_already_disabled() { msgs: vec![WasmMsg::Execute { contract_addr: gov_addr.to_string(), funds: vec![], - msg: to_binary(&ExecuteMsg::UpdateProposalModules { + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], to_disable, @@ -779,14 +799,16 @@ fn test_swap_voting_module() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -828,11 +850,12 @@ fn test_swap_voting_module() { msgs: vec![WasmMsg::Execute { contract_addr: gov_addr.to_string(), funds: vec![], - msg: to_binary(&ExecuteMsg::UpdateVotingModule { + msg: to_json_binary(&ExecuteMsg::UpdateVotingModule { module: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, }) @@ -880,14 +903,16 @@ fn test_permissions() { image_url: None, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -912,8 +937,9 @@ fn test_permissions() { ExecuteMsg::UpdateVotingModule { module: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, }, @@ -979,14 +1005,16 @@ fn do_standard_instantiate(auto_add: bool, admin: Option) -> (Addr, App) automatically_add_cw721s: auto_add, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: voting_id, - msg: to_binary(&voting_instantiate).unwrap(), + msg: to_json_binary(&voting_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -1032,7 +1060,7 @@ fn test_admin_permissions() { &ExecuteMsg::ExecuteAdminMsgs { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -1051,7 +1079,7 @@ fn test_admin_permissions() { &ExecuteMsg::ExecuteAdminMsgs { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -1082,7 +1110,7 @@ fn test_admin_permissions() { &ExecuteMsg::ExecuteProposalHook { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::NominateAdmin { + msg: to_json_binary(&ExecuteMsg::NominateAdmin { admin: Some("meow".to_string()), }) .unwrap(), @@ -1105,7 +1133,7 @@ fn test_admin_permissions() { &ExecuteMsg::ExecuteAdminMsgs { msgs: vec![WasmMsg::Execute { contract_addr: core_with_admin_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -1124,7 +1152,7 @@ fn test_admin_permissions() { &ExecuteMsg::ExecuteAdminMsgs { msgs: vec![WasmMsg::Execute { contract_addr: core_with_admin_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -1148,7 +1176,7 @@ fn test_admin_permissions() { ); // DAO unpauses after 10 blocks - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); // Admin can nominate a new admin. let res = app.execute_contract( @@ -1369,7 +1397,7 @@ fn test_admin_nomination() { &ExecuteMsg::ExecuteAdminMsgs { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -1390,7 +1418,7 @@ fn test_admin_nomination() { &ExecuteMsg::ExecuteAdminMsgs { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -1414,7 +1442,7 @@ fn test_admin_nomination() { ); // DAO unpauses after 10 blocks - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); // Remove the admin. app.execute_contract( @@ -1611,14 +1639,16 @@ fn test_list_items() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: voting_id, - msg: to_binary(&voting_instantiate).unwrap(), + msg: to_json_binary(&voting_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -1743,14 +1773,16 @@ fn test_instantiate_with_items() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: voting_id, - msg: to_binary(&voting_instantiate).unwrap(), + msg: to_json_binary(&voting_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: Some(initial_items.clone()), @@ -1863,7 +1895,7 @@ fn test_cw20_receive_auto_add() { &cw20::Cw20ExecuteMsg::Send { contract: gov_addr.to_string(), amount: Uint128::new(1), - msg: to_binary(&"").unwrap(), + msg: to_json_binary(&"").unwrap(), }, &[], ) @@ -1998,7 +2030,7 @@ fn test_cw20_receive_no_auto_add() { &cw20::Cw20ExecuteMsg::Send { contract: gov_addr.to_string(), amount: Uint128::new(1), - msg: to_binary(&"").unwrap(), + msg: to_json_binary(&"").unwrap(), }, &[], ) @@ -2079,14 +2111,12 @@ fn test_cw721_receive() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), cw721_addr.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint(cw721_base::msg::MintMsg::< - Option, - > { + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { token_id: "ekez".to_string(), owner: CREATOR_ADDR.to_string(), token_uri: None, extension: None, - }), + }, &[], ) .unwrap(); @@ -2097,7 +2127,7 @@ fn test_cw721_receive() { &cw721_base::msg::ExecuteMsg::, Empty>::SendNft { contract: gov_addr.to_string(), token_id: "ekez".to_string(), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -2211,14 +2241,12 @@ fn test_cw721_receive_no_auto_add() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), cw721_addr.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint(cw721_base::msg::MintMsg::< - Option, - > { + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { token_id: "ekez".to_string(), owner: CREATOR_ADDR.to_string(), token_uri: None, extension: None, - }), + }, &[], ) .unwrap(); @@ -2229,7 +2257,7 @@ fn test_cw721_receive_no_auto_add() { &cw721_base::msg::ExecuteMsg::, Empty>::SendNft { contract: gov_addr.to_string(), token_id: "ekez".to_string(), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -2354,7 +2382,7 @@ fn test_pause() { &ExecuteMsg::ExecuteProposalHook { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -2416,7 +2444,7 @@ fn test_pause() { &ExecuteMsg::ExecuteProposalHook { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -2432,7 +2460,7 @@ fn test_pause() { assert!(matches!(err, ContractError::Paused { .. })); - app.update_block(|mut block| block.height += 9); + app.update_block(|block| block.height += 9); // Still not unpaused. let err: ContractError = app @@ -2442,7 +2470,7 @@ fn test_pause() { &ExecuteMsg::ExecuteProposalHook { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -2458,7 +2486,7 @@ fn test_pause() { assert!(matches!(err, ContractError::Paused { .. })); - app.update_block(|mut block| block.height += 1); + app.update_block(|block| block.height += 1); let paused: PauseInfoResponse = app .wrap() @@ -2478,7 +2506,7 @@ fn test_pause() { &ExecuteMsg::ExecuteProposalHook { msgs: vec![WasmMsg::Execute { contract_addr: core_addr.to_string(), - msg: to_binary(&ExecuteMsg::Pause { + msg: to_json_binary(&ExecuteMsg::Pause { duration: Duration::Height(10), }) .unwrap(), @@ -2578,14 +2606,16 @@ fn test_migrate_from_compatible() { automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: voting_id, - msg: to_binary(&voting_instantiate).unwrap(), + msg: to_json_binary(&voting_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -2612,7 +2642,7 @@ fn test_migrate_from_compatible() { CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: core_addr.to_string(), new_code_id: gov_id, - msg: to_binary(&MigrateMsg::FromCompatible {}).unwrap(), + msg: to_json_binary(&MigrateMsg::FromCompatible {}).unwrap(), }), ) .unwrap(); @@ -2664,20 +2694,20 @@ fn test_migrate_from_beta() { automatically_add_cw721s: false, voting_module_instantiate_info: v1::msg::ModuleInstantiateInfo { code_id: voting_id, - msg: to_binary(&voting_instantiate).unwrap(), + msg: to_json_binary(&voting_instantiate).unwrap(), admin: v1::msg::Admin::CoreContract {}, label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ v1::msg::ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&proposal_instantiate).unwrap(), + msg: to_json_binary(&proposal_instantiate).unwrap(), admin: v1::msg::Admin::CoreContract {}, label: "governance module 1".to_string(), }, v1::msg::ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&proposal_instantiate).unwrap(), + msg: to_json_binary(&proposal_instantiate).unwrap(), admin: v1::msg::Admin::CoreContract {}, label: "governance module 2".to_string(), }, @@ -2701,7 +2731,7 @@ fn test_migrate_from_beta() { CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: core_addr.to_string(), new_code_id: core_id, - msg: to_binary(&MigrateMsg::FromV1 { + msg: to_json_binary(&MigrateMsg::FromV1 { dao_uri: None, params: None, }) @@ -2730,7 +2760,7 @@ fn test_migrate_from_beta() { CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: core_addr.to_string(), new_code_id: core_id, - msg: to_binary(&MigrateMsg::FromV1 { + msg: to_json_binary(&MigrateMsg::FromV1 { dao_uri: None, params: None, }) @@ -2760,7 +2790,7 @@ fn test_migrate_mock() { let proposal_modules_key = Addr::unchecked("addr"); let old_map: Map = Map::new("proposal_modules"); let path = old_map.key(proposal_modules_key.clone()); - deps.storage.set(&path, &to_binary(&Empty {}).unwrap()); + deps.storage.set(&path, &to_json_binary(&Empty {}).unwrap()); // Write to storage in old config format #[cw_serde] @@ -2788,7 +2818,7 @@ fn test_migrate_mock() { let new_path = PROPOSAL_MODULES.key(proposal_modules_key); let prop_module_bytes = deps.storage.get(&new_path).unwrap(); - let module: ProposalModule = from_slice(&prop_module_bytes).unwrap(); + let module: ProposalModule = from_json(prop_module_bytes).unwrap(); assert_eq!(module.address, Addr::unchecked("addr")); assert_eq!(module.prefix, derive_proposal_module_prefix(0).unwrap()); assert_eq!(module.status, ProposalModuleStatus::Enabled {}); @@ -2832,7 +2862,7 @@ fn test_execute_stargate_msg() { &ExecuteMsg::ExecuteProposalHook { msgs: vec![CosmosMsg::Stargate { type_url: "foo_type".to_string(), - value: to_binary("foo_bin").unwrap(), + value: to_json_binary("foo_bin").unwrap(), }], }, &[], @@ -2861,27 +2891,31 @@ fn test_module_prefixes() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "proposal module 1".to_string(), }, ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "proposal module 2".to_string(), }, ModuleInstantiateInfo { code_id: govmod_id, - msg: to_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "proposal module 2".to_string(), }, ], diff --git a/contracts/external/cw-admin-factory/README.md b/contracts/external/cw-admin-factory/README.md index 786329727..2ee2a1d98 100644 --- a/contracts/external/cw-admin-factory/README.md +++ b/contracts/external/cw-admin-factory/README.md @@ -1,5 +1,8 @@ # cw-admin-factory +[![cw-admin-factory on crates.io](https://img.shields.io/crates/v/cw-admin-factory.svg?logo=rust)](https://crates.io/crates/cw-admin-factory) +[![docs.rs](https://img.shields.io/docsrs/cw-admin-factory?logo=docsdotrs)](https://docs.rs/cw-admin-factory/latest/cw_admin_factory/) + Serves as a factory that instantiates contracts and sets them as their own wasm admins. diff --git a/contracts/external/cw-admin-factory/schema/cw-admin-factory.json b/contracts/external/cw-admin-factory/schema/cw-admin-factory.json index 67c92d629..629d84216 100644 --- a/contracts/external/cw-admin-factory/schema/cw-admin-factory.json +++ b/contracts/external/cw-admin-factory/schema/cw-admin-factory.json @@ -1,6 +1,6 @@ { "contract_name": "cw-admin-factory", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-admin-factory/src/tests.rs b/contracts/external/cw-admin-factory/src/tests.rs index 226d1f1ef..3bee180ee 100644 --- a/contracts/external/cw-admin-factory/src/tests.rs +++ b/contracts/external/cw-admin-factory/src/tests.rs @@ -2,7 +2,7 @@ use std::vec; use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, - to_binary, Addr, Binary, Empty, Reply, SubMsg, SubMsgResponse, SubMsgResult, WasmMsg, + to_json_binary, Addr, Binary, Empty, Reply, SubMsg, SubMsgResponse, SubMsgResult, WasmMsg, }; use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; @@ -82,21 +82,24 @@ pub fn test_set_admin() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: cw20_code_id, - msg: to_binary(&cw20_instantiate).unwrap(), + msg: to_json_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ ModuleInstantiateInfo { code_id: cw20_code_id, - msg: to_binary(&cw20_instantiate).unwrap(), + msg: to_json_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "prop module".to_string(), }, ModuleInstantiateInfo { code_id: cw20_code_id, - msg: to_binary(&cw20_instantiate).unwrap(), + msg: to_json_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "prop module 2".to_string(), }, ], @@ -108,7 +111,7 @@ pub fn test_set_admin() { Addr::unchecked("CREATOR"), factory_addr, &ExecuteMsg::InstantiateContractWithSelfAdmin { - instantiate_msg: to_binary(&instantiate_core).unwrap(), + instantiate_msg: to_json_binary(&instantiate_core).unwrap(), code_id: cw_core_code_id, label: "my contract".to_string(), }, diff --git a/contracts/external/cw-fund-distributor/schema/cw-fund-distributor.json b/contracts/external/cw-fund-distributor/schema/cw-fund-distributor.json index e8bf91f56..19a0541ca 100644 --- a/contracts/external/cw-fund-distributor/schema/cw-fund-distributor.json +++ b/contracts/external/cw-fund-distributor/schema/cw-fund-distributor.json @@ -11,6 +11,7 @@ ], "properties": { "active_threshold": { + "description": "The number or percentage of tokens that must be staked for the DAO to be active", "anyOf": [ { "$ref": "#/definitions/ActiveThreshold" diff --git a/contracts/external/cw-fund-distributor/src/contract.rs b/contracts/external/cw-fund-distributor/src/contract.rs index 2e5d4b2d1..c5539d1ef 100644 --- a/contracts/external/cw-fund-distributor/src/contract.rs +++ b/contracts/external/cw-fund-distributor/src/contract.rs @@ -10,8 +10,8 @@ use crate::state::{ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, BankMsg, Binary, Coin, Decimal, Deps, DepsMut, Env, Fraction, MessageInfo, - Order, Response, StdError, StdResult, Uint128, WasmMsg, + to_json_binary, Addr, BankMsg, Binary, Coin, Decimal, Deps, DepsMut, Env, Fraction, + MessageInfo, Order, Response, StdError, StdResult, Uint128, WasmMsg, }; use cw2::set_contract_version; use cw_paginate_storage::paginate_map; @@ -247,7 +247,7 @@ fn get_cw20_claim_wasm_messages( messages.push(WasmMsg::Execute { contract_addr: addr, - msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient: sender.to_string(), amount: entitlement, })?, @@ -406,7 +406,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { pub fn query_voting_contract(deps: Deps) -> StdResult { let contract = VOTING_CONTRACT.load(deps.storage)?; let distribution_height = DISTRIBUTION_HEIGHT.load(deps.storage)?; - to_binary(&VotingContractResponse { + to_json_binary(&VotingContractResponse { contract, distribution_height, }) @@ -414,7 +414,7 @@ pub fn query_voting_contract(deps: Deps) -> StdResult { pub fn query_total_power(deps: Deps) -> StdResult { let total_power: Uint128 = TOTAL_POWER.may_load(deps.storage)?.unwrap_or_default(); - to_binary(&TotalPowerResponse { total_power }) + to_json_binary(&TotalPowerResponse { total_power }) } pub fn query_native_denoms(deps: Deps) -> StdResult { @@ -429,7 +429,7 @@ pub fn query_native_denoms(deps: Deps) -> StdResult { }); } - to_binary(&denom_responses) + to_json_binary(&denom_responses) } pub fn query_cw20_tokens(deps: Deps) -> StdResult { @@ -444,7 +444,7 @@ pub fn query_cw20_tokens(deps: Deps) -> StdResult { }); } - to_binary(&cw20_responses) + to_json_binary(&cw20_responses) } pub fn query_native_entitlement(deps: Deps, sender: Addr, denom: String) -> StdResult { @@ -461,7 +461,7 @@ pub fn query_native_entitlement(deps: Deps, sender: Addr, denom: String) -> StdR total_bal.multiply_ratio(relative_share.numerator(), relative_share.denominator()); let entitlement = total_share.checked_sub(prev_claim)?; - to_binary(&NativeEntitlementResponse { + to_json_binary(&NativeEntitlementResponse { amount: entitlement, denom, }) @@ -483,7 +483,7 @@ pub fn query_cw20_entitlement(deps: Deps, sender: Addr, token: String) -> StdRes total_bal.multiply_ratio(relative_share.numerator(), relative_share.denominator()); let entitlement = total_share.checked_sub(prev_claim)?; - to_binary(&CW20EntitlementResponse { + to_json_binary(&CW20EntitlementResponse { amount: entitlement, token_contract: token, }) @@ -514,7 +514,7 @@ pub fn query_native_entitlements( }); } - to_binary(&entitlements) + to_json_binary(&entitlements) } pub fn query_cw20_entitlements( @@ -544,7 +544,7 @@ pub fn query_cw20_entitlements( }); } - to_binary(&entitlements) + to_json_binary(&entitlements) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/external/cw-fund-distributor/src/testing/adversarial_tests.rs b/contracts/external/cw-fund-distributor/src/testing/adversarial_tests.rs index 9c9736e8c..8df033529 100644 --- a/contracts/external/cw-fund-distributor/src/testing/adversarial_tests.rs +++ b/contracts/external/cw-fund-distributor/src/testing/adversarial_tests.rs @@ -1,6 +1,6 @@ use crate::msg::ExecuteMsg::ClaimAll; use crate::msg::{ExecuteMsg, InstantiateMsg}; -use cosmwasm_std::{to_binary, Addr, Binary, Coin, Empty, Uint128}; +use cosmwasm_std::{to_json_binary, Addr, Binary, Coin, Empty, Uint128}; use cw20::{BalanceResponse, Cw20Coin}; use cw_multi_test::{next_block, App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg}; use cw_utils::Duration; @@ -127,7 +127,7 @@ fn setup_test(initial_balances: Vec) -> BaseTest { &cw20_base::msg::ExecuteMsg::Send { contract: staking_contract.to_string(), amount, - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -203,7 +203,7 @@ pub fn test_claim_lots_of_native_tokens() { .unwrap(); } - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); app.execute_contract( Addr::unchecked("bekauz"), @@ -276,7 +276,7 @@ pub fn test_claim_lots_of_cw20s() { }) .collect(); - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); app.execute_contract( Addr::unchecked("bekauz"), diff --git a/contracts/external/cw-fund-distributor/src/testing/tests.rs b/contracts/external/cw-fund-distributor/src/testing/tests.rs index 6a6944ca2..544856b81 100644 --- a/contracts/external/cw-fund-distributor/src/testing/tests.rs +++ b/contracts/external/cw-fund-distributor/src/testing/tests.rs @@ -3,7 +3,7 @@ use crate::msg::{ NativeEntitlementResponse, QueryMsg, TotalPowerResponse, VotingContractResponse, }; use crate::ContractError; -use cosmwasm_std::{to_binary, Addr, Binary, Coin, Empty, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, Binary, Coin, Empty, Uint128, WasmMsg}; use cw20::Cw20Coin; use cw_multi_test::{next_block, App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg}; @@ -114,7 +114,7 @@ fn setup_test(initial_balances: Vec) -> BaseTest { &cw20_base::msg::ExecuteMsg::Send { contract: staking_contract.to_string(), amount, - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -406,7 +406,6 @@ fn test_fund_cw20() { } #[test] -#[should_panic(expected = "Invalid zero amount")] pub fn test_fund_cw20_zero_amount() { let BaseTest { mut app, @@ -437,7 +436,7 @@ pub fn test_fund_cw20_zero_amount() { token_address, &cw20::Cw20ExecuteMsg::Send { contract: distributor_address.to_string(), - amount: Uint128::zero(), + amount: Uint128::zero(), // since cw20-base v1.1.0 this is allowed msg: Binary::default(), }, &[], @@ -584,7 +583,7 @@ pub fn test_claim_cw20() { let balance = query_cw20_balance(&mut app, token_address.clone(), distributor_address.clone()); assert_eq!(balance.balance, amount); - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); // claim the tokens // should result in an entitlement of (10/(10 + 20))% @@ -655,7 +654,7 @@ pub fn test_claim_cw20_twice() { assert_eq!(balance.balance, amount); - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); // claim the tokens twice app.execute_contract( @@ -724,7 +723,7 @@ pub fn test_claim_cw20s_empty_list() { Addr::unchecked(CREATOR_ADDR), ); - app.update_block(|mut b| b.height += 11); + app.update_block(|b| b.height += 11); let err: ContractError = app .execute_contract( @@ -768,7 +767,7 @@ pub fn test_claim_natives_twice() { Addr::unchecked(CREATOR_ADDR), ); - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); // claim twice app.execute_contract( @@ -831,7 +830,7 @@ pub fn test_claim_natives() { Addr::unchecked(CREATOR_ADDR), ); - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); app.execute_contract( Addr::unchecked("bekauz"), @@ -899,7 +898,7 @@ pub fn test_claim_all() { ); // claiming period - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); app.execute_contract( Addr::unchecked("bekauz"), @@ -964,7 +963,7 @@ pub fn test_claim_natives_empty_list_of_denoms() { Addr::unchecked(CREATOR_ADDR), ); - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); let err: ContractError = app .execute_contract( @@ -1014,7 +1013,7 @@ pub fn test_redistribute_unclaimed_funds() { Addr::unchecked(CREATOR_ADDR), ); - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); // claim the initial allocation equal to 1/3rd of 500000 app.execute_contract( @@ -1045,7 +1044,7 @@ pub fn test_redistribute_unclaimed_funds() { WasmMsg::Migrate { contract_addr: distributor_address.to_string(), new_code_id: distributor_id, - msg: to_binary(migrate_msg).unwrap(), + msg: to_json_binary(migrate_msg).unwrap(), } .into(), ) @@ -1122,7 +1121,7 @@ pub fn test_unauthorized_redistribute_unclaimed_funds() { WasmMsg::Migrate { contract_addr: distributor_address.to_string(), new_code_id: distributor_id, - msg: to_binary(migrate_msg).unwrap(), + msg: to_json_binary(migrate_msg).unwrap(), } .into(), ) @@ -1287,7 +1286,7 @@ pub fn test_fund_cw20_during_claiming_period() { ); // skip into the claiming period - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); // attempt to fund the contract let err: ContractError = app @@ -1324,7 +1323,7 @@ pub fn test_fund_natives_during_claiming_period() { mint_natives(&mut app, Addr::unchecked(CREATOR_ADDR), amount); // skip into the claim period - app.update_block(|mut block| block.height += 11); + app.update_block(|block| block.height += 11); // attempt to fund let err: ContractError = app diff --git a/contracts/external/cw-payroll-factory/README.md b/contracts/external/cw-payroll-factory/README.md index 553134a96..ace69cabd 100644 --- a/contracts/external/cw-payroll-factory/README.md +++ b/contracts/external/cw-payroll-factory/README.md @@ -1,5 +1,8 @@ # cw-payroll-factory +[![cw-payroll-factory on crates.io](https://img.shields.io/crates/v/cw-payroll-factory.svg?logo=rust)](https://crates.io/crates/cw-payroll-factory) +[![docs.rs](https://img.shields.io/docsrs/cw-payroll-factory?logo=docsdotrs)](https://docs.rs/cw-payroll-factory/latest/cw_payroll_factory/) + Serves as a factory that instantiates [cw-vesting](../cw-vesting) contracts and stores them in an indexed maps for easy querying by recipient or the instantiator (i.e. give me all of my vesting payment contracts or give me all of a DAO's vesting payment contracts). An optional `owner` can be specified when instantiating `cw-payroll-factory` that limits contract instantiation to a single account. diff --git a/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json b/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json index f8faac397..890437b2a 100644 --- a/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json +++ b/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json @@ -1,6 +1,6 @@ { "contract_name": "cw-payroll-factory", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-payroll-factory/src/contract.rs b/contracts/external/cw-payroll-factory/src/contract.rs index aa7af2b65..ff033174d 100644 --- a/contracts/external/cw-payroll-factory/src/contract.rs +++ b/contracts/external/cw-payroll-factory/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Reply, + from_json, to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Reply, Response, StdResult, SubMsg, WasmMsg, }; use cosmwasm_std::{Addr, Coin}; @@ -72,7 +72,7 @@ pub fn execute_receive_cw20( // Only accepts cw20 tokens nonpayable(&info)?; - let msg: ReceiveMsg = from_binary(&receive_msg.msg)?; + let msg: ReceiveMsg = from_json(&receive_msg.msg)?; if TMP_INSTANTIATOR_INFO.may_load(deps.storage)?.is_some() { return Err(ContractError::Reentrancy); @@ -130,16 +130,6 @@ pub fn instantiate_contract( { return Err(ContractError::Unauthorized {}); } - // No owner is always allowed. If an owner is specified, it must - // exactly match the owner of the contract. - if instantiate_msg.owner.as_deref().map_or(false, |i| { - ownership.owner.as_ref().map_or(true, |o| o.as_str() != i) - }) { - return Err(ContractError::OwnerMissmatch { - actual: instantiate_msg.owner, - expected: ownership.owner.map(|a| a.into_string()), - }); - } let code_id = VESTING_CODE_ID.load(deps.storage)?; @@ -147,7 +137,7 @@ pub fn instantiate_contract( let instantiate = WasmMsg::Instantiate { admin: instantiate_msg.owner.clone(), code_id, - msg: to_binary(&instantiate_msg)?, + msg: to_json_binary(&instantiate_msg)?, funds: funds.unwrap_or_default(), label, }; @@ -194,7 +184,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { .flat_map(|vc| Ok::(vc?.1)) .collect(); - Ok(to_binary(&res)?) + Ok(to_json_binary(&res)?) } QueryMsg::ListVestingContractsReverse { start_before, @@ -209,7 +199,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { .flat_map(|vc| Ok::(vc?.1)) .collect(); - Ok(to_binary(&res)?) + Ok(to_json_binary(&res)?) } QueryMsg::ListVestingContractsByInstantiator { instantiator, @@ -231,7 +221,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { .flat_map(|vc| Ok::(vc?.1)) .collect(); - Ok(to_binary(&res)?) + Ok(to_json_binary(&res)?) } QueryMsg::ListVestingContractsByInstantiatorReverse { instantiator, @@ -253,7 +243,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { .flat_map(|vc| Ok::(vc?.1)) .collect(); - Ok(to_binary(&res)?) + Ok(to_json_binary(&res)?) } QueryMsg::ListVestingContractsByRecipient { recipient, @@ -275,7 +265,7 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { .flat_map(|vc| Ok::(vc?.1)) .collect(); - Ok(to_binary(&res)?) + Ok(to_json_binary(&res)?) } QueryMsg::ListVestingContractsByRecipientReverse { recipient, @@ -297,10 +287,10 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { .flat_map(|vc| Ok::(vc?.1)) .collect(); - Ok(to_binary(&res)?) + Ok(to_json_binary(&res)?) } - QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), - QueryMsg::CodeId {} => to_binary(&VESTING_CODE_ID.load(deps.storage)?), + QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), + QueryMsg::CodeId {} => to_json_binary(&VESTING_CODE_ID.load(deps.storage)?), } } @@ -339,10 +329,10 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result, - expected: Option, - }, - #[error("{0}")] ParseReplyError(#[from] ParseReplyError), diff --git a/contracts/external/cw-payroll-factory/src/tests.rs b/contracts/external/cw-payroll-factory/src/tests.rs index 9245fbad0..1feffd463 100644 --- a/contracts/external/cw-payroll-factory/src/tests.rs +++ b/contracts/external/cw-payroll-factory/src/tests.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{coins, to_binary, Addr, Empty, Uint128}; +use cosmwasm_std::{coins, to_json_binary, Addr, Empty, Uint128}; use cw20::{Cw20Coin, Cw20ExecuteMsg}; use cw_denom::UncheckedDenom; use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg}; @@ -296,7 +296,7 @@ pub fn test_instantiate_cw20_payroll_contract() { &Cw20ExecuteMsg::Send { contract: factory_addr.to_string(), amount: instantiate_payroll_msg.total, - msg: to_binary(&ReceiveMsg::InstantiatePayrollContract { + msg: to_json_binary(&ReceiveMsg::InstantiatePayrollContract { instantiate_msg: instantiate_payroll_msg, label: "Payroll".to_string(), }) @@ -381,40 +381,6 @@ fn test_instantiate_wrong_ownership_native() { ) .unwrap(); - let err: ContractError = app - .execute_contract( - Addr::unchecked(ALICE), - factory_addr.clone(), - &ExecuteMsg::InstantiateNativePayrollContract { - instantiate_msg: PayrollInstantiateMsg { - owner: Some("ekez".to_string()), - recipient: BOB.to_string(), - title: "title".to_string(), - description: Some("desc".to_string()), - total: amount, - denom: unchecked_denom.clone(), - schedule: Schedule::SaturatingLinear, - vesting_duration_seconds: 200, - unbonding_duration_seconds: 2592000, // 30 days - start_time: None, - }, - label: "vesting".to_string(), - }, - &coins(amount.u128(), NATIVE_DENOM), - ) - .unwrap_err() - .downcast() - .unwrap(); - - // Can't instantiate with an owner who is not the factory owner. - assert_eq!( - err, - ContractError::OwnerMissmatch { - actual: Some("ekez".to_string()), - expected: Some(ALICE.to_string()) - } - ); - let err: ContractError = app .execute_contract( Addr::unchecked("ekez"), @@ -444,92 +410,6 @@ fn test_instantiate_wrong_ownership_native() { assert_eq!(err, ContractError::Unauthorized {}); } -#[test] -fn test_instantiate_wrong_owner_cw20() { - let mut app = App::default(); - let code_id = app.store_code(factory_contract()); - let cw20_code_id = app.store_code(cw20_contract()); - let cw_vesting_code_id = app.store_code(cw_vesting_contract()); - - let cw20_addr = app - .instantiate_contract( - cw20_code_id, - Addr::unchecked(ALICE), - &cw20_base::msg::InstantiateMsg { - name: "cw20 token".to_string(), - symbol: "cwtwenty".to_string(), - decimals: 6, - initial_balances: vec![Cw20Coin { - address: ALICE.to_string(), - amount: Uint128::new(INITIAL_BALANCE), - }], - mint: None, - marketing: None, - }, - &[], - "cw20-base", - None, - ) - .unwrap(); - - let instantiate = InstantiateMsg { - owner: Some(ALICE.to_string()), - vesting_code_id: cw_vesting_code_id, - }; - let factory_addr = app - .instantiate_contract( - code_id, - Addr::unchecked("CREATOR"), - &instantiate, - &[], - "cw-admin-factory", - None, - ) - .unwrap(); - - let amount = Uint128::new(1000000); - let unchecked_denom = UncheckedDenom::Cw20(cw20_addr.to_string()); - - let instantiate_payroll_msg = PayrollInstantiateMsg { - owner: Some(BOB.to_string()), - recipient: BOB.to_string(), - title: "title".to_string(), - description: Some("desc".to_string()), - total: amount, - denom: unchecked_denom, - schedule: Schedule::SaturatingLinear, - vesting_duration_seconds: 200, - unbonding_duration_seconds: 2592000, // 30 days - start_time: None, - }; - - let err: ContractError = app - .execute_contract( - Addr::unchecked(ALICE), - cw20_addr, - &Cw20ExecuteMsg::Send { - contract: factory_addr.to_string(), - amount: instantiate_payroll_msg.total, - msg: to_binary(&ReceiveMsg::InstantiatePayrollContract { - instantiate_msg: instantiate_payroll_msg, - label: "Payroll".to_string(), - }) - .unwrap(), - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!( - err, - ContractError::OwnerMissmatch { - actual: Some(BOB.to_string()), - expected: Some(ALICE.to_string()) - } - ) -} - #[test] fn test_update_vesting_code_id() { let mut app = App::default(); @@ -699,7 +579,7 @@ pub fn test_inconsistent_cw20_amount() { &Cw20ExecuteMsg::Send { contract: factory_addr.to_string(), amount, - msg: to_binary(&ReceiveMsg::InstantiatePayrollContract { + msg: to_json_binary(&ReceiveMsg::InstantiatePayrollContract { instantiate_msg: instantiate_payroll_msg, label: "Payroll".to_string(), }) 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-token-swap/README.md b/contracts/external/cw-token-swap/README.md index 82695b3ca..3427bddae 100644 --- a/contracts/external/cw-token-swap/README.md +++ b/contracts/external/cw-token-swap/README.md @@ -1,5 +1,8 @@ # cw-token-swap +[![cw-token-swap on crates.io](https://img.shields.io/crates/v/cw-token-swap.svg?logo=rust)](https://crates.io/crates/cw-token-swap) +[![docs.rs](https://img.shields.io/docsrs/cw-token-swap?logo=docsdotrs)](https://docs.rs/cw-token-swap/latest/cw_token_swap/) + This is an escrow token swap contract for swapping between native and cw20 tokens. The contract is instantiated with two counterparties and their promised funds. Promised funds may either be native tokens or diff --git a/contracts/external/cw-token-swap/schema/cw-token-swap.json b/contracts/external/cw-token-swap/schema/cw-token-swap.json index dd0400086..64ab20225 100644 --- a/contracts/external/cw-token-swap/schema/cw-token-swap.json +++ b/contracts/external/cw-token-swap/schema/cw-token-swap.json @@ -1,6 +1,6 @@ { "contract_name": "cw-token-swap", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-token-swap/src/contract.rs b/contracts/external/cw-token-swap/src/contract.rs index 283ea5ac0..313ee6398 100644 --- a/contracts/external/cw-token-swap/src/contract.rs +++ b/contracts/external/cw-token-swap/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, + to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, }; use cw2::set_contract_version; use cw_storage_plus::Item; @@ -240,7 +240,7 @@ pub fn query_status(deps: Deps) -> StdResult { let counterparty_one = COUNTERPARTY_ONE.load(deps.storage)?; let counterparty_two = COUNTERPARTY_TWO.load(deps.storage)?; - to_binary(&StatusResponse { + to_json_binary(&StatusResponse { counterparty_one, counterparty_two, }) diff --git a/contracts/external/cw-token-swap/src/state.rs b/contracts/external/cw-token-swap/src/state.rs index 7ea5cd1a5..5a7071567 100644 --- a/contracts/external/cw-token-swap/src/state.rs +++ b/contracts/external/cw-token-swap/src/state.rs @@ -1,5 +1,7 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{to_binary, Addr, BankMsg, Coin, CosmosMsg, Deps, StdError, Uint128, WasmMsg}; +use cosmwasm_std::{ + to_json_binary, Addr, BankMsg, Coin, CosmosMsg, Deps, StdError, Uint128, WasmMsg, +}; use cw_storage_plus::Item; use crate::{ @@ -85,7 +87,7 @@ impl CheckedTokenInfo { amount, } => WasmMsg::Execute { contract_addr: contract_addr.into_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient: recipient.to_string(), amount, })?, @@ -133,7 +135,7 @@ mod tests { CosmosMsg::Wasm(WasmMsg::Execute { funds: vec![], contract_addr: "ekez_token".to_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient: "ekez".to_string(), amount: Uint128::new(100) }) diff --git a/contracts/external/cw-token-swap/src/tests.rs b/contracts/external/cw-token-swap/src/tests.rs index 7e6073f20..e2d2a9913 100644 --- a/contracts/external/cw-token-swap/src/tests.rs +++ b/contracts/external/cw-token-swap/src/tests.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ testing::{mock_dependencies, mock_env}, - to_binary, Addr, Coin, Empty, Uint128, + to_json_binary, Addr, Coin, Empty, Uint128, }; use cw20::Cw20Coin; use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg}; @@ -95,7 +95,7 @@ fn test_simple_escrow() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.to_string(), amount: Uint128::new(100), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -209,7 +209,7 @@ fn test_withdraw() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.to_string(), amount: Uint128::new(100), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -380,7 +380,7 @@ fn test_withdraw_post_completion() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.to_string(), amount: Uint128::new(100), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -618,7 +618,7 @@ fn test_fund_non_counterparty() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.to_string(), amount: Uint128::new(100), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -714,7 +714,7 @@ fn test_fund_twice() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.to_string(), amount: Uint128::new(100), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -763,7 +763,7 @@ fn test_fund_twice() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.into_string(), amount: Uint128::new(100), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -835,7 +835,7 @@ fn test_fund_invalid_amount() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.to_string(), amount: Uint128::new(10), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -1022,7 +1022,7 @@ fn test_fund_invalid_cw20() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.to_string(), amount: Uint128::new(100), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -1041,7 +1041,7 @@ fn test_fund_invalid_cw20() { &cw20::Cw20ExecuteMsg::Send { contract: escrow.to_string(), amount: Uint128::new(100), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) diff --git a/contracts/voting/dao-voting-native-staked/.cargo/config b/contracts/external/cw-tokenfactory-issuer/.cargo/config similarity index 100% rename from contracts/voting/dao-voting-native-staked/.cargo/config rename to contracts/external/cw-tokenfactory-issuer/.cargo/config diff --git a/contracts/external/cw-tokenfactory-issuer/Cargo.toml b/contracts/external/cw-tokenfactory-issuer/Cargo.toml new file mode 100644 index 000000000..66fc81ada --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/Cargo.toml @@ -0,0 +1,54 @@ +[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 } +cw2 = { workspace = true } +cw-ownable = { workspace = true } +cw-storage-plus = { 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..1cfdcf352 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/README.md @@ -0,0 +1,104 @@ +# `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 +- Updating the contract owner or Token Factory admin +- And more! (see [Advanced Features](#advanced-features)) + +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::UpdateOwnership {}`), 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. + +Ownership functionality for this contract is implemented using the `cw-ownable` library. + +The `cw_tokenfactory_issuer` contract is also the admin of newly created Token Factory denoms. 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. + +## Instantiation + +When instantiating `cw-tokenfactory-issuer`, you can either create a `new` or an `existing`. + +### 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}" + } +} +``` + +## Renouncing Token Factory Admin +Some DAOs or protocols after the initial setup phase may wish to render their tokens immutable, permanently disabling features of this contract. + +To do so, they must execute a `ExcuteMessage::UpdateTokenFactoryAdmin {}` method, setting the Admin to a null address or the bank module for your respective chain. + +For example, on Juno this could be: + +``` json +{ + "update_token_factory_admin": { + "new_admin": "juno1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq" + } +} +``` + +The Token Factory standard requires a Token Factory admin per token, by setting to a null address the Token is rendered immutable and the `cw-tokenfactory-issuer` will be unable to make future updates. This is secure as the cryptography that underlies the chain enforces that even with the largest super computers in the world it would take an astonomically large amount of time to compute the private key for this address. + +### Advanced Features + +This contract supports a number of advanced features which DAOs or token issuers may wist to leverage: +- Freezing and unfreezing transfers, with an allowlist to allow specified addresses to allow transfer to or from +- Denylist to prevent certain addresses from transferring +- Force transfering tokens via the contract owner + +**By default, these features are disabled**, and must be explictly enabled by the contract owner (for example via a DAO governance prop). + +Moreover, for these features to work, your chain must support the `MsgBeforeSendHook` bank module hook. This is not yet available on every chain using Token Factory, and so denylisting and freezing features are not available if `MsgBeforeSendHook` is not supported. + +On chains where `MsgBeforeSendHook` is supported, DAOs or issuers wishing to leverage these features must set the before send hook with `ExecuteMsg::SetBeforeSendHook {}`. + +This method takes a `cosmwasm_address`, which is the address of a contract implement a `SudoMsg::BlockBeforeSend` entrypoint. Normally this will be the address of the `cw_tokenfactory_issuer` contract itself, but it is possible to specify a custom contract. This contract contains a `SudoMsg::BlockBeforeSend` hook that allows for the denylisting of specific accounts as well as the freezing of all transfers if necessary. + +Example message to set before send hook: +``` json +{ + "set_before_send_hook": { + "cosmwasm_address": "
" + } +} +``` + +DAOs or issuers wishing to leverage these features on chains without support can call `ExecuteMsg::SetBeforeSendHook {}` when support is added. + +If a DAO or issuer wishes to disable and removed before send hook related functionality, they simply need to call `ExecuteMsg::SetBeforeSendHook {}` with an empty string for the `cosmwasm_address` like so: +``` json +{ + "set_before_send_hook": { + "cosmwasm_address": "" + } +} +``` 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..069b3b766 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json @@ -0,0 +1,1253 @@ +{ + "contract_name": "cw-tokenfactory-issuer", + "contract_version": "2.3.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "The message used to create a new instance of this smart contract.", + "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", + "description": "State changing methods available to this smart contract.", + "oneOf": [ + { + "description": "Allow adds the target address to the allowlist to be able to send or recieve tokens even if the token is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature to work.\n\nThis functionality is intedended for DAOs who do not wish to have a their tokens liquid while bootstrapping their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens).", + "type": "object", + "required": [ + "allow" + ], + "properties": { + "allow": { + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "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": "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": "Deny adds the target address to the denylist, whis prevents them from sending/receiving the token attached to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this feature to work as intended.", + "type": "object", + "required": [ + "deny" + ], + "properties": { + "deny": { + "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. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature 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 + }, + { + "description": "Attempt to SetBeforeSendHook on the token attached to this contract. This will fail if the chain does not support bank module hooks (many Token Factory implementations do not yet support).\n\nThis takes a cosmwasm_address as an argument, which is the address of the contract that will be called before every token transfer. Normally, this will be the issuer contract itself, though it can be a custom contract for greater flexibility.\n\nSetting the address to an empty string will remove the SetBeforeSendHook.\n\nThis method can only be called by the contract owner.", + "type": "object", + "required": [ + "set_before_send_hook" + ], + "properties": { + "set_before_send_hook": { + "type": "object", + "required": [ + "cosmwasm_address" + ], + "properties": { + "cosmwasm_address": { + "type": "string" + } + }, + "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": "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": "Updates the admin of the Token Factory token. Normally this is the cw-tokenfactory-issuer contract itself. This is intended to be used only if you seek to transfer ownership of the Token somewhere else (i.e. to another management contract).", + "type": "object", + "required": [ + "update_token_factory_admin" + ], + "properties": { + "update_token_factory_admin": { + "type": "object", + "required": [ + "new_admin" + ], + "properties": { + "new_admin": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Updates the owner of this contract who is allowed to call privileged methods. NOTE: this is separate from the Token Factory token admin, for this contract to work at all, it needs to the be the Token Factory token admin.\n\nNormally, the contract owner will be a DAO.\n\nThe `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "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 + } + } + }, + "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 + } + ] + }, + "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" + } + } + }, + "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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "Queries supported by this smart contract.", + "oneOf": [ + { + "description": "Returns if token transfer is disabled. Response: IsFrozenResponse", + "type": "object", + "required": [ + "is_frozen" + ], + "properties": { + "is_frozen": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "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 + }, + { + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the burn 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": "Enumerates over all burn allownances. Response: AllowancesResponse", + "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": "Returns the mint 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": "Enumerates over all mint 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": "Returns wether the user is on denylist or not. Response: StatusResponse", + "type": "object", + "required": [ + "is_denied" + ], + "properties": { + "is_denied": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Enumerates over all addresses on the denylist. Response: DenylistResponse", + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns wether the user is on the allowlist or not. Response: StatusResponse", + "type": "object", + "required": [ + "is_allowed" + ], + "properties": { + "is_allowed": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Enumerates over all addresses on the allowlist. Response: AllowlistResponse", + "type": "object", + "required": [ + "allowlist" + ], + "properties": { + "allowlist": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about the BeforeSendHook for the token. Note: many Token Factory chains do not yet support this feature.\n\nThe information returned is: - Whether features in this contract that require MsgBeforeSendHook are enabled. - The address of the BeforeSendHook contract if configured.\n\nResponse: BeforeSendHookInfo", + "type": "object", + "required": [ + "before_send_hook_info" + ], + "properties": { + "before_send_hook_info": { + "type": "object", + "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": { + "allowlist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowlistResponse", + "description": "Returns a list of addresses currently on the allowlist", + "type": "object", + "required": [ + "allowlist" + ], + "properties": { + "allowlist": { + "type": "array", + "items": { + "$ref": "#/definitions/StatusInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "StatusInfo": { + "description": "Account info for list queries related to allowlist and denylist", + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "before_send_hook_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "BeforeSendHookInfo", + "description": "Whether or not features that require MsgBeforeSendHook are enabled Many Token Factory chains do not yet support MsgBeforeSendHook", + "type": "object", + "required": [ + "advanced_features_enabled" + ], + "properties": { + "advanced_features_enabled": { + "description": "Whether or not features in this contract that require MsgBeforeSendHook are enabled.", + "type": "boolean" + }, + "hook_contract_address": { + "description": "The address of the contract that implements the BeforeSendHook interface. Most often this will be the cw_tokenfactory_issuer contract itself.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "burn_allowance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowanceResponse", + "description": "Returns a mint or burn allowance for a particular address, representing the amount of tokens the account is allowed to mint or burn", + "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", + "description": "Returns a list of all mint or burn allowances", + "type": "object", + "required": [ + "allowances" + ], + "properties": { + "allowances": { + "type": "array", + "items": { + "$ref": "#/definitions/AllowanceInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "AllowanceInfo": { + "description": "Information about a particular account and its mint / burn allowances. Used in list queries.", + "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", + "description": "Returns the full denomination for the Token Factory token. For example: `factory/{contract address}/{subdenom}`", + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + }, + "denylist": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "DenylistResponse", + "description": "Returns a list of addresses currently on the denylist.", + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "type": "array", + "items": { + "$ref": "#/definitions/StatusInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "StatusInfo": { + "description": "Account info for list queries related to allowlist and denylist", + "type": "object", + "required": [ + "address", + "status" + ], + "properties": { + "address": { + "type": "string" + }, + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "is_allowed": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StatusResponse", + "description": "Whether a particular account is allowed or denied", + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "is_denied": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StatusResponse", + "description": "Whether a particular account is allowed or denied", + "type": "object", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "is_frozen": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "IsFrozenResponse", + "description": "Returns whether or not the Token Factory token is frozen and transfers are disabled.", + "type": "object", + "required": [ + "is_frozen" + ], + "properties": { + "is_frozen": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "mint_allowance": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllowanceResponse", + "description": "Returns a mint or burn allowance for a particular address, representing the amount of tokens the account is allowed to mint or burn", + "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", + "description": "Returns a list of all mint or burn allowances", + "type": "object", + "required": [ + "allowances" + ], + "properties": { + "allowances": { + "type": "array", + "items": { + "$ref": "#/definitions/AllowanceInfo" + } + } + }, + "additionalProperties": false, + "definitions": { + "AllowanceInfo": { + "description": "Information about a particular account and its mint / burn allowances. Used in list queries.", + "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" + } + } + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_Addr", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "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" + }, + "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" + } + ] + }, + "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" + } + } + } + } +} 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..1879a52a6 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/contract.rs @@ -0,0 +1,194 @@ +use std::convert::TryInto; + +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, +}; +use cosmwasm_std::{CosmosMsg, Reply}; +use cw2::{get_contract_version, set_contract_version, ContractVersion}; +use osmosis_std::types::osmosis::tokenfactory::v1beta1::{MsgCreateDenom, MsgCreateDenomResponse}; +use token_bindings::TokenFactoryMsg; + +use crate::error::ContractError; +use crate::execute; +use crate::hooks; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SudoMsg}; +use crate::queries; +use crate::state::{BeforeSendHookInfo, BEFORE_SEND_HOOK_INFO, DENOM, IS_FROZEN}; + +// Version info for migration +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const CREATE_DENOM_REPLY_ID: u64 = 1; + +#[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 is the sender of the initial InstantiateMsg + cw_ownable::initialize_owner(deps.storage, deps.api, Some(info.sender.as_str()))?; + + // BeforeSendHook features are disabled by default. + BEFORE_SEND_HOOK_INFO.save( + deps.storage, + &BeforeSendHookInfo { + advanced_features_enabled: false, + hook_contract_address: None, + }, + )?; + 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, denom info is saved in the 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)?; + + 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::Deny { address, status } => execute::deny(deps, env, info, address, status), + ExecuteMsg::Allow { address, status } => execute::allow(deps, info, address, status), + ExecuteMsg::Freeze { status } => execute::freeze(deps, env, info, status), + ExecuteMsg::ForceTransfer { + amount, + from_address, + to_address, + } => execute::force_transfer(deps, env, info, amount, from_address, to_address), + + // Admin functions + ExecuteMsg::UpdateTokenFactoryAdmin { new_admin } => { + execute::update_tokenfactory_admin(deps, info, new_admin) + } + ExecuteMsg::UpdateOwnership(action) => { + execute::update_contract_owner(deps, env, info, action) + } + ExecuteMsg::SetMinterAllowance { address, allowance } => { + execute::set_minter(deps, info, address, allowance) + } + ExecuteMsg::SetBurnerAllowance { address, allowance } => { + execute::set_burner(deps, info, address, allowance) + } + ExecuteMsg::SetBeforeSendHook { cosmwasm_address } => { + execute::set_before_send_hook(deps, env, info, cosmwasm_address) + } + 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::Allowlist { start_after, limit } => { + to_json_binary(&queries::query_allowlist(deps, start_after, limit)?) + } + QueryMsg::BeforeSendHookInfo {} => { + to_json_binary(&queries::query_before_send_hook_features(deps)?) + } + QueryMsg::BurnAllowance { address } => { + to_json_binary(&queries::query_burn_allowance(deps, address)?) + } + QueryMsg::BurnAllowances { start_after, limit } => { + to_json_binary(&queries::query_burn_allowances(deps, start_after, limit)?) + } + QueryMsg::Denom {} => to_json_binary(&queries::query_denom(deps)?), + QueryMsg::Denylist { start_after, limit } => { + to_json_binary(&queries::query_denylist(deps, start_after, limit)?) + } + QueryMsg::IsAllowed { address } => { + to_json_binary(&queries::query_is_allowed(deps, address)?) + } + QueryMsg::IsDenied { address } => to_json_binary(&queries::query_is_denied(deps, address)?), + QueryMsg::IsFrozen {} => to_json_binary(&queries::query_is_frozen(deps)?), + QueryMsg::Ownership {} => to_json_binary(&queries::query_owner(deps)?), + QueryMsg::MintAllowance { address } => { + to_json_binary(&queries::query_mint_allowance(deps, address)?) + } + QueryMsg::MintAllowances { start_after, limit } => { + to_json_binary(&queries::query_mint_allowances(deps, start_after, limit)?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let storage_version: ContractVersion = get_contract_version(deps.storage)?; + + // Only migrate if newer + if storage_version.version.as_str() < CONTRACT_VERSION { + // 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 { + CREATE_DENOM_REPLY_ID => { + let MsgCreateDenomResponse { new_token_denom } = msg.result.try_into()?; + DENOM.save(deps.storage, &new_token_denom)?; + + Ok(Response::new().add_attribute("denom", new_token_denom)) + } + _ => 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..69dc28154 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/error.rs @@ -0,0 +1,76 @@ +use cosmwasm_std::{StdError, Uint128}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error(transparent)] + Ownership(#[from] cw_ownable::OwnershipError), + + #[error("BeforeSendHook not set. Features requiring it are disabled.")] + BeforeSendHookFeaturesDisabled {}, + + #[error("The address '{address}' is denied transfer abilities")] + Denied { address: String }, + + #[error("Cannot denylist the issuer contract itself")] + CannotDenylistSelf {}, + + #[error("The contract is frozen for denom {denom:?}. Addresses need to be added to the allowlist to enable transfers to or from an account.")] + 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..09a84f248 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/execute.rs @@ -0,0 +1,470 @@ +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; + +use crate::error::ContractError; +use crate::helpers::{check_before_send_hook_features_enabled, check_is_not_frozen}; +use crate::state::{ + BeforeSendHookInfo, ALLOWLIST, BEFORE_SEND_HOOK_INFO, BURNER_ALLOWANCES, DENOM, DENYLIST, + IS_FROZEN, MINTER_ALLOWANCES, +}; + +/// Mints new tokens. To mint new tokens, the address calling this method must +/// have an allowance of tokens to mint. This allowance is set by the contract through +/// the `ExecuteMsg::SetMinter { .. }` message. +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)?; + + // Check token is not frozen, or if from or to address is on allowlist + check_is_not_frozen(deps.as_ref(), info.sender.as_str(), &to_address, &denom)?; + + // 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), + }; + + 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)) +} + +/// Burns tokens. To burn tokens, the address calling this method must +/// have an allowance of tokens to burn and the tokens to burn must belong +/// to the `cw_tokenfactory_issuer` contract itself. The allowance is set by +/// the contract through the `ExecuteMsg::SetBurner { .. }` message, and funds +/// to be burnt must be sent to this contract prior to burning. +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(); + + Ok(Response::new() + .add_message(burn_tokens_msg) + .add_attribute("action", "burn") + .add_attribute("burner", info.sender) + .add_attribute("burn_from_address", burn_from_address.to_string()) + .add_attribute("amount", amount)) +} + +/// Updates the contract owner, must be the current contract owner to call +/// this method. +pub fn update_contract_owner( + deps: DepsMut, + env: Env, + info: MessageInfo, + action: cw_ownable::Action, +) -> Result, ContractError> { + // cw-ownable performs all validation and ownership checks for us + let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; + Ok(Response::default().add_attributes(ownership.into_attributes())) +} + +/// Updates the Token Factory token admin. To set no admin, specify the `new_admin` +/// argument to be either a null address or the address of the Cosmos SDK bank module +/// for the chain. +/// +/// Must be the contract owner to call this method. +pub fn update_tokenfactory_admin( + deps: DepsMut, + info: MessageInfo, + new_admin: String, +) -> Result, ContractError> { + // Only allow current contract owner to change tokenfactory admin + cw_ownable::assert_owner(deps.storage, &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 update_admin_msg = TokenFactoryMsg::ChangeAdmin { + denom: DENOM.load(deps.storage)?, + new_admin_address: new_admin_addr.into(), + }; + + Ok(Response::new() + .add_message(update_admin_msg) + .add_attribute("action", "update_tokenfactory_admin") + .add_attribute("new_admin", new_admin)) +} + +/// Sets metadata related to the Token Factory denom. +/// +/// Must be the contract owner to call this method. +pub fn set_denom_metadata( + deps: DepsMut, + env: Env, + info: MessageInfo, + metadata: Metadata, +) -> Result, ContractError> { + // Only allow current contract owner to set denom metadata + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + Ok(Response::new() + .add_attribute("action", "set_denom_metadata") + .add_message(MsgSetDenomMetadata { + sender: env.contract.address.to_string(), + metadata: Some(metadata), + })) +} + +/// Calls `MsgSetBeforeSendHook` and enables BeforeSendHook related features. +/// Takes a `cosmwasm_address` argument which is the address of the contract enforcing +/// the hook. Normally this will be the cw_tokenfactory_issuer contract address, but could +/// be a 3rd party address for more advanced use cases. +/// +/// As not all chains support the `BeforeSendHook` in the bank module, this +/// is intended to be called should chains add this feature at a later date. +/// +/// Must be the contract owner to call this method. +pub fn set_before_send_hook( + deps: DepsMut, + env: Env, + info: MessageInfo, + cosmwasm_address: String, +) -> Result, ContractError> { + // Only allow current contract owner + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + // The `cosmwasm_address` can be an empty string if setting the value to nil to + // disable the hook. If an empty string, we disable before send hook features. + // Otherwise, we validate the `cosmwasm_address` enable before send hook features. + if cosmwasm_address.is_empty() { + // Disable BeforeSendHook features + BEFORE_SEND_HOOK_INFO.save( + deps.storage, + &BeforeSendHookInfo { + advanced_features_enabled: false, + hook_contract_address: None, + }, + )?; + } else { + // Validate that address is a valid address + deps.api.addr_validate(&cosmwasm_address)?; + + // If the `cosmwasm_address` is not the same as the cw_tokenfactory_issuer contract + // BeforeSendHook features are disabled. + let mut advanced_features_enabled = true; + if cosmwasm_address != env.contract.address { + advanced_features_enabled = false; + } + + // Save the BeforeSendHookInfo + BEFORE_SEND_HOOK_INFO.save( + deps.storage, + &BeforeSendHookInfo { + advanced_features_enabled, + hook_contract_address: Some(cosmwasm_address.clone()), + }, + )?; + } + + // 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 denylisting / freezing possible. + let msg_set_beforesend_hook: CosmosMsg = MsgSetBeforeSendHook { + sender: env.contract.address.to_string(), + denom, + cosmwasm_address, + } + .into(); + + Ok(Response::new() + .add_attribute("action", "set_before_send_hook") + .add_message(msg_set_beforesend_hook)) +} + +/// Specifies and sets a burn allowance to allow for the burning of tokens. +/// To remove previously granted burn allowances, set this to zero. +/// +/// Must be the contract owner to call this method. +pub fn set_burner( + deps: DepsMut, + info: MessageInfo, + address: String, + allowance: Uint128, +) -> Result, ContractError> { + // Only allow current contract owner to set burner allowance + cw_ownable::assert_owner(deps.storage, &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)?; + } + + Ok(Response::new() + .add_attribute("action", "set_burner") + .add_attribute("burner", address) + .add_attribute("allowance", allowance)) +} + +/// Specifies and sets a burn allowance to allow for the minting of tokens. +/// To remove previously granted mint allowances, set this to zero. +/// +/// Must be the contract owner to call this method. +pub fn set_minter( + deps: DepsMut, + info: MessageInfo, + address: String, + allowance: Uint128, +) -> Result, ContractError> { + // Only allow current contract owner to set minter allowance + cw_ownable::assert_owner(deps.storage, &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)?; + } + + Ok(Response::new() + .add_attribute("action", "set_minter") + .add_attribute("minter", address) + .add_attribute("allowance", allowance)) +} + +/// Freezes / unfreezes token transfers, meaning that address will not be +/// able to send tokens until the token is unfrozen. This feature is dependent +/// on the BeforeSendHook. +/// +/// This feature works in conjunction with this contract's allowlist. For example, +/// a DAO may wish to prevent its token from being liquid during its bootstrapping +/// phase. It may wish to add its staking contract to the allowlist to allow users +/// to stake their tokens (thus users would be able to transfer to the staking +/// contract), or add an airdrop contract to the allowlist so users can claim +/// their tokens (but not yet trade them). +/// +/// This issuer contract itself is added to the allowlist when freezing, to allow +/// for minting of tokens (if minters with allowances are also on the allowlist). +/// +/// Must be the contract owner to call this method. +pub fn freeze( + deps: DepsMut, + env: Env, + info: MessageInfo, + status: bool, +) -> Result, ContractError> { + check_before_send_hook_features_enabled(deps.as_ref())?; + + // Only allow current contract owner to call this method + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + // Update config frozen status + // NOTE: Does not check if new status is same as old status + IS_FROZEN.save(deps.storage, &status)?; + + // Add the issue contract itself to the Allowlist, or remove + // if unfreezing to save storage. + if status { + ALLOWLIST.save(deps.storage, &env.contract.address, &status)?; + } else { + ALLOWLIST.remove(deps.storage, &env.contract.address); + } + + Ok(Response::new() + .add_attribute("action", "freeze") + .add_attribute("status", status.to_string())) +} + +/// Adds or removes an address from the denylist, meaning they will not +/// be able to transfer their tokens. This feature is dependent on +/// the BeforeSendHook. +/// +/// Must be the contract owner to call this method. +pub fn deny( + deps: DepsMut, + env: Env, + info: MessageInfo, + address: String, + status: bool, +) -> Result, ContractError> { + check_before_send_hook_features_enabled(deps.as_ref())?; + + // Only allow current contract owner to call this method + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let address = deps.api.addr_validate(&address)?; + + // Check this issuer contract is not denylisting itself + if address == env.contract.address { + return Err(ContractError::CannotDenylistSelf {}); + } + + // Update denylist status and validate that denylistee 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 { + DENYLIST.save(deps.storage, &address, &status)?; + } else { + DENYLIST.remove(deps.storage, &address); + } + + Ok(Response::new() + .add_attribute("action", "denylist") + .add_attribute("address", address) + .add_attribute("status", status.to_string())) +} + +/// Relevant only when the token is frozen. Addresses on the allowlist can +/// transfer tokens as well as have tokens sent to them. This feature is +/// dependent on the BeforeSendHook. +/// +/// See the `freeze` method for more information. +/// +/// Must be the contract owner to call this method. +pub fn allow( + 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 call this method + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + let address = deps.api.addr_validate(&address)?; + + // Update allowlist status and validate that allowlistee 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 { + ALLOWLIST.save(deps.storage, &address, &status)?; + } else { + ALLOWLIST.remove(deps.storage, &address); + } + + Ok(Response::new() + .add_attribute("action", "allowlist") + .add_attribute("address", address) + .add_attribute("status", status.to_string())) +} + +/// Force transfers tokens from one account to another. To disable this, +/// DAOs will need to renounce Token Factory admin by setting the token +/// admin to be a null address or the address of the bank module. +/// +/// Must be the contract owner to call this method. +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 + cw_ownable::assert_owner(deps.storage, &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).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..2328035a4 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/helpers.rs @@ -0,0 +1,63 @@ +use crate::state::{ALLOWLIST, BEFORE_SEND_HOOK_INFO, DENOM, DENYLIST, IS_FROZEN}; +use crate::ContractError; +use cosmwasm_std::Deps; + +/// Checks wether the BeforeSendHookFeatures gated features are enabled +pub fn check_before_send_hook_features_enabled(deps: Deps) -> Result<(), ContractError> { + let info = BEFORE_SEND_HOOK_INFO.load(deps.storage)?; + if !info.advanced_features_enabled { + Err(ContractError::BeforeSendHookFeaturesDisabled {}) + } else { + Ok(()) + } +} + +/// Checks wether the given address is on the denylist +pub fn check_is_not_denied(deps: Deps, address: String) -> Result<(), ContractError> { + let addr = deps.api.addr_validate(&address)?; + if let Some(is_denied) = DENYLIST.may_load(deps.storage, &addr)? { + if is_denied { + return Err(ContractError::Denied { address }); + } + }; + Ok(()) +} + +/// Checks wether the contract is frozen for the given denom, in which case +/// token transfers will not be allowed unless the to or from address is on +/// the allowlist +pub fn check_is_not_frozen( + deps: Deps, + from_address: &str, + to_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 from = deps.api.addr_validate(from_address)?; + let to = deps.api.addr_validate(to_address)?; + + // If either the from address or the to_address is allowed, then transaction proceeds + let is_from_allowed = ALLOWLIST.may_load(deps.storage, &from)?; + let is_to_allowed = ALLOWLIST.may_load(deps.storage, &to)?; + match (is_from_allowed, is_to_allowed) { + (Some(true), _) => return Ok(()), + (_, Some(true)) => 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..2b344f3e0 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/hooks.rs @@ -0,0 +1,25 @@ +use cosmwasm_std::{Coin, DepsMut, Response}; + +use crate::error::ContractError; +use crate::helpers::{check_is_not_denied, check_is_not_frozen}; + +/// The before send hook is called before every token transfer on chains that +/// support MsgSetBeforeSendHook. +/// +/// It is called by the bank module. +pub fn beforesend_hook( + deps: DepsMut, + from: String, + to: String, + coin: Coin, +) -> Result { + // Assert that denom of this contract is not frozen + // If it is frozen, check whether either 'from' or 'to' address is allowed + check_is_not_frozen(deps.as_ref(), &from, &to, &coin.denom)?; + + // Assert that neither 'from' or 'to' address is denylist + check_is_not_denied(deps.as_ref(), from)?; + check_is_not_denied(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..73e94be76 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/lib.rs @@ -0,0 +1,23 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +/// The smart contract itself, including the execute, instantiate, query, migrate +/// and reply entry points +pub mod contract; +/// Private error module, ContractError is re-exported in the public interface +mod error; +/// Contract methods that can be executed and alter state +pub mod execute; +/// Helper functions used for validation and checks +pub mod helpers; +/// Contract hooks +pub mod hooks; +/// Contract messages describing the API of the contract as well as responses +/// from contract queries +pub mod msg; +/// Contract queries +pub mod queries; +/// The contract state +pub mod state; + +/// Error messages used in this contract +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..3c874e584 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/msg.rs @@ -0,0 +1,253 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Coin, Uint128}; +pub use osmosis_std::types::cosmos::bank::v1beta1::{DenomUnit, Metadata}; + +use crate::state::BeforeSendHookInfo; + +/// The message used to create a new instance of this smart contract. +#[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 }, +} + +/// State changing methods available to this smart contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Allow adds the target address to the allowlist to be able to send or recieve tokens even if the token + /// is frozen. Token Factory's BeforeSendHook listener must be set to this contract in order for this feature + /// to work. + /// + /// This functionality is intedended for DAOs who do not wish to have a their tokens liquid while bootstrapping + /// their DAO. For example, a DAO may wish to white list a Token Staking contract (to allow users to stake their + /// tokens in the DAO) or a Merkle Drop contract (to allow users to claim their tokens). + Allow { address: String, status: bool }, + + /// Burn token to address. Burn allowance is required and wiil be deducted after successful burn. + Burn { + from_address: String, + amount: Uint128, + }, + + /// Mint token to address. Mint allowance is required and wiil be deducted after successful mint. + Mint { to_address: String, amount: Uint128 }, + + /// Deny adds the target address to the denylist, whis prevents them from sending/receiving the token attached + /// to this contract tokenfactory's BeforeSendHook listener must be set to this contract in order for this + /// feature to work as intended. + Deny { address: String, status: bool }, + + /// Block every token transfers of the token attached to this contract. + /// Token Factory's BeforeSendHook listener must be set to this contract in order for this + /// feature to work as intended. + Freeze { status: bool }, + + /// Force transfer token from one address to another. + ForceTransfer { + amount: Uint128, + from_address: String, + to_address: String, + }, + + /// Attempt to SetBeforeSendHook on the token attached to this contract. + /// This will fail if the chain does not support bank module hooks (many Token + /// Factory implementations do not yet support). + /// + /// This takes a cosmwasm_address as an argument, which is the address of the + /// contract that will be called before every token transfer. Normally, this + /// will be the issuer contract itself, though it can be a custom contract for + /// greater flexibility. + /// + /// Setting the address to an empty string will remove the SetBeforeSendHook. + /// + /// This method can only be called by the contract owner. + SetBeforeSendHook { cosmwasm_address: String }, + + /// Grant/revoke burn allowance. + SetBurnerAllowance { address: String, allowance: Uint128 }, + + /// 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 }, + + /// Updates the admin of the Token Factory token. + /// Normally this is the cw-tokenfactory-issuer contract itself. + /// This is intended to be used only if you seek to transfer ownership + /// of the Token somewhere else (i.e. to another management contract). + UpdateTokenFactoryAdmin { new_admin: String }, + + /// Updates the owner of this contract who is allowed to call privileged methods. + /// NOTE: this is separate from the Token Factory token admin, for this contract to work + /// at all, it needs to the be the Token Factory token admin. + /// + /// Normally, the contract owner will be a DAO. + /// + /// The `action` to be provided can be either to propose transferring ownership to an + /// account, accept a pending ownership transfer, or renounce the ownership permanently. + UpdateOwnership(cw_ownable::Action), +} + +/// Used for smart contract migration. +#[cw_serde] +pub struct MigrateMsg {} + +/// Queries supported by this smart contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns if token transfer is disabled. Response: IsFrozenResponse + #[returns(IsFrozenResponse)] + IsFrozen {}, + + /// Returns the token denom that this contract is the admin for. Response: DenomResponse + #[returns(DenomResponse)] + Denom {}, + + #[returns(::cw_ownable::Ownership<::cosmwasm_std::Addr>)] + Ownership {}, + + /// Returns the burn allowance of the specified address. Response: AllowanceResponse + #[returns(AllowanceResponse)] + BurnAllowance { address: String }, + + /// Enumerates over all burn allownances. Response: AllowancesResponse + #[returns(AllowancesResponse)] + BurnAllowances { + start_after: Option, + limit: Option, + }, + + /// Returns the mint allowance of the specified user. Response: AllowanceResponse + #[returns(AllowanceResponse)] + MintAllowance { address: String }, + + /// Enumerates over all mint allownances. Response: AllowancesResponse + #[returns(AllowancesResponse)] + MintAllowances { + start_after: Option, + limit: Option, + }, + + /// Returns wether the user is on denylist or not. Response: StatusResponse + #[returns(StatusResponse)] + IsDenied { address: String }, + + /// Enumerates over all addresses on the denylist. Response: DenylistResponse + #[returns(DenylistResponse)] + Denylist { + start_after: Option, + limit: Option, + }, + + /// Returns wether the user is on the allowlist or not. Response: StatusResponse + #[returns(StatusResponse)] + IsAllowed { address: String }, + + /// Enumerates over all addresses on the allowlist. Response: AllowlistResponse + #[returns(AllowlistResponse)] + Allowlist { + start_after: Option, + limit: Option, + }, + + /// Returns information about the BeforeSendHook for the token. Note: many Token + /// Factory chains do not yet support this feature. + /// + /// The information returned is: + /// - Whether features in this contract that require MsgBeforeSendHook are enabled. + /// - The address of the BeforeSendHook contract if configured. + /// + /// Response: BeforeSendHookInfo + #[returns(BeforeSendHookInfo)] + BeforeSendHookInfo {}, +} + +/// 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, + }, +} + +/// Returns whether or not the Token Factory token is frozen and transfers +/// are disabled. +#[cw_serde] +pub struct IsFrozenResponse { + pub is_frozen: bool, +} + +/// Returns the full denomination for the Token Factory token. For example: +/// `factory/{contract address}/{subdenom}` +#[cw_serde] +pub struct DenomResponse { + pub denom: String, +} + +/// Returns the current owner of this issuer contract who is allowed to +/// call priviledged methods. +#[cw_serde] +pub struct OwnerResponse { + pub address: String, +} + +/// Returns a mint or burn allowance for a particular address, representing +/// the amount of tokens the account is allowed to mint or burn +#[cw_serde] +pub struct AllowanceResponse { + pub allowance: Uint128, +} + +/// Information about a particular account and its mint / burn allowances. +/// Used in list queries. +#[cw_serde] +pub struct AllowanceInfo { + pub address: String, + pub allowance: Uint128, +} + +/// Returns a list of all mint or burn allowances +#[cw_serde] +pub struct AllowancesResponse { + pub allowances: Vec, +} + +/// Whether a particular account is allowed or denied +#[cw_serde] +pub struct StatusResponse { + pub status: bool, +} + +/// Account info for list queries related to allowlist and denylist +#[cw_serde] +pub struct StatusInfo { + pub address: String, + pub status: bool, +} + +/// Returns a list of addresses currently on the denylist. +#[cw_serde] +pub struct DenylistResponse { + pub denylist: Vec, +} + +/// Returns a list of addresses currently on the allowlist +#[cw_serde] +pub struct AllowlistResponse { + pub allowlist: 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..a6b7f3b5d --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/queries.rs @@ -0,0 +1,175 @@ +use cosmwasm_std::{Addr, Deps, Order, StdResult, Uint128}; +use cw_storage_plus::{Bound, Map}; + +use crate::msg::{ + AllowanceInfo, AllowanceResponse, AllowancesResponse, AllowlistResponse, DenomResponse, + DenylistResponse, IsFrozenResponse, StatusInfo, StatusResponse, +}; +use crate::state::{ + BeforeSendHookInfo, ALLOWLIST, BEFORE_SEND_HOOK_INFO, BURNER_ALLOWANCES, DENOM, DENYLIST, + IS_FROZEN, MINTER_ALLOWANCES, +}; + +// Default settings for pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +/// Returns the token denom that this contract is the admin for. Response: DenomResponse +pub fn query_denom(deps: Deps) -> StdResult { + let denom = DENOM.load(deps.storage)?; + Ok(DenomResponse { denom }) +} + +/// Returns if token transfer is disabled. Response: IsFrozenResponse +pub fn query_is_frozen(deps: Deps) -> StdResult { + let is_frozen = IS_FROZEN.load(deps.storage)?; + Ok(IsFrozenResponse { is_frozen }) +} + +/// Returns the owner of the contract. Response: Ownership +pub fn query_owner(deps: Deps) -> StdResult> { + cw_ownable::get_ownership(deps.storage) +} + +/// Returns the mint allowance of the specified user. Response: AllowanceResponse +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 }) +} + +/// Returns the allowance of the specified address. Response: AllowanceResponse +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 }) +} + +/// Helper function used in allowance list queries. +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() +} + +/// Enumerates over all allownances. Response: AllowancesResponse +pub fn query_mint_allowances( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(AllowancesResponse { + allowances: query_allowances(deps, start_after, limit, MINTER_ALLOWANCES)?, + }) +} + +/// Enumerates over all burn allownances. Response: AllowancesResponse +pub fn query_burn_allowances( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(AllowancesResponse { + allowances: query_allowances(deps, start_after, limit, BURNER_ALLOWANCES)?, + }) +} + +/// Returns wether the user is on denylist or not. Response: StatusResponse +pub fn query_is_denied(deps: Deps, address: String) -> StdResult { + let status = DENYLIST + .load(deps.storage, &deps.api.addr_validate(&address)?) + .unwrap_or(false); + Ok(StatusResponse { status }) +} + +/// Returns wether the user is on the allowlist or not. Response: StatusResponse +pub fn query_is_allowed(deps: Deps, address: String) -> StdResult { + let status = ALLOWLIST + .load(deps.storage, &deps.api.addr_validate(&address)?) + .unwrap_or(false); + Ok(StatusResponse { status }) +} + +/// Returns whether features that require MsgBeforeSendHook are enabled. +/// Most Cosmos chains do not support this feature yet. +pub fn query_before_send_hook_features(deps: Deps) -> StdResult { + BEFORE_SEND_HOOK_INFO.load(deps.storage) +} + +/// A helper function used in list queries +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() +} + +/// Enumerates over all addresses on the allowlist. Response: AllowlistResponse +pub fn query_allowlist( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(AllowlistResponse { + allowlist: query_status_map(deps, start_after, limit, ALLOWLIST)?, + }) +} + +/// Enumerates over all addresses on the denylist. Response: DenylistResponse +pub fn query_denylist( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + Ok(DenylistResponse { + denylist: query_status_map(deps, start_after, limit, DENYLIST)?, + }) +} 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..3c694c8c4 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/src/state.rs @@ -0,0 +1,33 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Uint128}; +use cw_storage_plus::{Item, Map}; + +/// Holds the Token Factory denom managed by this contract +pub const DENOM: Item = Item::new("denom"); + +/// Denylist addresses prevented from transferring tokens +pub const DENYLIST: Map<&Addr, bool> = Map::new("denylist"); + +/// Addresses allowed to transfer tokens even if the token is frozen +pub const ALLOWLIST: Map<&Addr, bool> = Map::new("allowlist"); + +/// Whether or not features that require MsgBeforeSendHook are enabled +/// Many Token Factory chains do not yet support MsgBeforeSendHook +#[cw_serde] +pub struct BeforeSendHookInfo { + /// Whether or not features in this contract that require MsgBeforeSendHook are enabled. + pub advanced_features_enabled: bool, + /// The address of the contract that implements the BeforeSendHook interface. + /// Most often this will be the cw_tokenfactory_issuer contract itself. + pub hook_contract_address: Option, +} +pub const BEFORE_SEND_HOOK_INFO: Item = Item::new("hook_features_enabled"); + +/// Whether or not token transfers are frozen +pub const IS_FROZEN: Item = Item::new("is_frozen"); + +/// Allowances for burning +pub const BURNER_ALLOWANCES: Map<&Addr, Uint128> = Map::new("burner_allowances"); + +/// Allowances for minting +pub const MINTER_ALLOWANCES: Map<&Addr, Uint128> = Map::new("minter_allowances"); diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/allowlist.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/allowlist.rs new file mode 100644 index 000000000..9d1da91d8 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/allowlist.rs @@ -0,0 +1,134 @@ +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 allowlist_by_owner_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let allowlistee = &env.test_accs[2]; + + // Owner sets before send hook to enable allowlist feature + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .allow(&allowlistee.address(), true, owner) + .unwrap(); + + // Should be allowlist after set true + assert!( + env.cw_tokenfactory_issuer + .query_is_allowed(&allowlistee.address()) + .unwrap() + .status + ); + + env.cw_tokenfactory_issuer + .allow(&allowlistee.address(), false, owner) + .unwrap(); + + // Should be unallowlist after set false + assert!( + !env.cw_tokenfactory_issuer + .query_is_allowed(&allowlistee.address()) + .unwrap() + .status + ); +} + +#[test] +fn allowlist_by_non_owern_should_fail() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + let allowlistee = &env.test_accs[2]; + + // Owner sets before send hook to enable allowlist feature + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Non-owner cannot add address to allowlist + let err = env + .cw_tokenfactory_issuer + .allow(&allowlistee.address(), true, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); +} + +#[test] +fn query_allowlist_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]; + + // Owner sets before send hook to enable allowlist feature + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Allowlist the address + env.cw_tokenfactory_issuer + .allow(&expected_result.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_allowlist(start_after, limit) + .unwrap() + .allowlist + } + }, + ); +} + +#[test] +fn query_allowlist_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]; + + // Owner sets before send hook to enable allowlist feature + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Allowlist the address + env.cw_tokenfactory_issuer + .allow(&expected_result.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_allowlist(start_after, limit) + .unwrap() + .allowlist + } + }, + ); +} 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..49320bed8 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/beforesend.rs @@ -0,0 +1,202 @@ +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; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Freeze + 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}\". Addresses need to be added to the allowlist to enable transfers to or from an account.: execute wasm contract failed") }); +} + +#[test] +fn allowlisted_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 allowlistee = &env.test_accs[1]; + let other = &env.test_accs[2]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Mint to owner and allowlistee + env.cw_tokenfactory_issuer + .set_minter(&owner.address(), 100000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&owner.address(), 10000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&allowlistee.address(), 10000, owner) + .unwrap(); + + // Freeze + env.cw_tokenfactory_issuer.freeze(true, owner).unwrap(); + + // Allowlist address + env.cw_tokenfactory_issuer + .allow(&allowlistee.address(), true, owner) + .unwrap(); + + // Bank send should pass + env.send_tokens(other.address(), coins(10000, denom.clone()), allowlistee) + .unwrap(); + + // Non allowlist 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}\". Addresses need to be added to the allowlist to enable transfers to or from an account.: execute wasm contract failed") }); + + // Other assets are not affected + env.send_tokens(other.address(), coins(10000, "uosmo"), owner) + .unwrap(); +} + +#[test] +fn non_allowlisted_accounts_can_transfer_to_allowlisted_address_frozen() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + let allowlistee = &env.test_accs[1]; + let other = &env.test_accs[2]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Mint to other + env.cw_tokenfactory_issuer + .set_minter(&owner.address(), 100000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&other.address(), 10000, owner) + .unwrap(); + + // Freeze + env.cw_tokenfactory_issuer.freeze(true, owner).unwrap(); + + // Allowlist address + env.cw_tokenfactory_issuer + .allow(&allowlistee.address(), true, owner) + .unwrap(); + + // Bank send to allow listed address should pass + env.send_tokens(allowlistee.address(), coins(10000, denom.clone()), other) + .unwrap(); +} + +#[test] +fn before_send_should_block_sending_from_denylist_address() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denylistee = &env.test_accs[1]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Mint to denylistee + env.cw_tokenfactory_issuer + .set_minter(&owner.address(), 20000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&denylistee.address(), 20000, owner) + .unwrap(); + + // Denylist + env.cw_tokenfactory_issuer + .deny(&denylistee.address(), true, owner) + .unwrap(); + + // Bank send should fail + let err = env + .send_tokens( + env.test_accs[2].address(), + coins(10000, denom.clone()), + denylistee, + ) + .unwrap_err(); + + let denylistee_addr = denylistee.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 '{denylistee_addr}' is denied transfer abilities: execute wasm contract failed") }); +} + +#[test] +fn before_send_should_block_sending_to_denylist_address() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denylistee = &env.test_accs[1]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Mint to self + env.cw_tokenfactory_issuer + .set_minter(&owner.address(), 10000, owner) + .unwrap(); + env.cw_tokenfactory_issuer + .mint(&owner.address(), 10000, owner) + .unwrap(); + + // Denylist + env.cw_tokenfactory_issuer + .deny(&denylistee.address(), true, owner) + .unwrap(); + + // Bank send should fail + let err = env + .send_tokens(denylistee.address(), coins(10000, denom.clone()), owner) + .unwrap_err(); + + let denylistee_addr = denylistee.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 '{denylistee_addr}' is denied transfer abilities: execute wasm contract failed") }); +} 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..6c63e1e38 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/burn.rs @@ -0,0 +1,355 @@ +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::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); +} + +#[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..f7cc5491f --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/contract_owner.rs @@ -0,0 +1,120 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_tokenfactory_issuer::{msg::ExecuteMsg, 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!( + Some(Addr::unchecked(prev_owner.address())), + env.cw_tokenfactory_issuer.query_owner().unwrap().owner, + ); + + env.cw_tokenfactory_issuer + .update_contract_owner(new_owner, prev_owner) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer.query_owner().unwrap().owner, + Some(Addr::unchecked(new_owner.address())), + ); + + // Previous owner should not be able to execute owner action + assert_eq!( + env.cw_tokenfactory_issuer + .update_contract_owner(prev_owner, prev_owner) + .unwrap_err(), + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); +} + +#[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 + .update_contract_owner(new_owner, new_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); +} + +#[test] +fn renounce_ownership() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + let hook = &env.test_accs[1]; + + assert_eq!( + Some(Addr::unchecked(owner.address())), + env.cw_tokenfactory_issuer.query_owner().unwrap().owner, + ); + + // Renounce ownership + env.cw_tokenfactory_issuer + .execute( + &ExecuteMsg::UpdateOwnership(cw_ownable::Action::RenounceOwnership), + &[], + owner, + ) + .unwrap(); + + assert_eq!( + env.cw_tokenfactory_issuer.query_owner().unwrap().owner, + None, + ); + + // Cannot perform actions that require ownership + assert_eq!( + env.cw_tokenfactory_issuer + .set_minter(&non_owner.address(), 10000, owner) + .unwrap_err(), + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NoOwner + )) + ); + assert_eq!( + env.cw_tokenfactory_issuer + .set_burner(&non_owner.address(), 10000, owner) + .unwrap_err(), + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NoOwner + )) + ); + assert_eq!( + env.cw_tokenfactory_issuer + .force_transfer( + non_owner, + Uint128::new(10000), + owner.address(), + non_owner.address(), + ) + .unwrap_err(), + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NoOwner + )) + ); + assert_eq!( + env.cw_tokenfactory_issuer + .set_before_send_hook(hook.address(), owner) + .unwrap_err(), + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NoOwner + )) + ); +} 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..cfcced250 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/denom_metadata.rs @@ -0,0 +1,114 @@ +use cw_tokenfactory_issuer::{msg::InstantiateMsg, ContractError}; + +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::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ) +} + +#[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(); +} diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/denylist.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/denylist.rs new file mode 100644 index 000000000..b15de2f22 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/denylist.rs @@ -0,0 +1,156 @@ +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 denylist_by_owner_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denylistee = &env.test_accs[2]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + env.cw_tokenfactory_issuer + .deny(&denylistee.address(), true, owner) + .unwrap(); + + // Should be denylist after set true + assert!( + env.cw_tokenfactory_issuer + .query_is_denied(&denylistee.address()) + .unwrap() + .status + ); + + env.cw_tokenfactory_issuer + .deny(&denylistee.address(), false, owner) + .unwrap(); + + // Should be undenylist after set false + assert!( + !env.cw_tokenfactory_issuer + .query_is_denied(&denylistee.address()) + .unwrap() + .status + ); +} + +#[test] +fn denylist_by_non_owner_should_fail() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + let denylistee = &env.test_accs[2]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Non-owner cannot add address to denylist + let err = env + .cw_tokenfactory_issuer + .deny(&denylistee.address(), true, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); +} + +#[test] +fn set_denylist_to_issuer_itself_fails() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Owner cannot deny issuer itself + let err = env + .cw_tokenfactory_issuer + .deny(&env.cw_tokenfactory_issuer.contract_addr, true, owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::CannotDenylistSelf {}) + ); +} + +#[test] +fn query_denylist_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]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Deny address + env.cw_tokenfactory_issuer + .deny(&expected_result.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_denylist(start_after, limit) + .unwrap() + .denylist + } + }, + ); +} + +#[test] +fn query_denylist_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]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Deny address + env.cw_tokenfactory_issuer + .deny(&expected_result.address, true, owner) + .unwrap(); + } + }, + |env| { + move |start_after, limit| { + env.cw_tokenfactory_issuer + .query_denylist(start_after, limit) + .unwrap() + .denylist + } + }, + ); +} 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..a02bafce9 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/force_transfer.rs @@ -0,0 +1,54 @@ +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::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); + + // 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..8a9963c73 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/freeze.rs @@ -0,0 +1,59 @@ +use cw_tokenfactory_issuer::ContractError; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn freeze_by_owener_should_pass() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + env.cw_tokenfactory_issuer.freeze(true, 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, owner).unwrap(); + + // Should be unfrozen after set false + assert!( + !env.cw_tokenfactory_issuer + .query_is_frozen() + .unwrap() + .is_frozen + ); +} + +#[test] +fn freeze_by_non_owner_should_fail() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let non_owner = &env.test_accs[1]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Non-owner cannot freeze + let err = env + .cw_tokenfactory_issuer + .freeze(true, non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); +} 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..32d176f1e --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/instantiate.rs @@ -0,0 +1,106 @@ +use cosmwasm_std::Addr; +use cw_tokenfactory_issuer::{ + msg::{InstantiateMsg, QueryMsg}, + state::BeforeSendHookInfo, +}; +use osmosis_test_tube::{Account, OsmosisTestApp}; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn instantiate_with_new_token_should_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//`" + ); + + // Contract is not frozen + let is_frozen = env + .cw_tokenfactory_issuer + .query_is_frozen() + .unwrap() + .is_frozen; + assert!(!is_frozen, "newly instantiated contract must not be frozen"); + + // Advanced features requiring BeforeSendHook are disabled + let info: BeforeSendHookInfo = env + .cw_tokenfactory_issuer + .query(&QueryMsg::BeforeSendHookInfo {}) + .unwrap(); + assert!(!info.advanced_features_enabled); + + let owner_addr = env.cw_tokenfactory_issuer.query_owner().unwrap().owner; + assert_eq!( + owner_addr, + Some(Addr::unchecked(owner.address())), + "owner must be contract instantiate tx signer" + ); +} + +#[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().owner; + assert_eq!( + owner_addr, + Some(Addr::unchecked(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..594a07f4c --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs @@ -0,0 +1,346 @@ +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_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::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); +} + +#[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 granular_minting_permissions_when_frozen() { + 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 minter_two = &env.test_accs[2]; + let mint_to = &env.test_accs[3]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Owner freezes contract + env.cw_tokenfactory_issuer.freeze(true, owner).unwrap(); + + // Owner grants minter a mint allowance + env.cw_tokenfactory_issuer + .set_minter(&minter.address(), 1000000, owner) + .unwrap(); + + // Owner grants minter_two a mint allowance + env.cw_tokenfactory_issuer + .set_minter(&minter_two.address(), 1000000, owner) + .unwrap(); + + // Minter can't mint when frozen + let err = env + .cw_tokenfactory_issuer + .mint(&mint_to.address(), 100, minter) + .unwrap_err(); + assert_eq!( + err, + RunnerError::ExecuteError { + msg: format!("failed to execute message; message index: 0: The contract is frozen for denom \"{}\". Addresses need to be added to the allowlist to enable transfers to or from an account.: execute wasm contract failed", denom) + } + ); + + // Own puts minter on allowlist + env.cw_tokenfactory_issuer + .allow(&minter.address(), true, owner) + .unwrap(); + + // Minter can mint + env.cw_tokenfactory_issuer + .mint(&mint_to.address(), 100, minter) + .unwrap(); + + // Minter-two can't mint because not allowed + let err = env + .cw_tokenfactory_issuer + .mint(&mint_to.address(), 100, minter_two) + .unwrap_err(); + assert_eq!( + err, + RunnerError::ExecuteError { + msg: format!("failed to execute message; message index: 0: The contract is frozen for denom \"{}\". Addresses need to be added to the allowlist to enable transfers to or from an account.: execute wasm contract failed", denom) + } + ); +} + +#[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..056859504 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/mod.rs @@ -0,0 +1,12 @@ +mod allowlist; +mod beforesend; +mod burn; +mod contract_owner; +mod denom_metadata; +mod denylist; +mod force_transfer; +mod freeze; +mod instantiate; +mod mint; +mod set_before_send_hook; +mod tokenfactory_admin; diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/set_before_send_hook.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/set_before_send_hook.rs new file mode 100644 index 000000000..74810986d --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/set_before_send_hook.rs @@ -0,0 +1,105 @@ +use cosmwasm_std::coins; +use cw_tokenfactory_issuer::msg::QueryMsg; +use cw_tokenfactory_issuer::{state::BeforeSendHookInfo, ContractError}; +use osmosis_test_tube::{Account, RunnerError}; + +use crate::test_env::{TestEnv, TokenfactoryIssuer}; + +#[test] +fn test_set_before_send_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(env.cw_tokenfactory_issuer.contract_addr.clone(), non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ); + + // Owner can set before update hook, but hook is already set + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Query before update hook + let info: BeforeSendHookInfo = env + .cw_tokenfactory_issuer + .query(&QueryMsg::BeforeSendHookInfo {}) + .unwrap(); + assert!(info.advanced_features_enabled); +} + +#[test] +fn test_set_before_send_hook_nil() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + // Owner can set before update hook to nil + env.cw_tokenfactory_issuer + .set_before_send_hook("".to_string(), owner) + .unwrap(); + + // Query before update hook, should now be disabled + let info: BeforeSendHookInfo = env + .cw_tokenfactory_issuer + .query(&QueryMsg::BeforeSendHookInfo {}) + .unwrap(); + assert!(!info.advanced_features_enabled); +} + +#[test] +fn test_set_before_send_hook_invalid_address_fails() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + + // Invalid address fails + let err = env + .cw_tokenfactory_issuer + .set_before_send_hook("invalid".to_string(), owner) + .unwrap_err(); + + assert_eq!( + err, + RunnerError::ExecuteError { msg: "failed to execute message; message index: 0: Generic error: addr_validate errored: decoding bech32 failed: invalid bech32 string length 7: execute wasm contract failed".to_string() } + ); +} + +#[test] +fn test_set_before_send_hook_to_a_different_contract() { + let env = TestEnv::default(); + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + let owner = &env.test_accs[0]; + let hook = &env.test_accs[1]; + + // Owner can set before update hook to nil + env.cw_tokenfactory_issuer + .set_before_send_hook(hook.address(), owner) + .unwrap(); + + // Query before update hook, should now be disabled + let info: BeforeSendHookInfo = env + .cw_tokenfactory_issuer + .query(&QueryMsg::BeforeSendHookInfo {}) + .unwrap(); + // Advanced features for this contract are not enabled + assert!(!info.advanced_features_enabled); + // But the hook contract address is set + assert_eq!(info.hook_contract_address.unwrap(), hook.address()); + + // Bank send should pass + env.send_tokens(hook.address(), coins(10000, "uosmo"), owner) + .unwrap(); + + // Bank send of TF denom should fail as the hook account isn't a contract + // and doesn't implement the required interface. + env.send_tokens(hook.address(), coins(10000, denom), owner) + .unwrap_err(); +} 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..7edb5efc6 --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/tokenfactory_admin.rs @@ -0,0 +1,37 @@ +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 + .update_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 + .update_tokenfactory_admin(&someone_else.address(), non_owner) + .unwrap_err(); + + assert_eq!( + err, + TokenfactoryIssuer::execute_error(ContractError::Ownership( + cw_ownable::OwnershipError::NotOwner + )) + ) +} 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..f6f4b3d1e --- /dev/null +++ b/contracts/external/cw-tokenfactory-issuer/tests/test_env.rs @@ -0,0 +1,594 @@ +// 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::{Addr, Coin, Uint128}; +use cw_tokenfactory_issuer::msg::{AllowlistResponse, DenylistResponse, Metadata, MigrateMsg}; +use cw_tokenfactory_issuer::{ + msg::{ + AllowanceResponse, AllowancesResponse, DenomResponse, ExecuteMsg, InstantiateMsg, + IsFrozenResponse, 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 update_contract_owner( + &self, + new_owner: &SigningAccount, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::UpdateOwnership(cw_ownable::Action::TransferOwnership { + new_owner: new_owner.address(), + expiry: None, + }), + &[], + signer, + )?; + self.execute( + &ExecuteMsg::UpdateOwnership(cw_ownable::Action::AcceptOwnership {}), + &[], + new_owner, + ) + } + pub fn update_tokenfactory_admin( + &self, + new_admin: &str, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::UpdateTokenFactoryAdmin { + 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_before_send_hook( + &self, + cosmwasm_address: String, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::SetBeforeSendHook { cosmwasm_address }, + &[], + 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 deny( + &self, + address: &str, + status: bool, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::Deny { + address: address.to_string(), + status, + }, + &[], + signer, + ) + } + + pub fn allow( + &self, + address: &str, + status: bool, + signer: &SigningAccount, + ) -> RunnerExecuteResult { + self.execute( + &ExecuteMsg::Allow { + 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_frozen(&self) -> Result { + self.query(&QueryMsg::IsFrozen {}) + } + + pub fn query_is_denied(&self, address: &str) -> Result { + self.query(&QueryMsg::IsDenied { + address: address.to_string(), + }) + } + + pub fn query_is_allowed(&self, address: &str) -> Result { + self.query(&QueryMsg::IsAllowed { + address: address.to_string(), + }) + } + + pub fn query_owner(&self) -> Result, RunnerError> { + self.query(&QueryMsg::Ownership {}) + } + + 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 query_allowlist( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::Allowlist { start_after, limit }) + } + + pub fn query_denylist( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query(&QueryMsg::Denylist { 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/external/cw-vesting/README.md b/contracts/external/cw-vesting/README.md index a8d918c10..acd62106e 100644 --- a/contracts/external/cw-vesting/README.md +++ b/contracts/external/cw-vesting/README.md @@ -1,5 +1,8 @@ # cw-vesting +[![cw-vesting on crates.io](https://img.shields.io/crates/v/cw-vesting.svg?logo=rust)](https://crates.io/crates/cw-vesting) +[![docs.rs](https://img.shields.io/docsrs/cw-vesting?logo=docsdotrs)](https://docs.rs/cw-vesting/latest/cw_vesting/) + This contract enables the creation of native && cw20 token streams, which allows a payment to be vested continuously over time. Key features include: diff --git a/contracts/external/cw-vesting/schema/cw-vesting.json b/contracts/external/cw-vesting/schema/cw-vesting.json index aa6b2aa8b..daefea9a5 100644 --- a/contracts/external/cw-vesting/schema/cw-vesting.json +++ b/contracts/external/cw-vesting/schema/cw-vesting.json @@ -1,6 +1,6 @@ { "contract_name": "cw-vesting", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-vesting/src/contract.rs b/contracts/external/cw-vesting/src/contract.rs index 70c0cea96..264a062e0 100644 --- a/contracts/external/cw-vesting/src/contract.rs +++ b/contracts/external/cw-vesting/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Binary, Coin, CosmosMsg, DelegationResponse, Deps, DepsMut, + from_json, to_json_binary, Binary, Coin, CosmosMsg, DelegationResponse, Deps, DepsMut, DistributionMsg, Env, MessageInfo, Response, StakingMsg, StakingQuery, StdResult, Timestamp, Uint128, }; @@ -137,7 +137,7 @@ pub fn execute_receive_cw20( // Only accepts cw20 tokens nonpayable(&info)?; - let msg: ReceiveMsg = from_binary(&receive_msg.msg)?; + let msg: ReceiveMsg = from_json(&receive_msg.msg)?; match msg { ReceiveMsg::Fund {} => { @@ -446,20 +446,20 @@ pub fn execute_register_slash( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), - QueryMsg::Info {} => to_binary(&PAYMENT.get_vest(deps.storage)?), - QueryMsg::Distributable { t } => to_binary(&PAYMENT.distributable( + QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), + QueryMsg::Info {} => to_json_binary(&PAYMENT.get_vest(deps.storage)?), + QueryMsg::Distributable { t } => to_json_binary(&PAYMENT.distributable( deps.storage, &PAYMENT.get_vest(deps.storage)?, t.unwrap_or(env.block.time), )?), QueryMsg::Stake(q) => PAYMENT.query_stake(deps.storage, q), - QueryMsg::Vested { t } => to_binary( + QueryMsg::Vested { t } => to_json_binary( &PAYMENT .get_vest(deps.storage)? .vested(t.unwrap_or(env.block.time)), ), - QueryMsg::TotalToVest {} => to_binary(&PAYMENT.get_vest(deps.storage)?.total()), - QueryMsg::VestDuration {} => to_binary(&PAYMENT.duration(deps.storage)?), + QueryMsg::TotalToVest {} => to_json_binary(&PAYMENT.get_vest(deps.storage)?.total()), + QueryMsg::VestDuration {} => to_json_binary(&PAYMENT.duration(deps.storage)?), } } diff --git a/contracts/external/cw-vesting/src/suite_tests/tests.rs b/contracts/external/cw-vesting/src/suite_tests/tests.rs index f93656c64..2738a3539 100644 --- a/contracts/external/cw-vesting/src/suite_tests/tests.rs +++ b/contracts/external/cw-vesting/src/suite_tests/tests.rs @@ -376,7 +376,12 @@ fn test_slash_while_cancelled_counts_against_owner() { assert_eq!(balance, distributable); let vest = suite.query_vest(); - let Status::Canceled { owner_withdrawable: pre_slash } = vest.status else { panic!("should be canceled") }; + let Status::Canceled { + owner_withdrawable: pre_slash, + } = vest.status + else { + panic!("should be canceled") + }; // register the slash. even though the time of the slash was // during the vest, the contract should deduct this from @@ -390,7 +395,9 @@ fn test_slash_while_cancelled_counts_against_owner() { .unwrap(); let vest = suite.query_vest(); - let Status::Canceled { owner_withdrawable } = vest.status else { panic!("should be canceled") }; + let Status::Canceled { owner_withdrawable } = vest.status else { + panic!("should be canceled") + }; assert_eq!(pre_slash - Uint128::new(10_000_000), owner_withdrawable); } diff --git a/contracts/external/cw-vesting/src/tests.rs b/contracts/external/cw-vesting/src/tests.rs index c9f78099c..9a55807b5 100644 --- a/contracts/external/cw-vesting/src/tests.rs +++ b/contracts/external/cw-vesting/src/tests.rs @@ -1,5 +1,5 @@ use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{coins, to_binary, Addr, Coin, Decimal, Empty, Uint128, Validator}; +use cosmwasm_std::{coins, to_json_binary, Addr, Coin, Decimal, Empty, Uint128, Validator}; use cw20::{Cw20Coin, Cw20ExecuteMsg, Cw20ReceiveMsg}; use cw_denom::{CheckedDenom, UncheckedDenom}; use cw_multi_test::{ @@ -170,7 +170,7 @@ fn setup_test_case(app: &mut App, msg: InstantiateMsg, funds: &[Coin]) -> TestCa let msg = Cw20ExecuteMsg::Send { contract: cw_vesting_addr.to_string(), amount: msg.total, - msg: to_binary(&ReceiveMsg::Fund {}).unwrap(), + msg: to_json_binary(&ReceiveMsg::Fund {}).unwrap(), }; app.execute_contract( Addr::unchecked(OWNER), @@ -344,7 +344,7 @@ fn test_staking_rewards_go_to_receiver() { StakingInfo { bonded_denom: NATIVE_DENOM.to_string(), unbonding_time: 60, - /// Interest rate per year (60 * 60 * 24 * 365 seconds) + // Interest rate per year (60 * 60 * 24 * 365 seconds) apr: Decimal::percent(10), }, ) @@ -509,7 +509,7 @@ fn test_catch_imposter_cw20() { let msg = Cw20ExecuteMsg::Send { contract: cw_vesting_addr.to_string(), amount: Uint128::new(TOTAL_VEST), - msg: to_binary(&ReceiveMsg::Fund {}).unwrap(), + msg: to_json_binary(&ReceiveMsg::Fund {}).unwrap(), }; // Errors that cw20 does not match what was expected @@ -595,7 +595,7 @@ fn test_execution_rejection_recv() { Cw20ReceiveMsg { sender: "random".to_string(), amount: Uint128::new(100), - msg: to_binary(&ReceiveMsg::Fund {}).unwrap(), + msg: to_json_binary(&ReceiveMsg::Fund {}).unwrap(), }, ) .unwrap_err(); @@ -608,7 +608,7 @@ fn test_execution_rejection_recv() { Cw20ReceiveMsg { sender: "random".to_string(), amount: Uint128::new(101), - msg: to_binary(&ReceiveMsg::Fund {}).unwrap(), + msg: to_json_binary(&ReceiveMsg::Fund {}).unwrap(), }, ) .unwrap_err(); diff --git a/contracts/external/cw721-roles/.cargo/config b/contracts/external/cw721-roles/.cargo/config new file mode 100644 index 000000000..7d1a066c8 --- /dev/null +++ b/contracts/external/cw721-roles/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/external/cw721-roles/Cargo.toml b/contracts/external/cw721-roles/Cargo.toml new file mode 100644 index 000000000..1a6935974 --- /dev/null +++ b/contracts/external/cw721-roles/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "cw721-roles" +authors = ["Jake Hartnell"] +description = "Non-transferable CW721 NFT contract that incorporates voting weights and on-chain roles." +version = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-controllers = { workspace = true } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +cw4 = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +dao-cw721-extensions = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +dao-testing = { workspace = true } +dao-voting-cw721-staked = { workspace = true } diff --git a/contracts/external/cw721-roles/README.md b/contracts/external/cw721-roles/README.md new file mode 100644 index 000000000..46c5f7079 --- /dev/null +++ b/contracts/external/cw721-roles/README.md @@ -0,0 +1,65 @@ +# cw721-roles + +This is a non-transferable NFT contract intended for use with DAOs. `cw721-roles` has an extension that allows for each NFT to have a `weight` associated with it, and also implements much of the functionality behind the [cw4-group contract](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw4-group) (credit to [Confio](https://confio.gmbh/) for their work on that). + +All methods of this contract are only callable via the configurable `minter` when the contract is created. It is primarily intended for use with DAOs. + +The `mint`, `burn`, `send`, and `transfer` methods have all been overriden from their default `cw721-base` versions, but work roughly the same with the caveat being they are only callable via the `minter`. All methods related to approvals are unsupported. + +## Extensions + +`cw721-roles` contains the following extensions: + +Token metadata has been extended with a weight and an optional human readable on-chain role which may be used in separate contracts for enforcing additional permissions. + +```rust +pub struct MetadataExt { + /// Optional on-chain role for this member, can be used by other contracts to enforce permissions + pub role: Option, + /// The voting weight of this role + pub weight: u64, +} +``` + +The contract has an additional execution extension that includes the ability to add and remove hooks for membership change events, as well as update a particular token's `token_uri`, `weight`, and `role`. All of these are only callable by the configured `minter`. + +```rust +pub enum ExecuteExt { + /// Add a new hook to be informed of all membership changes. + /// Must be called by Admin + AddHook { addr: String }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: String }, + /// Update the token_uri for a particular NFT. Must be called by minter / admin + UpdateTokenUri { + token_id: String, + token_uri: Option, + }, + /// Updates the voting weight of a token. Must be called by minter / admin + UpdateTokenWeight { token_id: String, weight: u64 }, + /// Udates the role of a token. Must be called by minter / admin + UpdateTokenRole { + token_id: String, + role: Option, + }, +} +``` + +The query extension implements queries that are compatible with the previously mentioned [cw4-group contract](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw4-group). + +```ignore +pub enum QueryExt { + /// Total weight at a given height + #[returns(cw4::TotalWeightResponse)] + TotalWeight { at_height: Option }, + /// Returns the weight of a certain member + #[returns(cw4::MemberResponse)] + Member { + addr: String, + at_height: Option, + }, + /// Shows all registered hooks. + #[returns(cw_controllers::HooksResponse)] + Hooks {}, +} +``` diff --git a/contracts/external/cw721-roles/examples/schema.rs b/contracts/external/cw721-roles/examples/schema.rs new file mode 100644 index 000000000..044f69e42 --- /dev/null +++ b/contracts/external/cw721-roles/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use cw721_roles::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/external/cw721-roles/schema/cw721-roles.json b/contracts/external/cw721-roles/schema/cw721-roles.json new file mode 100644 index 000000000..7b9591496 --- /dev/null +++ b/contracts/external/cw721-roles/schema/cw721-roles.json @@ -0,0 +1,2126 @@ +{ + "contract_name": "cw721-roles", + "contract_version": "2.3.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "minter", + "name", + "symbol" + ], + "properties": { + "minter": { + "description": "The minter is the only one who can create new NFTs. This is designed for a base NFT that is controlled by an external program or contract. You will likely replace this with custom logic in custom NFTs", + "type": "string" + }, + "name": { + "description": "Name of the NFT contract", + "type": "string" + }, + "symbol": { + "description": "Symbol of the NFT contract", + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.", + "oneOf": [ + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send_nft" + ], + "properties": { + "send_nft": { + "type": "object", + "required": [ + "contract", + "msg", + "token_id" + ], + "properties": { + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve" + ], + "properties": { + "approve": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve_all" + ], + "properties": { + "approve_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "operator": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Mint a new NFT, can only be called by the contract minter", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "extension", + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "allOf": [ + { + "$ref": "#/definitions/MetadataExt" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Burn an NFT the sender has access to", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Extension msg", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/ExecuteExt" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "ExecuteExt": { + "oneOf": [ + { + "description": "Add a new hook to be informed of all membership changes. Must be called by Admin", + "type": "object", + "required": [ + "add_hook" + ], + "properties": { + "add_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove a hook. Must be called by Admin", + "type": "object", + "required": [ + "remove_hook" + ], + "properties": { + "remove_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the token_uri for a particular NFT. Must be called by minter / admin", + "type": "object", + "required": [ + "update_token_uri" + ], + "properties": { + "update_token_uri": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + }, + "token_uri": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Updates the voting weight of a token. Must be called by minter / admin", + "type": "object", + "required": [ + "update_token_weight" + ], + "properties": { + "update_token_weight": { + "type": "object", + "required": [ + "token_id", + "weight" + ], + "properties": { + "token_id": { + "type": "string" + }, + "weight": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Udates the role of a token. Must be called by minter / admin", + "type": "object", + "required": [ + "update_token_role" + ], + "properties": { + "update_token_role": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "role": { + "type": [ + "string", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "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 + } + ] + }, + "MetadataExt": { + "type": "object", + "required": [ + "weight" + ], + "properties": { + "role": { + "description": "Optional on-chain role for this member, can be used by other contracts to enforce permissions", + "type": [ + "string", + "null" + ] + }, + "weight": { + "description": "The voting weight of this role", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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" + } + ] + }, + "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" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Return the owner of the given token, error if token does not exist", + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return operator that can access all of the owner's tokens.", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return approvals that a token has", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return approval of a given operator for all tokens of an owner, error if not set", + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "object", + "required": [ + "operator", + "owner" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "operator": { + "type": "string" + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "List all operators that can access all of the owner's tokens", + "type": "object", + "required": [ + "all_operators" + ], + "properties": { + "all_operators": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired items, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Total number of tokens issued", + "type": "object", + "required": [ + "num_tokens" + ], + "properties": { + "num_tokens": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns top-level metadata about the contract", + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients", + "type": "object", + "required": [ + "all_nft_info" + ], + "properties": { + "all_nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset.", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract.", + "type": "object", + "required": [ + "all_tokens" + ], + "properties": { + "all_tokens": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return the minter", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Extension query", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryExt" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "QueryExt": { + "oneOf": [ + { + "description": "Total weight at a given height", + "type": "object", + "required": [ + "total_weight" + ], + "properties": { + "total_weight": { + "type": "object", + "properties": { + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of Members", + "type": "object", + "required": [ + "list_members" + ], + "properties": { + "list_members": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the weight of a certain member", + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Shows all registered hooks.", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "migrate": null, + "sudo": null, + "responses": { + "all_nft_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllNftInfoResponse_for_QueryExt", + "type": "object", + "required": [ + "access", + "info" + ], + "properties": { + "access": { + "description": "Who can transfer the token", + "allOf": [ + { + "$ref": "#/definitions/OwnerOfResponse" + } + ] + }, + "info": { + "description": "Data on the token itself,", + "allOf": [ + { + "$ref": "#/definitions/NftInfoResponse_for_QueryExt" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "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 + } + ] + }, + "NftInfoResponse_for_QueryExt": { + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "allOf": [ + { + "$ref": "#/definitions/QueryExt" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "OwnerOfResponse": { + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "QueryExt": { + "oneOf": [ + { + "description": "Total weight at a given height", + "type": "object", + "required": [ + "total_weight" + ], + "properties": { + "total_weight": { + "type": "object", + "properties": { + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of Members", + "type": "object", + "required": [ + "list_members" + ], + "properties": { + "list_members": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the weight of a certain member", + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Shows all registered hooks.", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "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" + } + ] + }, + "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" + } + } + }, + "all_operators": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorsResponse", + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "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" + } + ] + }, + "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" + } + } + }, + "all_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_after` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "approval": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "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" + } + ] + }, + "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" + } + } + }, + "approvals": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalsResponse", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "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" + } + ] + }, + "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" + } + } + }, + "contract_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractInfoResponse", + "type": "object", + "required": [ + "name", + "symbol" + ], + "properties": { + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + }, + "additionalProperties": false + }, + "extension": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Null", + "type": "null" + }, + "minter": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MinterResponse", + "description": "Shows who can mint these tokens", + "type": "object", + "properties": { + "minter": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "nft_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NftInfoResponse_for_QueryExt", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "allOf": [ + { + "$ref": "#/definitions/QueryExt" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "QueryExt": { + "oneOf": [ + { + "description": "Total weight at a given height", + "type": "object", + "required": [ + "total_weight" + ], + "properties": { + "total_weight": { + "type": "object", + "properties": { + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of Members", + "type": "object", + "required": [ + "list_members" + ], + "properties": { + "list_members": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the weight of a certain member", + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Shows all registered hooks.", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "num_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NumTokensResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "operator": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "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" + } + ] + }, + "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" + } + } + }, + "owner_of": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerOfResponse", + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "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" + } + ] + }, + "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" + } + } + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "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" + } + ] + }, + "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" + } + } + }, + "tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_after` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/external/cw721-roles/src/contract.rs b/contracts/external/cw721-roles/src/contract.rs new file mode 100644 index 000000000..833007e46 --- /dev/null +++ b/contracts/external/cw721-roles/src/contract.rs @@ -0,0 +1,495 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + from_json, to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, + StdResult, SubMsg, Uint64, +}; +use cw4::{ + Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, + TotalWeightResponse, +}; +use cw721::{Cw721ReceiveMsg, NftInfoResponse, OwnerOfResponse}; +use cw721_base::{Cw721Contract, InstantiateMsg as Cw721BaseInstantiateMsg}; +use cw_storage_plus::Bound; +use cw_utils::maybe_addr; +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; +use std::cmp::Ordering; + +use crate::msg::{ExecuteMsg, QueryMsg}; +use crate::state::{MEMBERS, TOTAL}; +use crate::{error::RolesContractError as ContractError, state::HOOKS}; + +// Version info for migration +const CONTRACT_NAME: &str = "crates.io:cw721-roles"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Settings for query pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +pub type Cw721Roles<'a> = Cw721Contract<'a, MetadataExt, Empty, ExecuteExt, QueryExt>; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Cw721BaseInstantiateMsg, +) -> Result { + Cw721Roles::default().instantiate(deps.branch(), env.clone(), info, msg)?; + + // Initialize total weight to zero + TOTAL.save(deps.storage, &0, env.block.height)?; + + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default() + .add_attribute("contract_name", CONTRACT_NAME) + .add_attribute("contract_version", CONTRACT_VERSION)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + // Only owner / minter can execute + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + match msg { + ExecuteMsg::Mint { + token_id, + owner, + token_uri, + extension, + } => execute_mint(deps, env, info, token_id, owner, token_uri, extension), + ExecuteMsg::Burn { token_id } => execute_burn(deps, env, info, token_id), + ExecuteMsg::Extension { msg } => match msg { + ExecuteExt::AddHook { addr } => execute_add_hook(deps, info, addr), + ExecuteExt::RemoveHook { addr } => execute_remove_hook(deps, info, addr), + ExecuteExt::UpdateTokenRole { token_id, role } => { + execute_update_token_role(deps, env, info, token_id, role) + } + ExecuteExt::UpdateTokenUri { + token_id, + token_uri, + } => execute_update_token_uri(deps, env, info, token_id, token_uri), + ExecuteExt::UpdateTokenWeight { token_id, weight } => { + execute_update_token_weight(deps, env, info, token_id, weight) + } + }, + ExecuteMsg::TransferNft { + recipient, + token_id, + } => execute_transfer(deps, env, info, recipient, token_id), + ExecuteMsg::SendNft { + contract, + token_id, + msg, + } => execute_send(deps, env, info, token_id, contract, msg), + _ => Cw721Roles::default() + .execute(deps, env, info, msg) + .map_err(Into::into), + } +} + +pub fn execute_mint( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, + owner: String, + token_uri: Option, + extension: MetadataExt, +) -> Result { + let mut total = Uint64::from(TOTAL.load(deps.storage)?); + let mut diff = MemberDiff::new(owner.clone(), None, None); + + // Update member weights and total + MEMBERS.update( + deps.storage, + &deps.api.addr_validate(&owner)?, + env.block.height, + |old| -> StdResult<_> { + // Increment the total weight by the weight of the new token + total = total.checked_add(Uint64::from(extension.weight))?; + // Add the new NFT weight to the old weight for the owner + let new_weight = old.unwrap_or_default() + extension.weight; + // Set the diff for use in hooks + diff = MemberDiff::new(owner.clone(), old, Some(new_weight)); + Ok(new_weight) + }, + )?; + TOTAL.save(deps.storage, &total.u64(), env.block.height)?; + + let diffs = MemberChangedHookMsg { diffs: vec![diff] }; + + // Prepare hook messages + let msgs = HOOKS.prepare_hooks(deps.storage, |h| { + diffs.clone().into_cosmos_msg(h).map(SubMsg::new) + })?; + + // Call base mint + let res = Cw721Roles::default().execute( + deps, + env, + info, + ExecuteMsg::Mint { + token_id, + owner, + token_uri, + extension, + }, + )?; + + Ok(res.add_submessages(msgs)) +} + +pub fn execute_burn( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, +) -> Result { + // Lookup the owner of the NFT + let owner: OwnerOfResponse = from_json(Cw721Roles::default().query( + deps.as_ref(), + env.clone(), + QueryMsg::OwnerOf { + token_id: token_id.clone(), + include_expired: None, + }, + )?)?; + + // Get the weight of the token + let nft_info: NftInfoResponse = from_json(Cw721Roles::default().query( + deps.as_ref(), + env.clone(), + QueryMsg::NftInfo { + token_id: token_id.clone(), + }, + )?)?; + + let mut total = Uint64::from(TOTAL.load(deps.storage)?); + let mut diff = MemberDiff::new(owner.owner.clone(), None, None); + + // Update member weights and total + let owner_addr = deps.api.addr_validate(&owner.owner)?; + let old_weight = MEMBERS.load(deps.storage, &owner_addr)?; + + // Subtract the nft weight from the member's old weight + let new_weight = old_weight + .checked_sub(nft_info.extension.weight) + .ok_or(ContractError::CannotBurn {})?; + + // Subtract nft weight from the total + total = total.checked_sub(Uint64::from(nft_info.extension.weight))?; + + // Check if the new weight is now zero + if new_weight == 0 { + // New weight is now None + diff = MemberDiff::new(owner.owner, Some(old_weight), None); + // Remove owner from list of members + MEMBERS.remove(deps.storage, &owner_addr, env.block.height)?; + } else { + MEMBERS.update( + deps.storage, + &owner_addr, + env.block.height, + |old| -> StdResult<_> { + diff = MemberDiff::new(owner.owner.clone(), old, Some(new_weight)); + Ok(new_weight) + }, + )?; + } + + TOTAL.save(deps.storage, &total.u64(), env.block.height)?; + + let diffs = MemberChangedHookMsg { diffs: vec![diff] }; + + // Prepare hook messages + let msgs = HOOKS.prepare_hooks(deps.storage, |h| { + diffs.clone().into_cosmos_msg(h).map(SubMsg::new) + })?; + + // Remove the token + Cw721Roles::default() + .tokens + .remove(deps.storage, &token_id)?; + // Decrement the account + Cw721Roles::default().decrement_tokens(deps.storage)?; + + Ok(Response::new() + .add_attribute("action", "burn") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id) + .add_submessages(msgs)) +} + +pub fn execute_transfer( + deps: DepsMut, + _env: Env, + info: MessageInfo, + recipient: String, + token_id: String, +) -> Result { + let contract = Cw721Roles::default(); + + let mut token = contract.tokens.load(deps.storage, &token_id)?; + // set owner and remove existing approvals + token.owner = deps.api.addr_validate(&recipient)?; + token.approvals = vec![]; + contract.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::new() + .add_attribute("action", "transfer_nft") + .add_attribute("sender", info.sender) + .add_attribute("recipient", recipient) + .add_attribute("token_id", token_id)) +} + +pub fn execute_send( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token_id: String, + recipient_contract: String, + msg: Binary, +) -> Result { + let contract = Cw721Roles::default(); + + let mut token = contract.tokens.load(deps.storage, &token_id)?; + // set owner and remove existing approvals + token.owner = deps.api.addr_validate(&recipient_contract)?; + token.approvals = vec![]; + contract.tokens.save(deps.storage, &token_id, &token)?; + + let send = Cw721ReceiveMsg { + sender: info.sender.to_string(), + token_id: token_id.clone(), + msg, + }; + + Ok(Response::new() + .add_message(send.into_cosmos_msg(recipient_contract.clone())?) + .add_attribute("action", "send_nft") + .add_attribute("sender", info.sender) + .add_attribute("recipient", recipient_contract) + .add_attribute("token_id", token_id)) +} + +pub fn execute_add_hook( + deps: DepsMut, + _info: MessageInfo, + addr: String, +) -> Result { + let hook = deps.api.addr_validate(&addr)?; + HOOKS.add_hook(deps.storage, hook)?; + + Ok(Response::default() + .add_attribute("action", "add_hook") + .add_attribute("hook", addr)) +} + +pub fn execute_remove_hook( + deps: DepsMut, + _info: MessageInfo, + addr: String, +) -> Result { + let hook = deps.api.addr_validate(&addr)?; + HOOKS.remove_hook(deps.storage, hook)?; + + Ok(Response::default() + .add_attribute("action", "remove_hook") + .add_attribute("hook", addr)) +} + +pub fn execute_update_token_role( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token_id: String, + role: Option, +) -> Result { + let contract = Cw721Roles::default(); + + // Make sure NFT exists + let mut token = contract.tokens.load(deps.storage, &token_id)?; + + // Update role with new value + token.extension.role = role.clone(); + contract.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::default() + .add_attribute("action", "update_token_role") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id) + .add_attribute("role", role.unwrap_or_default())) +} + +pub fn execute_update_token_uri( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token_id: String, + token_uri: Option, +) -> Result { + let contract = Cw721Roles::default(); + + let mut token = contract.tokens.load(deps.storage, &token_id)?; + + // Set new token URI + token.token_uri = token_uri.clone(); + contract.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::new() + .add_attribute("action", "update_token_uri") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id) + .add_attribute("token_uri", token_uri.unwrap_or_default())) +} + +pub fn execute_update_token_weight( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, + weight: u64, +) -> Result { + let contract = Cw721Roles::default(); + + // Make sure NFT exists + let mut token = contract.tokens.load(deps.storage, &token_id)?; + + let mut total = Uint64::from(TOTAL.load(deps.storage)?); + let mut diff = MemberDiff::new(token.clone().owner, None, None); + + // Update member weights and total + MEMBERS.update( + deps.storage, + &token.owner, + env.block.height, + |old| -> Result<_, ContractError> { + let new_total_weight; + let old_total_weight = old.unwrap_or_default(); + + // Check if new token weight is great than, less than, or equal to + // the old token weight + match weight.cmp(&token.extension.weight) { + Ordering::Greater => { + // Subtract the old token weight from the new token weight + let weight_difference = weight + .checked_sub(token.extension.weight) + .ok_or(ContractError::NegativeValue {})?; + + // Increment the total weight by the weight difference of the new token + total = total.checked_add(Uint64::from(weight_difference))?; + // Add the new NFT weight to the old weight for the owner + new_total_weight = old_total_weight + weight_difference; + // Set the diff for use in hooks + diff = MemberDiff::new(token.clone().owner, old, Some(new_total_weight)); + } + Ordering::Less => { + // Subtract the new token weight from the old token weight + let weight_difference = token + .extension + .weight + .checked_sub(weight) + .ok_or(ContractError::NegativeValue {})?; + + // Subtract the weight difference from the old total weight + new_total_weight = old_total_weight + .checked_sub(weight_difference) + .ok_or(ContractError::NegativeValue {})?; + + // Subtract difference from the total + total = total.checked_sub(Uint64::from(weight_difference))?; + } + Ordering::Equal => return Err(ContractError::NoWeightChange {}), + } + + Ok(new_total_weight) + }, + )?; + TOTAL.save(deps.storage, &total.u64(), env.block.height)?; + + let diffs = MemberChangedHookMsg { diffs: vec![diff] }; + + // Prepare hook messages + let msgs = HOOKS.prepare_hooks(deps.storage, |h| { + diffs.clone().into_cosmos_msg(h).map(SubMsg::new) + })?; + + // Save token weight + token.extension.weight = weight; + contract.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::default() + .add_submessages(msgs) + .add_attribute("action", "update_token_weight") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id) + .add_attribute("weight", weight.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Extension { msg } => match msg { + QueryExt::Hooks {} => to_json_binary(&HOOKS.query_hooks(deps)?), + QueryExt::ListMembers { start_after, limit } => { + to_json_binary(&query_list_members(deps, start_after, limit)?) + } + QueryExt::Member { addr, at_height } => { + to_json_binary(&query_member(deps, addr, at_height)?) + } + QueryExt::TotalWeight { at_height } => { + to_json_binary(&query_total_weight(deps, at_height)?) + } + }, + _ => Cw721Roles::default().query(deps, env, msg), + } +} + +pub fn query_total_weight(deps: Deps, height: Option) -> StdResult { + let weight = match height { + Some(h) => TOTAL.may_load_at_height(deps.storage, h), + None => TOTAL.may_load(deps.storage), + }? + .unwrap_or_default(); + Ok(TotalWeightResponse { weight }) +} + +pub fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { + let addr = deps.api.addr_validate(&addr)?; + let weight = match height { + Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h), + None => MEMBERS.may_load(deps.storage, &addr), + }?; + Ok(MemberResponse { weight }) +} + +pub fn query_list_members( + 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 members = MEMBERS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + item.map(|(addr, weight)| Member { + addr: addr.into(), + weight, + }) + }) + .collect::>()?; + + Ok(MemberListResponse { members }) +} diff --git a/contracts/external/cw721-roles/src/error.rs b/contracts/external/cw721-roles/src/error.rs new file mode 100644 index 000000000..4d63e6efa --- /dev/null +++ b/contracts/external/cw721-roles/src/error.rs @@ -0,0 +1,29 @@ +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum RolesContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Base(#[from] cw721_base::ContractError), + + #[error(transparent)] + HookError(#[from] cw_controllers::HookError), + + #[error("{0}")] + OverflowErr(#[from] OverflowError), + + #[error(transparent)] + Ownable(#[from] cw_ownable::OwnershipError), + + #[error("Cannot burn NFT, member weight would be negative")] + CannotBurn {}, + + #[error("Would result in negative value")] + NegativeValue {}, + + #[error("The submitted weight is equal to the previous value, no change will occur")] + NoWeightChange {}, +} diff --git a/contracts/external/cw721-roles/src/lib.rs b/contracts/external/cw721-roles/src/lib.rs new file mode 100644 index 000000000..fa8b1eadb --- /dev/null +++ b/contracts/external/cw721-roles/src/lib.rs @@ -0,0 +1,16 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::RolesContractError as ContractError; + +// So consumers don't need dependencies to interact with this contract. +pub use cw721_base::MinterResponse; +pub use cw_ownable::{Action, Ownership}; +pub use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; diff --git a/contracts/external/cw721-roles/src/msg.rs b/contracts/external/cw721-roles/src/msg.rs new file mode 100644 index 000000000..fbfb15fd2 --- /dev/null +++ b/contracts/external/cw721-roles/src/msg.rs @@ -0,0 +1,5 @@ +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; + +pub type InstantiateMsg = cw721_base::InstantiateMsg; +pub type ExecuteMsg = cw721_base::ExecuteMsg; +pub type QueryMsg = cw721_base::QueryMsg; diff --git a/contracts/external/cw721-roles/src/state.rs b/contracts/external/cw721-roles/src/state.rs new file mode 100644 index 000000000..fa1a88570 --- /dev/null +++ b/contracts/external/cw721-roles/src/state.rs @@ -0,0 +1,22 @@ +use cosmwasm_std::Addr; +use cw_controllers::Hooks; +use cw_storage_plus::{SnapshotItem, SnapshotMap, Strategy}; + +// Hooks to contracts that will receive staking and unstaking messages. +pub const HOOKS: Hooks = Hooks::new("hooks"); + +/// A historic snapshot of total weight over time +pub const TOTAL: SnapshotItem = SnapshotItem::new( + "total", + "total__checkpoints", + "total__changelog", + Strategy::EveryBlock, +); + +/// A historic list of members and total voting weights +pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( + "members", + "members__checkpoints", + "members__changelog", + Strategy::EveryBlock, +); diff --git a/contracts/external/cw721-roles/src/tests.rs b/contracts/external/cw721-roles/src/tests.rs new file mode 100644 index 000000000..005c2e61f --- /dev/null +++ b/contracts/external/cw721-roles/src/tests.rs @@ -0,0 +1,587 @@ +use cosmwasm_std::{to_json_binary, Addr, Binary}; +use cw4::{HooksResponse, Member, MemberListResponse, MemberResponse, TotalWeightResponse}; +use cw721::{NftInfoResponse, OwnerOfResponse}; +use cw_multi_test::{App, Executor}; +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; +use dao_testing::contracts::{cw721_roles_contract, voting_cw721_staked_contract}; +use dao_voting_cw721_staked::msg::{InstantiateMsg as Cw721StakedInstantiateMsg, NftContract}; + +use crate::error::RolesContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +const ALICE: &str = "alice"; +const BOB: &str = "bob"; +const DAO: &str = "dao"; + +pub fn setup() -> (App, Addr) { + let mut app = App::default(); + + let cw721_id = app.store_code(cw721_roles_contract()); + let cw721_addr = app + .instantiate_contract( + cw721_id, + Addr::unchecked(DAO), + &InstantiateMsg { + name: "bad kids".to_string(), + symbol: "bad kids".to_string(), + minter: DAO.to_string(), + }, + &[], + "cw721_roles".to_string(), + None, + ) + .unwrap(); + + (app, cw721_addr) +} + +pub fn query_nft_owner( + app: &App, + nft: &Addr, + token_id: &str, +) -> Result { + let owner = app.wrap().query_wasm_smart( + nft, + &QueryMsg::OwnerOf { + token_id: token_id.to_string(), + include_expired: None, + }, + )?; + Ok(owner) +} + +pub fn query_member( + app: &App, + nft: &Addr, + member: &str, + at_height: Option, +) -> Result { + let member = app.wrap().query_wasm_smart( + nft, + &QueryMsg::Extension { + msg: QueryExt::Member { + addr: member.to_string(), + at_height, + }, + }, + )?; + Ok(member) +} + +pub fn query_total_weight( + app: &App, + nft: &Addr, + at_height: Option, +) -> Result { + let member = app.wrap().query_wasm_smart( + nft, + &QueryMsg::Extension { + msg: QueryExt::TotalWeight { at_height }, + }, + )?; + Ok(member) +} + +pub fn query_token_info( + app: &App, + nft: &Addr, + token_id: &str, +) -> Result, RolesContractError> { + let info = app.wrap().query_wasm_smart( + nft, + &QueryMsg::NftInfo { + token_id: token_id.to_string(), + }, + )?; + Ok(info) +} + +#[test] +fn test_minting_and_burning() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was created successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.extension.weight, 1); + + // Create another token for alice to give her even more total weight + let msg = ExecuteMsg::Mint { + token_id: "2".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Create a token for bob + let msg = ExecuteMsg::Mint { + token_id: "3".to_string(), + owner: BOB.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Query list of members + let members_list: MemberListResponse = app + .wrap() + .query_wasm_smart( + cw721_addr.clone(), + &QueryMsg::Extension { + msg: QueryExt::ListMembers { + start_after: None, + limit: None, + }, + }, + ) + .unwrap(); + assert_eq!( + members_list, + MemberListResponse { + members: vec![ + Member { + addr: ALICE.to_string(), + weight: 2 + }, + Member { + addr: BOB.to_string(), + weight: 1 + } + ] + } + ); + + // Member query returns total weight for alice + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(2)); + + // Total weight is now 3 + let total: TotalWeightResponse = query_total_weight(&app, &cw721_addr, None).unwrap(); + assert_eq!(total.weight, 3); + + // Burn a role for alice + let msg = ExecuteMsg::Burn { + token_id: "2".to_string(), + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token is now gone + let res = query_token_info(&app, &cw721_addr, "2"); + assert!(res.is_err()); + + // Alice's weight has been update acordingly + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(1)); +} + +#[test] +fn test_minting_and_transfer_permissions() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: Some("member".to_string()), + weight: 1, + }, + }; + + // Non-minter can't mint + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // DAO can mint successfully as the minter + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Non-minter can't transfer + let msg = ExecuteMsg::TransferNft { + recipient: BOB.to_string(), + token_id: "1".to_string(), + }; + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // DAO can transfer + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let owner: OwnerOfResponse = query_nft_owner(&app, &cw721_addr, "1").unwrap(); + assert_eq!(owner.owner, BOB); +} + +#[test] +fn test_send_permissions() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: Some("member".to_string()), + weight: 1, + }, + }; + // DAO can mint successfully as the minter + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Instantiate an NFT staking voting contract for testing SendNft + let dao_voting_cw721_staked_id = app.store_code(voting_cw721_staked_contract()); + let cw721_staked_addr = app + .instantiate_contract( + dao_voting_cw721_staked_id, + Addr::unchecked(DAO), + &Cw721StakedInstantiateMsg { + nft_contract: NftContract::Existing { + address: cw721_addr.to_string(), + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721-staking", + None, + ) + .unwrap(); + + // Non-minter can't send + let msg = ExecuteMsg::SendNft { + contract: cw721_staked_addr.to_string(), + token_id: "1".to_string(), + msg: to_json_binary(&Binary::default()).unwrap(), + }; + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // DAO can send + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Staking contract now owns the NFT + let owner: OwnerOfResponse = query_nft_owner(&app, &cw721_addr, "1").unwrap(); + assert_eq!(owner.owner, cw721_staked_addr.as_str()); +} + +#[test] +fn test_update_token_role() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenRole { + token_id: "1".to_string(), + role: Some("queen".to_string()), + }, + }; + + // Only admin / minter can update role + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Update token role + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was updated successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.extension.role, Some("queen".to_string())); +} + +#[test] +fn test_update_token_uri() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenUri { + token_id: "1".to_string(), + token_uri: Some("ipfs://abc...".to_string()), + }, + }; + + // Only admin / minter can update token_uri + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Update token_uri + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was updated successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.token_uri, Some("ipfs://abc...".to_string())); +} + +#[test] +fn test_update_token_weight() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenWeight { + token_id: "1".to_string(), + weight: 2, + }, + }; + + // Only admin / minter can update token weight + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Update token weight + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was updated successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.extension.weight, 2); + + // New value should be reflected in member's voting weight + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(2)); + + // Update weight to a smaller value + app.execute_contract( + Addr::unchecked(DAO), + cw721_addr.clone(), + &ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenWeight { + token_id: "1".to_string(), + weight: 1, + }, + }, + &[], + ) + .unwrap(); + + // New value should be reflected in member's voting weight + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(1)); + + // Create another token for alice to give her even more total weight + let msg = ExecuteMsg::Mint { + token_id: "2".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 10, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Alice's weight should be updated to include both tokens + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(11)); + + // Update Alice's second token to 0 weight + // Update weight to a smaller value + app.execute_contract( + Addr::unchecked(DAO), + cw721_addr.clone(), + &ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenWeight { + token_id: "2".to_string(), + weight: 0, + }, + }, + &[], + ) + .unwrap(); + + // Alice's voting value should be 1 + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(1)); +} + +#[test] +fn test_zero_weight_token() { + let (mut app, cw721_addr) = setup(); + + // Mint token with zero weight + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 0, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was created successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.extension.weight, 0); + + // Member query returns total weight for alice + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(0)); +} + +#[test] +fn test_hooks() { + let (mut app, cw721_addr) = setup(); + + // Mint initial NFT + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::AddHook { + addr: DAO.to_string(), + }, + }; + + // Hook can't be added by non-minter + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Hook can be added by the owner / minter + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Query hooks + let hooks: HooksResponse = app + .wrap() + .query_wasm_smart( + cw721_addr.clone(), + &QueryMsg::Extension { + msg: QueryExt::Hooks {}, + }, + ) + .unwrap(); + assert_eq!( + hooks, + HooksResponse { + hooks: vec![DAO.to_string()] + } + ); + + // Test hook fires when a new member is added + let msg = ExecuteMsg::Mint { + token_id: "2".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + // Should error as the DAO is not a contract, meaning hooks fired + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Should also error for burn, as this also fires hooks + let msg = ExecuteMsg::Burn { + token_id: "1".to_string(), + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::RemoveHook { + addr: DAO.to_string(), + }, + }; + + // Hook can't be removed by non-minter + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Hook can be removed by the owner / minter + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Minting should now work again as there are no hooks to dead + app.execute_contract( + Addr::unchecked(DAO), + cw721_addr, + &ExecuteMsg::Mint { + token_id: "2".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }, + &[], + ) + .unwrap(); +} diff --git a/contracts/external/dao-migrator/Cargo.toml b/contracts/external/dao-migrator/Cargo.toml index 346ec7f6f..4d2b48cb8 100644 --- a/contracts/external/dao-migrator/Cargo.toml +++ b/contracts/external/dao-migrator/Cargo.toml @@ -26,7 +26,7 @@ cw2 = { workspace = true } cw20 = { workspace = true } dao-interface = { workspace = true } -dao-dao-core = { workspace = true, features = ["library"] } +dao-dao-core = { workspace = true, features = ["library"] } dao-voting = { workspace = true } dao-proposal-single = { workspace = true, features = ["library"] } dao-voting-cw4 = { workspace = true, features = ["library"] } @@ -43,7 +43,7 @@ cw20-stake-v1 = { workspace = true, features = ["library"] } cw-core-interface-v1 = { package = "cw-core-interface", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" } cw4-voting-v1 = { package = "cw4-voting", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" } cw20-v1 = { version = "0.13", package = "cw20" } -cw4-v1 = { version = "0.13", package = "cw4" } +cw4-v1 = { version = "0.13", package = "cw4" } [dev-dependencies] cosmwasm-schema = { workspace = true } diff --git a/contracts/external/dao-migrator/README.md b/contracts/external/dao-migrator/README.md index 944dc7bc0..f61a3bb52 100644 --- a/contracts/external/dao-migrator/README.md +++ b/contracts/external/dao-migrator/README.md @@ -1,5 +1,8 @@ # dao-migrator +[![dao-migrator on crates.io](https://img.shields.io/crates/v/dao-migrator.svg?logo=rust)](https://crates.io/crates/dao-migrator) +[![docs.rs](https://img.shields.io/docsrs/dao-migrator?logo=docsdotrs)](https://docs.rs/dao-migrator/latest/dao_migrator/) + Here is the [discussion](https://github.com/DA0-DA0/dao-contracts/discussions/607). A migrator module for a DAO DAO DAO which handles migration for DAO modules diff --git a/contracts/external/dao-migrator/examples/schema.rs b/contracts/external/dao-migrator/examples/schema.rs index 9a8dfd2c1..8b7a1300e 100644 --- a/contracts/external/dao-migrator/examples/schema.rs +++ b/contracts/external/dao-migrator/examples/schema.rs @@ -1,11 +1,10 @@ use cosmwasm_schema::write_api; -use dao_proposal_single::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use dao_migrator::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { write_api! { instantiate: InstantiateMsg, query: QueryMsg, execute: ExecuteMsg, - migrate: MigrateMsg, } } diff --git a/contracts/external/dao-migrator/schema/dao-migrator.json b/contracts/external/dao-migrator/schema/dao-migrator.json index 81e796ddd..5a3030f4e 100644 --- a/contracts/external/dao-migrator/schema/dao-migrator.json +++ b/contracts/external/dao-migrator/schema/dao-migrator.json @@ -1,66 +1,32 @@ { "contract_name": "dao-migrator", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", "type": "object", "required": [ - "allow_revoting", - "close_proposal_on_execution_failure", - "max_voting_period", - "only_members_execute", - "pre_propose_info", - "threshold" + "migration_params", + "sub_daos", + "v1_code_ids", + "v2_code_ids" ], "properties": { - "allow_revoting": { - "description": "Allows changing votes before the proposal expires. If this is enabled proposals will not be able to complete early as final vote information is not known until the time of proposal expiration.", - "type": "boolean" + "migration_params": { + "$ref": "#/definitions/MigrationParams" }, - "close_proposal_on_execution_failure": { - "description": "If set to true proposals will be closed if their execution fails. Otherwise, proposals will remain open after execution failure. For example, with this enabled a proposal to send 5 tokens out of a DAO's treasury with 4 tokens would be closed when it is executed. With this disabled, that same proposal would remain open until the DAO's treasury was large enough for it to be executed.", - "type": "boolean" - }, - "max_voting_period": { - "description": "The default maximum amount of time a proposal may be voted on before expiring.", - "allOf": [ - { - "$ref": "#/definitions/Duration" - } - ] - }, - "min_voting_period": { - "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker aquires a large number of tokens and forces a proposal through.", - "anyOf": [ - { - "$ref": "#/definitions/Duration" - }, - { - "type": "null" - } - ] - }, - "only_members_execute": { - "description": "If set to true only members may execute passed proposals. Otherwise, any address may execute a passed proposal.", - "type": "boolean" + "sub_daos": { + "type": "array", + "items": { + "$ref": "#/definitions/SubDao" + } }, - "pre_propose_info": { - "description": "Information about what addresses may create proposals.", - "allOf": [ - { - "$ref": "#/definitions/PreProposeInfo" - } - ] + "v1_code_ids": { + "$ref": "#/definitions/V1CodeIds" }, - "threshold": { - "description": "The threshold a proposal must reach to complete.", - "allOf": [ - { - "$ref": "#/definitions/Threshold" - } - ] + "v2_code_ids": { + "$ref": "#/definitions/V2CodeIds" } }, "additionalProperties": false, @@ -110,9 +76,20 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, - "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" + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "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", @@ -148,11 +125,45 @@ } ] }, + "MigrationParams": { + "type": "object", + "required": [ + "proposal_params" + ], + "properties": { + "migrate_stake_cw20_manager": { + "description": "Rather or not to migrate the stake_cw20 contract and its manager. If this is not set to true and a stake_cw20 contract is detected in the DAO's configuration the migration will be aborted.", + "type": [ + "boolean", + "null" + ] + }, + "proposal_params": { + "description": "List of (address, ProposalParams) where `address` is an address of a proposal module currently part of the DAO.", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/ProposalParams" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "additionalProperties": false + }, "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -174,6 +185,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -189,38 +207,6 @@ }, "additionalProperties": false }, - "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", - "oneOf": [ - { - "description": "The majority of voters must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "majority" - ], - "properties": { - "majority": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "A percentage of voting power >= percent must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "additionalProperties": false - } - ] - }, "PreProposeInfo": { "oneOf": [ { @@ -261,403 +247,183 @@ } ] }, - "Threshold": { - "description": "The ways a proposal may reach its passing / failing threshold.", - "oneOf": [ - { - "description": "Declares a percentage of the total weight that must cast Yes votes in order for a proposal to pass. See `ThresholdResponse::AbsolutePercentage` in the cw3 spec for details.", - "type": "object", - "required": [ - "absolute_percentage" - ], - "properties": { - "absolute_percentage": { - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Declares a `quorum` of the total votes that must participate in the election in order for the vote to be considered at all. See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for details.", - "type": "object", - "required": [ - "threshold_quorum" - ], - "properties": { - "threshold_quorum": { - "type": "object", - "required": [ - "quorum", - "threshold" - ], - "properties": { - "quorum": { - "$ref": "#/definitions/PercentageThreshold" - }, - "threshold": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "threshold" - ], - "properties": { - "threshold": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "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": "Creates a proposal in the module.", + "ProposalParams": { + "description": "The params we need to provide for migration msgs", "type": "object", "required": [ - "propose" + "close_proposal_on_execution_failure", + "pre_propose_info" ], "properties": { - "propose": { - "$ref": "#/definitions/SingleChoiceProposeMsg" + "close_proposal_on_execution_failure": { + "type": "boolean" + }, + "pre_propose_info": { + "$ref": "#/definitions/PreProposeInfo" + }, + "veto": { + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false }, - { - "description": "Votes on a proposal. Voting power is determined by the DAO's voting power module.", + "SubDao": { "type": "object", "required": [ - "vote" + "addr" ], "properties": { - "vote": { - "type": "object", - "required": [ - "proposal_id", - "vote" - ], - "properties": { - "proposal_id": { - "description": "The ID of the proposal to vote on.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "rationale": { - "description": "An optional rationale for why this vote was cast. This can be updated, set, or removed later by the address casting the vote.", - "type": [ - "string", - "null" - ] - }, - "vote": { - "description": "The senders position on the proposal.", - "allOf": [ - { - "$ref": "#/definitions/Vote" - } - ] - } - }, - "additionalProperties": false + "addr": { + "description": "The contract address of the SubDAO", + "type": "string" + }, + "charter": { + "description": "The purpose/constitution for the SubDAO", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false }, - { - "description": "Updates the sender's rationale for their vote on the specified proposal. Errors if no vote vote has been cast.", + "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" + }, + "V1CodeIds": { "type": "object", "required": [ - "update_rationale" + "cw20_stake", + "cw20_staked_balances_voting", + "cw4_voting", + "proposal_single" ], "properties": { - "update_rationale": { - "type": "object", - "required": [ - "proposal_id" - ], - "properties": { - "proposal_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "rationale": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false + "cw20_stake": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "cw20_staked_balances_voting": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "cw4_voting": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "proposal_single": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false }, - { - "description": "Causes the messages associated with a passed proposal to be executed by the DAO.", + "V2CodeIds": { "type": "object", "required": [ - "execute" + "cw20_stake", + "cw20_staked_balances_voting", + "cw4_voting", + "proposal_single" ], "properties": { - "execute": { - "type": "object", - "required": [ - "proposal_id" - ], - "properties": { - "proposal_id": { - "description": "The ID of the proposal to execute.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false + "cw20_stake": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "cw20_staked_balances_voting": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "cw4_voting": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "proposal_single": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false }, - { - "description": "Closes a proposal that has failed (either not passed or timed out). If applicable this will cause the proposal deposit associated wth said proposal to be returned.", + "VetoConfig": { "type": "object", "required": [ - "close" + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" ], "properties": { - "close": { - "type": "object", - "required": [ - "proposal_id" - ], - "properties": { - "proposal_id": { - "description": "The ID of the proposal to close.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" } - }, - "additionalProperties": false + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" } }, "additionalProperties": false + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "type": "object", + "required": [ + "migration_params", + "sub_daos", + "v1_code_ids", + "v2_code_ids" + ], + "properties": { + "migration_params": { + "$ref": "#/definitions/MigrationParams" }, - { - "description": "Updates the governance module's config.", - "type": "object", - "required": [ - "update_config" - ], - "properties": { - "update_config": { - "type": "object", - "required": [ - "allow_revoting", - "close_proposal_on_execution_failure", - "dao", - "max_voting_period", - "only_members_execute", - "threshold" - ], - "properties": { - "allow_revoting": { - "description": "Allows changing votes before the proposal expires. If this is enabled proposals will not be able to complete early as final vote information is not known until the time of proposal expiration.", - "type": "boolean" - }, - "close_proposal_on_execution_failure": { - "description": "If set to true proposals will be closed if their execution fails. Otherwise, proposals will remain open after execution failure. For example, with this enabled a proposal to send 5 tokens out of a DAO's treasury with 4 tokens would be closed when it is executed. With this disabled, that same proposal would remain open until the DAO's treasury was large enough for it to be executed.", - "type": "boolean" - }, - "dao": { - "description": "The address if tge DAO that this governance module is associated with.", - "type": "string" - }, - "max_voting_period": { - "description": "The default maximum amount of time a proposal may be voted on before expiring. This will only apply to proposals created after the config update.", - "allOf": [ - { - "$ref": "#/definitions/Duration" - } - ] - }, - "min_voting_period": { - "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker aquires a large number of tokens and forces a proposal through.", - "anyOf": [ - { - "$ref": "#/definitions/Duration" - }, - { - "type": "null" - } - ] - }, - "only_members_execute": { - "description": "If set to true only members may execute passed proposals. Otherwise, any address may execute a passed proposal. Applies to all outstanding and future proposals.", - "type": "boolean" - }, - "threshold": { - "description": "The new proposal passing threshold. This will only apply to proposals created after the config update.", - "allOf": [ - { - "$ref": "#/definitions/Threshold" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Update's the proposal creation policy used for this module. Only the DAO may call this method.", - "type": "object", - "required": [ - "update_pre_propose_info" - ], - "properties": { - "update_pre_propose_info": { - "type": "object", - "required": [ - "info" - ], - "properties": { - "info": { - "$ref": "#/definitions/PreProposeInfo" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Adds an address as a consumer of proposal hooks. Consumers of proposal hooks have hook messages executed on them whenever the status of a proposal changes or a proposal is created. If a consumer contract errors when handling a hook message it will be removed from the list of consumers.", - "type": "object", - "required": [ - "add_proposal_hook" - ], - "properties": { - "add_proposal_hook": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Removes a consumer of proposal hooks.", - "type": "object", - "required": [ - "remove_proposal_hook" - ], - "properties": { - "remove_proposal_hook": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "sub_daos": { + "type": "array", + "items": { + "$ref": "#/definitions/SubDao" + } }, - { - "description": "Adds an address as a consumer of vote hooks. Consumers of vote hooks have hook messages executed on them whenever the a vote is cast. If a consumer contract errors when handling a hook message it will be removed from the list of consumers.", - "type": "object", - "required": [ - "add_vote_hook" - ], - "properties": { - "add_vote_hook": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "v1_code_ids": { + "$ref": "#/definitions/V1CodeIds" }, - { - "description": "Removed a consumer of vote hooks.", - "type": "object", - "required": [ - "remove_vote_hook" - ], - "properties": { - "remove_vote_hook": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false + "v2_code_ids": { + "$ref": "#/definitions/V2CodeIds" } - ], + }, + "additionalProperties": false, "definitions": { "Admin": { "description": "Information about the CosmWasm level admin of a contract. Used in conjunction with `ModuleInstantiateInfo` to instantiate modules.", @@ -700,63 +466,6 @@ } ] }, - "BankMsg": { - "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", - "oneOf": [ - { - "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "send" - ], - "properties": { - "send": { - "type": "object", - "required": [ - "amount", - "to_address" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "to_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - } - } - }, - "additionalProperties": false - } - ] - }, "Binary": { "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" @@ -776,5146 +485,318 @@ } } }, - "CosmosMsg_for_Empty": { + "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": [ - "bank" + "height" ], "properties": { - "bank": { - "$ref": "#/definitions/BankMsg" + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false }, { + "description": "Time in seconds", "type": "object", "required": [ - "custom" + "time" ], "properties": { - "custom": { - "$ref": "#/definitions/Empty" + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false + } + ] + }, + "MigrationParams": { + "type": "object", + "required": [ + "proposal_params" + ], + "properties": { + "migrate_stake_cw20_manager": { + "description": "Rather or not to migrate the stake_cw20 contract and its manager. If this is not set to true and a stake_cw20 contract is detected in the DAO's configuration the migration will be aborted.", + "type": [ + "boolean", + "null" + ] }, - { - "type": "object", - "required": [ - "staking" - ], - "properties": { - "staking": { - "$ref": "#/definitions/StakingMsg" + "proposal_params": { + "description": "List of (address, ProposalParams) where `address` is an address of a proposal module currently part of the DAO.", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/ProposalParams" + } + ], + "maxItems": 2, + "minItems": 2 + } + } + }, + "additionalProperties": false + }, + "ModuleInstantiateInfo": { + "description": "Information needed to instantiate a module.", + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg" + ], + "properties": { + "admin": { + "description": "CosmWasm level admin of the instantiated contract. See: ", + "anyOf": [ + { + "$ref": "#/definitions/Admin" + }, + { + "type": "null" } - }, - "additionalProperties": false + ] + }, + "code_id": { + "description": "Code ID of the contract to be instantiated.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "label": { + "description": "Label for the instantiated contract.", + "type": "string" }, + "msg": { + "description": "Instantiate message to be used to create the contract.", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + } + }, + "additionalProperties": false + }, + "PreProposeInfo": { + "oneOf": [ { + "description": "Anyone may create a proposal free of charge.", "type": "object", "required": [ - "distribution" + "anyone_may_propose" ], "properties": { - "distribution": { - "$ref": "#/definitions/DistributionMsg" + "anyone_may_propose": { + "type": "object", + "additionalProperties": false } }, "additionalProperties": false }, { - "description": "A Stargate message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md)", + "description": "The module specified in INFO has exclusive rights to proposal creation.", "type": "object", "required": [ - "stargate" + "module_may_propose" ], "properties": { - "stargate": { + "module_may_propose": { "type": "object", "required": [ - "type_url", - "value" + "info" ], "properties": { - "type_url": { - "type": "string" - }, - "value": { - "$ref": "#/definitions/Binary" + "info": { + "$ref": "#/definitions/ModuleInstantiateInfo" } - } + }, + "additionalProperties": false } }, "additionalProperties": false + } + ] + }, + "ProposalParams": { + "description": "The params we need to provide for migration msgs", + "type": "object", + "required": [ + "close_proposal_on_execution_failure", + "pre_propose_info" + ], + "properties": { + "close_proposal_on_execution_failure": { + "type": "boolean" }, - { - "type": "object", - "required": [ - "ibc" - ], - "properties": { - "ibc": { - "$ref": "#/definitions/IbcMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "wasm" - ], - "properties": { - "wasm": { - "$ref": "#/definitions/WasmMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "gov" - ], - "properties": { - "gov": { - "$ref": "#/definitions/GovMsg" - } - }, - "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" - }, - "DistributionMsg": { - "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "description": "The `withdraw_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "withdraw_delegator_reward" - ], - "properties": { - "withdraw_delegator_reward": { - "type": "object", - "required": [ - "validator" - ], - "properties": { - "validator": { - "description": "The `validator_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "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 - } - ] - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", - "oneOf": [ - { - "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", - "type": "object", - "required": [ - "vote" - ], - "properties": { - "vote": { - "type": "object", - "required": [ - "proposal_id", - "vote" - ], - "properties": { - "proposal_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] - } - } - } - }, - "additionalProperties": false - } - ] - }, - "IbcMsg": { - "description": "These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts (contracts that directly speak the IBC protocol via 6 entry points)", - "oneOf": [ - { - "description": "Sends bank tokens owned by the contract to the given address on another chain. The channel must already be established between the ibctransfer module on this chain and a matching module on the remote chain. We cannot select the port_id, this is whatever the local chain has bound the ibctransfer module to.", - "type": "object", - "required": [ - "transfer" - ], - "properties": { - "transfer": { - "type": "object", - "required": [ - "amount", - "channel_id", - "timeout", - "to_address" - ], - "properties": { - "amount": { - "description": "packet data only supports one coin https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "channel_id": { - "description": "exisiting channel to send the tokens over", - "type": "string" - }, - "timeout": { - "description": "when packet times out, measured on remote chain", - "allOf": [ - { - "$ref": "#/definitions/IbcTimeout" - } - ] - }, - "to_address": { - "description": "address on the remote chain to receive these tokens", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Sends an IBC packet with given data over the existing channel. Data should be encoded in a format defined by the channel version, and the module on the other side should know how to parse this.", - "type": "object", - "required": [ - "send_packet" - ], - "properties": { - "send_packet": { - "type": "object", - "required": [ - "channel_id", - "data", - "timeout" - ], - "properties": { - "channel_id": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/Binary" - }, - "timeout": { - "description": "when packet times out, measured on remote chain", - "allOf": [ - { - "$ref": "#/definitions/IbcTimeout" - } - ] - } - } - } - }, - "additionalProperties": false + "pre_propose_info": { + "$ref": "#/definitions/PreProposeInfo" }, - { - "description": "This will close an existing channel that is owned by this contract. Port is auto-assigned to the contract's IBC port", - "type": "object", - "required": [ - "close_channel" - ], - "properties": { - "close_channel": { - "type": "object", - "required": [ - "channel_id" - ], - "properties": { - "channel_id": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "IbcTimeout": { - "description": "In IBC each package must set at least one type of timeout: the timestamp or the block height. Using this rather complex enum instead of two timeout fields we ensure that at least one timeout is set.", - "type": "object", - "properties": { - "block": { + "veto": { "anyOf": [ { - "$ref": "#/definitions/IbcTimeoutBlock" + "$ref": "#/definitions/VetoConfig" }, { "type": "null" } ] + } + }, + "additionalProperties": false + }, + "SubDao": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "description": "The contract address of the SubDAO", + "type": "string" }, - "timestamp": { - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } + "charter": { + "description": "The purpose/constitution for the SubDAO", + "type": [ + "string", + "null" ] } - } + }, + "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" }, - "IbcTimeoutBlock": { - "description": "IBCTimeoutHeight Height is a monotonically increasing data type that can be compared against another Height for the purposes of updating and freezing clients. Ordering is (revision_number, timeout_height)", + "V1CodeIds": { "type": "object", "required": [ - "height", - "revision" + "cw20_stake", + "cw20_staked_balances_voting", + "cw4_voting", + "proposal_single" ], "properties": { - "height": { - "description": "block height after which the packet times out. the height within the given revision", + "cw20_stake": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "cw20_staked_balances_voting": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "cw4_voting": { "type": "integer", "format": "uint64", "minimum": 0.0 }, - "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "proposal_single": { "type": "integer", "format": "uint64", "minimum": 0.0 } - } + }, + "additionalProperties": false }, - "ModuleInstantiateInfo": { - "description": "Information needed to instantiate a module.", + "V2CodeIds": { "type": "object", "required": [ - "code_id", - "label", - "msg" + "cw20_stake", + "cw20_staked_balances_voting", + "cw4_voting", + "proposal_single" ], "properties": { - "admin": { - "description": "CosmWasm level admin of the instantiated contract. See: ", - "anyOf": [ - { - "$ref": "#/definitions/Admin" - }, - { - "type": "null" - } - ] + "cw20_stake": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "code_id": { - "description": "Code ID of the contract to be instantiated.", + "cw20_staked_balances_voting": { "type": "integer", "format": "uint64", "minimum": 0.0 }, - "label": { - "description": "Label for the instantiated contract.", - "type": "string" + "cw4_voting": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "msg": { - "description": "Instantiate message to be used to create the contract.", + "proposal_single": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", "allOf": [ { - "$ref": "#/definitions/Binary" + "$ref": "#/definitions/Duration" } ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" } }, "additionalProperties": false - }, - "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", - "oneOf": [ - { - "description": "The majority of voters must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "majority" - ], - "properties": { - "majority": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "A percentage of voting power >= percent must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "additionalProperties": false - } - ] - }, - "PreProposeInfo": { - "oneOf": [ - { - "description": "Anyone may create a proposal free of charge.", - "type": "object", - "required": [ - "anyone_may_propose" - ], - "properties": { - "anyone_may_propose": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "The module specified in INFO has exclusive rights to proposal creation.", - "type": "object", - "required": [ - "module_may_propose" - ], - "properties": { - "module_may_propose": { - "type": "object", - "required": [ - "info" - ], - "properties": { - "info": { - "$ref": "#/definitions/ModuleInstantiateInfo" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "SingleChoiceProposeMsg": { - "description": "The contents of a message to create a proposal in the single choice proposal module.\n\nWe break this type out of `ExecuteMsg` because we want pre-propose modules that interact with this contract to be able to get type checking on their propose messages.\n\nWe move this type to this package so that pre-propose modules can import it without importing dao-proposal-single with the library feature which (as it is not additive) cause the execute exports to not be included in wasm builds.", - "type": "object", - "required": [ - "description", - "msgs", - "title" - ], - "properties": { - "description": { - "description": "A description of the proposal.", - "type": "string" - }, - "msgs": { - "description": "The messages that should be executed in response to this proposal passing.", - "type": "array", - "items": { - "$ref": "#/definitions/CosmosMsg_for_Empty" - } - }, - "proposer": { - "description": "The address creating the proposal. If no pre-propose module is attached to this module this must always be None as the proposer is the sender of the propose message. If a pre-propose module is attached, this must be Some and will set the proposer of the proposal it creates.", - "type": [ - "string", - "null" - ] - }, - "title": { - "description": "The title of the proposal.", - "type": "string" - } - }, - "additionalProperties": false - }, - "StakingMsg": { - "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "type": "string" - }, - "src_validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Threshold": { - "description": "The ways a proposal may reach its passing / failing threshold.", - "oneOf": [ - { - "description": "Declares a percentage of the total weight that must cast Yes votes in order for a proposal to pass. See `ThresholdResponse::AbsolutePercentage` in the cw3 spec for details.", - "type": "object", - "required": [ - "absolute_percentage" - ], - "properties": { - "absolute_percentage": { - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Declares a `quorum` of the total votes that must participate in the election in order for the vote to be considered at all. See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for details.", - "type": "object", - "required": [ - "threshold_quorum" - ], - "properties": { - "threshold_quorum": { - "type": "object", - "required": [ - "quorum", - "threshold" - ], - "properties": { - "quorum": { - "$ref": "#/definitions/PercentageThreshold" - }, - "threshold": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "threshold" - ], - "properties": { - "threshold": { - "$ref": "#/definitions/Uint128" - } - }, - "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" - }, - "Vote": { - "oneOf": [ - { - "description": "Marks support for the proposal.", - "type": "string", - "enum": [ - "yes" - ] - }, - { - "description": "Marks opposition to the proposal.", - "type": "string", - "enum": [ - "no" - ] - }, - { - "description": "Marks participation but does not count towards the ratio of support / opposed.", - "type": "string", - "enum": [ - "abstain" - ] - } - ] - }, - "VoteOption": { - "type": "string", - "enum": [ - "yes", - "no", - "abstain", - "no_with_veto" - ] - }, - "WasmMsg": { - "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", - "oneOf": [ - { - "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "execute" - ], - "properties": { - "execute": { - "type": "object", - "required": [ - "contract_addr", - "funds", - "msg" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "msg": { - "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "instantiate" - ], - "properties": { - "instantiate": { - "type": "object", - "required": [ - "code_id", - "funds", - "label", - "msg" - ], - "properties": { - "admin": { - "type": [ - "string", - "null" - ] - }, - "code_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "label": { - "description": "A human-readbale label for the contract", - "type": "string" - }, - "msg": { - "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "migrate" - ], - "properties": { - "migrate": { - "type": "object", - "required": [ - "contract_addr", - "msg", - "new_code_id" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "msg": { - "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", - "type": "object", - "required": [ - "update_admin" - ], - "properties": { - "update_admin": { - "type": "object", - "required": [ - "admin", - "contract_addr" - ], - "properties": { - "admin": { - "type": "string" - }, - "contract_addr": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", - "type": "object", - "required": [ - "clear_admin" - ], - "properties": { - "clear_admin": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - } - } - }, - "query": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", - "oneOf": [ - { - "description": "Gets the proposal module's config.", - "type": "object", - "required": [ - "config" - ], - "properties": { - "config": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets information about a proposal.", - "type": "object", - "required": [ - "proposal" - ], - "properties": { - "proposal": { - "type": "object", - "required": [ - "proposal_id" - ], - "properties": { - "proposal_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Lists all the proposals that have been cast in this module.", - "type": "object", - "required": [ - "list_proposals" - ], - "properties": { - "list_proposals": { - "type": "object", - "properties": { - "limit": { - "description": "The maximum number of proposals to return as part of this query. If no limit is set a max of 30 proposals will be returned.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "start_after": { - "description": "The proposal ID to start listing proposals after. For example, if this is set to 2 proposals with IDs 3 and higher will be returned.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Lists all of the proposals that have been cast in this module in decending order of proposal ID.", - "type": "object", - "required": [ - "reverse_proposals" - ], - "properties": { - "reverse_proposals": { - "type": "object", - "properties": { - "limit": { - "description": "The maximum number of proposals to return as part of this query. If no limit is set a max of 30 proposals will be returned.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "start_before": { - "description": "The proposal ID to start listing proposals before. For example, if this is set to 6 proposals with IDs 5 and lower will be returned.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns a voters position on a propsal.", - "type": "object", - "required": [ - "get_vote" - ], - "properties": { - "get_vote": { - "type": "object", - "required": [ - "proposal_id", - "voter" - ], - "properties": { - "proposal_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "voter": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Lists all of the votes that have been cast on a proposal.", - "type": "object", - "required": [ - "list_votes" - ], - "properties": { - "list_votes": { - "type": "object", - "required": [ - "proposal_id" - ], - "properties": { - "limit": { - "description": "The maximum number of votes to return in response to this query. If no limit is specified a max of 30 are returned.", - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "proposal_id": { - "description": "The proposal to list the votes of.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "start_after": { - "description": "The voter to start listing votes after. Ordering is done alphabetically.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Returns the number of proposals that have been created in this module.", - "type": "object", - "required": [ - "proposal_count" - ], - "properties": { - "proposal_count": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the current proposal creation policy for this module.", - "type": "object", - "required": [ - "proposal_creation_policy" - ], - "properties": { - "proposal_creation_policy": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Lists all of the consumers of proposal hooks for this module.", - "type": "object", - "required": [ - "proposal_hooks" - ], - "properties": { - "proposal_hooks": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Lists all of the consumers of vote hooks for this module.", - "type": "object", - "required": [ - "vote_hooks" - ], - "properties": { - "vote_hooks": { - "type": "object", - "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 - }, - { - "description": "Returns the proposal ID that will be assigned to the next proposal created.", - "type": "object", - "required": [ - "next_proposal_id" - ], - "properties": { - "next_proposal_id": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "oneOf": [ - { - "type": "object", - "required": [ - "from_v1" - ], - "properties": { - "from_v1": { - "type": "object", - "required": [ - "close_proposal_on_execution_failure", - "pre_propose_info" - ], - "properties": { - "close_proposal_on_execution_failure": { - "description": "This field was not present in DAO DAO v1. To migrate, a value must be specified.\n\nIf set to true proposals will be closed if their execution fails. Otherwise, proposals will remain open after execution failure. For example, with this enabled a proposal to send 5 tokens out of a DAO's treasury with 4 tokens would be closed when it is executed. With this disabled, that same proposal would remain open until the DAO's treasury was large enough for it to be executed.", - "type": "boolean" - }, - "pre_propose_info": { - "description": "This field was not present in DAO DAO v1. To migrate, a value must be specified.\n\nThis contains information about how a pre-propose module may be configured. If set to \"AnyoneMayPropose\", there will be no pre-propose module and consequently, no deposit or membership checks when submitting a proposal. The \"ModuleMayPropose\" option allows for instantiating a prepropose module which will handle deposit verification and return logic.", - "allOf": [ - { - "$ref": "#/definitions/PreProposeInfo" - } - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "from_compatible" - ], - "properties": { - "from_compatible": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "Admin": { - "description": "Information about the CosmWasm level admin of a contract. Used in conjunction with `ModuleInstantiateInfo` to instantiate modules.", - "oneOf": [ - { - "description": "Set the admin to a specified address.", - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Sets the admin as the core module address.", - "type": "object", - "required": [ - "core_module" - ], - "properties": { - "core_module": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "ModuleInstantiateInfo": { - "description": "Information needed to instantiate a module.", - "type": "object", - "required": [ - "code_id", - "label", - "msg" - ], - "properties": { - "admin": { - "description": "CosmWasm level admin of the instantiated contract. See: ", - "anyOf": [ - { - "$ref": "#/definitions/Admin" - }, - { - "type": "null" - } - ] - }, - "code_id": { - "description": "Code ID of the contract to be instantiated.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "label": { - "description": "Label for the instantiated contract.", - "type": "string" - }, - "msg": { - "description": "Instantiate message to be used to create the contract.", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - }, - "additionalProperties": false - }, - "PreProposeInfo": { - "oneOf": [ - { - "description": "Anyone may create a proposal free of charge.", - "type": "object", - "required": [ - "anyone_may_propose" - ], - "properties": { - "anyone_may_propose": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "The module specified in INFO has exclusive rights to proposal creation.", - "type": "object", - "required": [ - "module_may_propose" - ], - "properties": { - "module_may_propose": { - "type": "object", - "required": [ - "info" - ], - "properties": { - "info": { - "$ref": "#/definitions/ModuleInstantiateInfo" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ] - } - } - }, - "sudo": null, - "responses": { - "config": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Config", - "description": "The governance module's configuration.", - "type": "object", - "required": [ - "allow_revoting", - "close_proposal_on_execution_failure", - "dao", - "max_voting_period", - "only_members_execute", - "threshold" - ], - "properties": { - "allow_revoting": { - "description": "Allows changing votes before the proposal expires. If this is enabled proposals will not be able to complete early as final vote information is not known until the time of proposal expiration.", - "type": "boolean" - }, - "close_proposal_on_execution_failure": { - "description": "If set to true proposals will be closed if their execution fails. Otherwise, proposals will remain open after execution failure. For example, with this enabled a proposal to send 5 tokens out of a DAO's treasury with 4 tokens would be closed when it is executed. With this disabled, that same proposal would remain open until the DAO's treasury was large enough for it to be executed.", - "type": "boolean" - }, - "dao": { - "description": "The address of the DAO that this governance module is associated with.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "max_voting_period": { - "description": "The default maximum amount of time a proposal may be voted on before expiring.", - "allOf": [ - { - "$ref": "#/definitions/Duration" - } - ] - }, - "min_voting_period": { - "description": "The minimum amount of time a proposal must be open before passing. A proposal may fail before this amount of time has elapsed, but it will not pass. This can be useful for preventing governance attacks wherein an attacker aquires a large number of tokens and forces a proposal through.", - "anyOf": [ - { - "$ref": "#/definitions/Duration" - }, - { - "type": "null" - } - ] - }, - "only_members_execute": { - "description": "If set to true only members may execute passed proposals. Otherwise, any address may execute a passed proposal.", - "type": "boolean" - }, - "threshold": { - "description": "The threshold a proposal must reach to complete.", - "allOf": [ - { - "$ref": "#/definitions/Threshold" - } - ] - } - }, - "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" - }, - "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 - } - ] - }, - "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", - "oneOf": [ - { - "description": "The majority of voters must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "majority" - ], - "properties": { - "majority": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "A percentage of voting power >= percent must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "additionalProperties": false - } - ] - }, - "Threshold": { - "description": "The ways a proposal may reach its passing / failing threshold.", - "oneOf": [ - { - "description": "Declares a percentage of the total weight that must cast Yes votes in order for a proposal to pass. See `ThresholdResponse::AbsolutePercentage` in the cw3 spec for details.", - "type": "object", - "required": [ - "absolute_percentage" - ], - "properties": { - "absolute_percentage": { - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Declares a `quorum` of the total votes that must participate in the election in order for the vote to be considered at all. See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for details.", - "type": "object", - "required": [ - "threshold_quorum" - ], - "properties": { - "threshold_quorum": { - "type": "object", - "required": [ - "quorum", - "threshold" - ], - "properties": { - "quorum": { - "$ref": "#/definitions/PercentageThreshold" - }, - "threshold": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "threshold" - ], - "properties": { - "threshold": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - } - }, - "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" - } - } - }, - "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" - }, - "get_vote": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "VoteResponse", - "description": "Information about a vote.", - "type": "object", - "properties": { - "vote": { - "description": "None if no such vote, Some otherwise.", - "anyOf": [ - { - "$ref": "#/definitions/VoteInfo" - }, - { - "type": "null" - } - ] - } - }, - "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" - }, - "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" - }, - "Vote": { - "oneOf": [ - { - "description": "Marks support for the proposal.", - "type": "string", - "enum": [ - "yes" - ] - }, - { - "description": "Marks opposition to the proposal.", - "type": "string", - "enum": [ - "no" - ] - }, - { - "description": "Marks participation but does not count towards the ratio of support / opposed.", - "type": "string", - "enum": [ - "abstain" - ] - } - ] - }, - "VoteInfo": { - "description": "Information about a vote that was cast.", - "type": "object", - "required": [ - "power", - "vote", - "voter" - ], - "properties": { - "power": { - "description": "The voting power behind the vote.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "rationale": { - "description": "Address-specified rationale for the vote.", - "type": [ - "string", - "null" - ] - }, - "vote": { - "description": "Position on the vote.", - "allOf": [ - { - "$ref": "#/definitions/Vote" - } - ] - }, - "voter": { - "description": "The address that voted.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - } - }, - "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 - } - } - }, - "list_proposals": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ProposalListResponse", - "description": "A list of proposals returned by `ListProposals` and `ReverseProposals`.", - "type": "object", - "required": [ - "proposals" - ], - "properties": { - "proposals": { - "type": "array", - "items": { - "$ref": "#/definitions/ProposalResponse" - } - } - }, - "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" - }, - "BankMsg": { - "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", - "oneOf": [ - { - "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "send" - ], - "properties": { - "send": { - "type": "object", - "required": [ - "amount", - "to_address" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "to_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "CosmosMsg_for_Empty": { - "oneOf": [ - { - "type": "object", - "required": [ - "bank" - ], - "properties": { - "bank": { - "$ref": "#/definitions/BankMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "staking" - ], - "properties": { - "staking": { - "$ref": "#/definitions/StakingMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "distribution" - ], - "properties": { - "distribution": { - "$ref": "#/definitions/DistributionMsg" - } - }, - "additionalProperties": false - }, - { - "description": "A Stargate message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md)", - "type": "object", - "required": [ - "stargate" - ], - "properties": { - "stargate": { - "type": "object", - "required": [ - "type_url", - "value" - ], - "properties": { - "type_url": { - "type": "string" - }, - "value": { - "$ref": "#/definitions/Binary" - } - } - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "ibc" - ], - "properties": { - "ibc": { - "$ref": "#/definitions/IbcMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "wasm" - ], - "properties": { - "wasm": { - "$ref": "#/definitions/WasmMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "gov" - ], - "properties": { - "gov": { - "$ref": "#/definitions/GovMsg" - } - }, - "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" - }, - "DistributionMsg": { - "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "description": "The `withdraw_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "withdraw_delegator_reward" - ], - "properties": { - "withdraw_delegator_reward": { - "type": "object", - "required": [ - "validator" - ], - "properties": { - "validator": { - "description": "The `validator_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "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 - } - ] - }, - "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", - "oneOf": [ - { - "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", - "type": "object", - "required": [ - "vote" - ], - "properties": { - "vote": { - "type": "object", - "required": [ - "proposal_id", - "vote" - ], - "properties": { - "proposal_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] - } - } - } - }, - "additionalProperties": false - } - ] - }, - "IbcMsg": { - "description": "These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts (contracts that directly speak the IBC protocol via 6 entry points)", - "oneOf": [ - { - "description": "Sends bank tokens owned by the contract to the given address on another chain. The channel must already be established between the ibctransfer module on this chain and a matching module on the remote chain. We cannot select the port_id, this is whatever the local chain has bound the ibctransfer module to.", - "type": "object", - "required": [ - "transfer" - ], - "properties": { - "transfer": { - "type": "object", - "required": [ - "amount", - "channel_id", - "timeout", - "to_address" - ], - "properties": { - "amount": { - "description": "packet data only supports one coin https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "channel_id": { - "description": "exisiting channel to send the tokens over", - "type": "string" - }, - "timeout": { - "description": "when packet times out, measured on remote chain", - "allOf": [ - { - "$ref": "#/definitions/IbcTimeout" - } - ] - }, - "to_address": { - "description": "address on the remote chain to receive these tokens", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Sends an IBC packet with given data over the existing channel. Data should be encoded in a format defined by the channel version, and the module on the other side should know how to parse this.", - "type": "object", - "required": [ - "send_packet" - ], - "properties": { - "send_packet": { - "type": "object", - "required": [ - "channel_id", - "data", - "timeout" - ], - "properties": { - "channel_id": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/Binary" - }, - "timeout": { - "description": "when packet times out, measured on remote chain", - "allOf": [ - { - "$ref": "#/definitions/IbcTimeout" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This will close an existing channel that is owned by this contract. Port is auto-assigned to the contract's IBC port", - "type": "object", - "required": [ - "close_channel" - ], - "properties": { - "close_channel": { - "type": "object", - "required": [ - "channel_id" - ], - "properties": { - "channel_id": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "IbcTimeout": { - "description": "In IBC each package must set at least one type of timeout: the timestamp or the block height. Using this rather complex enum instead of two timeout fields we ensure that at least one timeout is set.", - "type": "object", - "properties": { - "block": { - "anyOf": [ - { - "$ref": "#/definitions/IbcTimeoutBlock" - }, - { - "type": "null" - } - ] - }, - "timestamp": { - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - } - } - }, - "IbcTimeoutBlock": { - "description": "IBCTimeoutHeight Height is a monotonically increasing data type that can be compared against another Height for the purposes of updating and freezing clients. Ordering is (revision_number, timeout_height)", - "type": "object", - "required": [ - "height", - "revision" - ], - "properties": { - "height": { - "description": "block height after which the packet times out. the height within the given revision", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - }, - "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", - "oneOf": [ - { - "description": "The majority of voters must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "majority" - ], - "properties": { - "majority": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "A percentage of voting power >= percent must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "additionalProperties": false - } - ] - }, - "ProposalResponse": { - "description": "Information about a proposal returned by proposal queries.", - "type": "object", - "required": [ - "id", - "proposal" - ], - "properties": { - "id": { - "description": "The ID of the proposal being returned.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "proposal": { - "$ref": "#/definitions/SingleChoiceProposal" - } - }, - "additionalProperties": false - }, - "SingleChoiceProposal": { - "type": "object", - "required": [ - "allow_revoting", - "description", - "expiration", - "msgs", - "proposer", - "start_height", - "status", - "threshold", - "title", - "total_power", - "votes" - ], - "properties": { - "allow_revoting": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "expiration": { - "description": "The the time at which this proposal will expire and close for additional votes.", - "allOf": [ - { - "$ref": "#/definitions/Expiration" - } - ] - }, - "min_voting_period": { - "description": "The minimum amount of time this proposal must remain open for voting. The proposal may not pass unless this is expired or None.", - "anyOf": [ - { - "$ref": "#/definitions/Expiration" - }, - { - "type": "null" - } - ] - }, - "msgs": { - "description": "The messages that will be executed should this proposal pass.", - "type": "array", - "items": { - "$ref": "#/definitions/CosmosMsg_for_Empty" - } - }, - "proposer": { - "description": "The address that created this proposal.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "start_height": { - "description": "The block height at which this proposal was created. Voting power queries should query for voting power at this block height.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "status": { - "$ref": "#/definitions/Status" - }, - "threshold": { - "description": "The threshold at which this proposal will pass.", - "allOf": [ - { - "$ref": "#/definitions/Threshold" - } - ] - }, - "title": { - "type": "string" - }, - "total_power": { - "description": "The total amount of voting power at the time of this proposal's creation.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "votes": { - "$ref": "#/definitions/Votes" - } - }, - "additionalProperties": false - }, - "StakingMsg": { - "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "type": "string" - }, - "src_validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Status": { - "oneOf": [ - { - "description": "The proposal is open for voting.", - "type": "string", - "enum": [ - "open" - ] - }, - { - "description": "The proposal has been rejected.", - "type": "string", - "enum": [ - "rejected" - ] - }, - { - "description": "The proposal has been passed but has not been executed.", - "type": "string", - "enum": [ - "passed" - ] - }, - { - "description": "The proposal has been passed and executed.", - "type": "string", - "enum": [ - "executed" - ] - }, - { - "description": "The proposal has failed or expired and has been closed. A proposal deposit refund has been issued if applicable.", - "type": "string", - "enum": [ - "closed" - ] - }, - { - "description": "The proposal's execution failed.", - "type": "string", - "enum": [ - "execution_failed" - ] - } - ] - }, - "Threshold": { - "description": "The ways a proposal may reach its passing / failing threshold.", - "oneOf": [ - { - "description": "Declares a percentage of the total weight that must cast Yes votes in order for a proposal to pass. See `ThresholdResponse::AbsolutePercentage` in the cw3 spec for details.", - "type": "object", - "required": [ - "absolute_percentage" - ], - "properties": { - "absolute_percentage": { - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Declares a `quorum` of the total votes that must participate in the election in order for the vote to be considered at all. See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for details.", - "type": "object", - "required": [ - "threshold_quorum" - ], - "properties": { - "threshold_quorum": { - "type": "object", - "required": [ - "quorum", - "threshold" - ], - "properties": { - "quorum": { - "$ref": "#/definitions/PercentageThreshold" - }, - "threshold": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "threshold" - ], - "properties": { - "threshold": { - "$ref": "#/definitions/Uint128" - } - }, - "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" - }, - "VoteOption": { - "type": "string", - "enum": [ - "yes", - "no", - "abstain", - "no_with_veto" - ] - }, - "Votes": { - "type": "object", - "required": [ - "abstain", - "no", - "yes" - ], - "properties": { - "abstain": { - "$ref": "#/definitions/Uint128" - }, - "no": { - "$ref": "#/definitions/Uint128" - }, - "yes": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - }, - "WasmMsg": { - "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", - "oneOf": [ - { - "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "execute" - ], - "properties": { - "execute": { - "type": "object", - "required": [ - "contract_addr", - "funds", - "msg" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "msg": { - "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "instantiate" - ], - "properties": { - "instantiate": { - "type": "object", - "required": [ - "code_id", - "funds", - "label", - "msg" - ], - "properties": { - "admin": { - "type": [ - "string", - "null" - ] - }, - "code_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "label": { - "description": "A human-readbale label for the contract", - "type": "string" - }, - "msg": { - "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "migrate" - ], - "properties": { - "migrate": { - "type": "object", - "required": [ - "contract_addr", - "msg", - "new_code_id" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "msg": { - "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", - "type": "object", - "required": [ - "update_admin" - ], - "properties": { - "update_admin": { - "type": "object", - "required": [ - "admin", - "contract_addr" - ], - "properties": { - "admin": { - "type": "string" - }, - "contract_addr": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", - "type": "object", - "required": [ - "clear_admin" - ], - "properties": { - "clear_admin": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - } - } - }, - "list_votes": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "VoteListResponse", - "description": "Information about the votes for a proposal.", - "type": "object", - "required": [ - "votes" - ], - "properties": { - "votes": { - "type": "array", - "items": { - "$ref": "#/definitions/VoteInfo" - } - } - }, - "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" - }, - "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" - }, - "Vote": { - "oneOf": [ - { - "description": "Marks support for the proposal.", - "type": "string", - "enum": [ - "yes" - ] - }, - { - "description": "Marks opposition to the proposal.", - "type": "string", - "enum": [ - "no" - ] - }, - { - "description": "Marks participation but does not count towards the ratio of support / opposed.", - "type": "string", - "enum": [ - "abstain" - ] - } - ] - }, - "VoteInfo": { - "description": "Information about a vote that was cast.", - "type": "object", - "required": [ - "power", - "vote", - "voter" - ], - "properties": { - "power": { - "description": "The voting power behind the vote.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "rationale": { - "description": "Address-specified rationale for the vote.", - "type": [ - "string", - "null" - ] - }, - "vote": { - "description": "Position on the vote.", - "allOf": [ - { - "$ref": "#/definitions/Vote" - } - ] - }, - "voter": { - "description": "The address that voted.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - } - }, - "additionalProperties": false - } - } - }, - "next_proposal_id": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "uint64", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "proposal": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ProposalResponse", - "description": "Information about a proposal returned by proposal queries.", - "type": "object", - "required": [ - "id", - "proposal" - ], - "properties": { - "id": { - "description": "The ID of the proposal being returned.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "proposal": { - "$ref": "#/definitions/SingleChoiceProposal" - } - }, - "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" - }, - "BankMsg": { - "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", - "oneOf": [ - { - "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "send" - ], - "properties": { - "send": { - "type": "object", - "required": [ - "amount", - "to_address" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "to_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "CosmosMsg_for_Empty": { - "oneOf": [ - { - "type": "object", - "required": [ - "bank" - ], - "properties": { - "bank": { - "$ref": "#/definitions/BankMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "staking" - ], - "properties": { - "staking": { - "$ref": "#/definitions/StakingMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "distribution" - ], - "properties": { - "distribution": { - "$ref": "#/definitions/DistributionMsg" - } - }, - "additionalProperties": false - }, - { - "description": "A Stargate message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md)", - "type": "object", - "required": [ - "stargate" - ], - "properties": { - "stargate": { - "type": "object", - "required": [ - "type_url", - "value" - ], - "properties": { - "type_url": { - "type": "string" - }, - "value": { - "$ref": "#/definitions/Binary" - } - } - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "ibc" - ], - "properties": { - "ibc": { - "$ref": "#/definitions/IbcMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "wasm" - ], - "properties": { - "wasm": { - "$ref": "#/definitions/WasmMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "gov" - ], - "properties": { - "gov": { - "$ref": "#/definitions/GovMsg" - } - }, - "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" - }, - "DistributionMsg": { - "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "description": "The `withdraw_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "withdraw_delegator_reward" - ], - "properties": { - "withdraw_delegator_reward": { - "type": "object", - "required": [ - "validator" - ], - "properties": { - "validator": { - "description": "The `validator_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "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 - } - ] - }, - "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", - "oneOf": [ - { - "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", - "type": "object", - "required": [ - "vote" - ], - "properties": { - "vote": { - "type": "object", - "required": [ - "proposal_id", - "vote" - ], - "properties": { - "proposal_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] - } - } - } - }, - "additionalProperties": false - } - ] - }, - "IbcMsg": { - "description": "These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts (contracts that directly speak the IBC protocol via 6 entry points)", - "oneOf": [ - { - "description": "Sends bank tokens owned by the contract to the given address on another chain. The channel must already be established between the ibctransfer module on this chain and a matching module on the remote chain. We cannot select the port_id, this is whatever the local chain has bound the ibctransfer module to.", - "type": "object", - "required": [ - "transfer" - ], - "properties": { - "transfer": { - "type": "object", - "required": [ - "amount", - "channel_id", - "timeout", - "to_address" - ], - "properties": { - "amount": { - "description": "packet data only supports one coin https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "channel_id": { - "description": "exisiting channel to send the tokens over", - "type": "string" - }, - "timeout": { - "description": "when packet times out, measured on remote chain", - "allOf": [ - { - "$ref": "#/definitions/IbcTimeout" - } - ] - }, - "to_address": { - "description": "address on the remote chain to receive these tokens", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Sends an IBC packet with given data over the existing channel. Data should be encoded in a format defined by the channel version, and the module on the other side should know how to parse this.", - "type": "object", - "required": [ - "send_packet" - ], - "properties": { - "send_packet": { - "type": "object", - "required": [ - "channel_id", - "data", - "timeout" - ], - "properties": { - "channel_id": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/Binary" - }, - "timeout": { - "description": "when packet times out, measured on remote chain", - "allOf": [ - { - "$ref": "#/definitions/IbcTimeout" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This will close an existing channel that is owned by this contract. Port is auto-assigned to the contract's IBC port", - "type": "object", - "required": [ - "close_channel" - ], - "properties": { - "close_channel": { - "type": "object", - "required": [ - "channel_id" - ], - "properties": { - "channel_id": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "IbcTimeout": { - "description": "In IBC each package must set at least one type of timeout: the timestamp or the block height. Using this rather complex enum instead of two timeout fields we ensure that at least one timeout is set.", - "type": "object", - "properties": { - "block": { - "anyOf": [ - { - "$ref": "#/definitions/IbcTimeoutBlock" - }, - { - "type": "null" - } - ] - }, - "timestamp": { - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - } - } - }, - "IbcTimeoutBlock": { - "description": "IBCTimeoutHeight Height is a monotonically increasing data type that can be compared against another Height for the purposes of updating and freezing clients. Ordering is (revision_number, timeout_height)", - "type": "object", - "required": [ - "height", - "revision" - ], - "properties": { - "height": { - "description": "block height after which the packet times out. the height within the given revision", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - }, - "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", - "oneOf": [ - { - "description": "The majority of voters must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "majority" - ], - "properties": { - "majority": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "A percentage of voting power >= percent must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "additionalProperties": false - } - ] - }, - "SingleChoiceProposal": { - "type": "object", - "required": [ - "allow_revoting", - "description", - "expiration", - "msgs", - "proposer", - "start_height", - "status", - "threshold", - "title", - "total_power", - "votes" - ], - "properties": { - "allow_revoting": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "expiration": { - "description": "The the time at which this proposal will expire and close for additional votes.", - "allOf": [ - { - "$ref": "#/definitions/Expiration" - } - ] - }, - "min_voting_period": { - "description": "The minimum amount of time this proposal must remain open for voting. The proposal may not pass unless this is expired or None.", - "anyOf": [ - { - "$ref": "#/definitions/Expiration" - }, - { - "type": "null" - } - ] - }, - "msgs": { - "description": "The messages that will be executed should this proposal pass.", - "type": "array", - "items": { - "$ref": "#/definitions/CosmosMsg_for_Empty" - } - }, - "proposer": { - "description": "The address that created this proposal.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "start_height": { - "description": "The block height at which this proposal was created. Voting power queries should query for voting power at this block height.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "status": { - "$ref": "#/definitions/Status" - }, - "threshold": { - "description": "The threshold at which this proposal will pass.", - "allOf": [ - { - "$ref": "#/definitions/Threshold" - } - ] - }, - "title": { - "type": "string" - }, - "total_power": { - "description": "The total amount of voting power at the time of this proposal's creation.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "votes": { - "$ref": "#/definitions/Votes" - } - }, - "additionalProperties": false - }, - "StakingMsg": { - "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "type": "string" - }, - "src_validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Status": { - "oneOf": [ - { - "description": "The proposal is open for voting.", - "type": "string", - "enum": [ - "open" - ] - }, - { - "description": "The proposal has been rejected.", - "type": "string", - "enum": [ - "rejected" - ] - }, - { - "description": "The proposal has been passed but has not been executed.", - "type": "string", - "enum": [ - "passed" - ] - }, - { - "description": "The proposal has been passed and executed.", - "type": "string", - "enum": [ - "executed" - ] - }, - { - "description": "The proposal has failed or expired and has been closed. A proposal deposit refund has been issued if applicable.", - "type": "string", - "enum": [ - "closed" - ] - }, - { - "description": "The proposal's execution failed.", - "type": "string", - "enum": [ - "execution_failed" - ] - } - ] - }, - "Threshold": { - "description": "The ways a proposal may reach its passing / failing threshold.", - "oneOf": [ - { - "description": "Declares a percentage of the total weight that must cast Yes votes in order for a proposal to pass. See `ThresholdResponse::AbsolutePercentage` in the cw3 spec for details.", - "type": "object", - "required": [ - "absolute_percentage" - ], - "properties": { - "absolute_percentage": { - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Declares a `quorum` of the total votes that must participate in the election in order for the vote to be considered at all. See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for details.", - "type": "object", - "required": [ - "threshold_quorum" - ], - "properties": { - "threshold_quorum": { - "type": "object", - "required": [ - "quorum", - "threshold" - ], - "properties": { - "quorum": { - "$ref": "#/definitions/PercentageThreshold" - }, - "threshold": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "threshold" - ], - "properties": { - "threshold": { - "$ref": "#/definitions/Uint128" - } - }, - "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" - }, - "VoteOption": { - "type": "string", - "enum": [ - "yes", - "no", - "abstain", - "no_with_veto" - ] - }, - "Votes": { - "type": "object", - "required": [ - "abstain", - "no", - "yes" - ], - "properties": { - "abstain": { - "$ref": "#/definitions/Uint128" - }, - "no": { - "$ref": "#/definitions/Uint128" - }, - "yes": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - }, - "WasmMsg": { - "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", - "oneOf": [ - { - "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "execute" - ], - "properties": { - "execute": { - "type": "object", - "required": [ - "contract_addr", - "funds", - "msg" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "msg": { - "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "instantiate" - ], - "properties": { - "instantiate": { - "type": "object", - "required": [ - "code_id", - "funds", - "label", - "msg" - ], - "properties": { - "admin": { - "type": [ - "string", - "null" - ] - }, - "code_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "label": { - "description": "A human-readbale label for the contract", - "type": "string" - }, - "msg": { - "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "migrate" - ], - "properties": { - "migrate": { - "type": "object", - "required": [ - "contract_addr", - "msg", - "new_code_id" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "msg": { - "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", - "type": "object", - "required": [ - "update_admin" - ], - "properties": { - "update_admin": { - "type": "object", - "required": [ - "admin", - "contract_addr" - ], - "properties": { - "admin": { - "type": "string" - }, - "contract_addr": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", - "type": "object", - "required": [ - "clear_admin" - ], - "properties": { - "clear_admin": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - } - } - }, - "proposal_count": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "uint64", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "proposal_creation_policy": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ProposalCreationPolicy", - "oneOf": [ - { - "description": "Anyone may create a proposal, free of charge.", - "type": "object", - "required": [ - "anyone" - ], - "properties": { - "anyone": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only ADDR may create proposals. It is expected that ADDR is a pre-propose module, though we only require that it is a valid address.", - "type": "object", - "required": [ - "module" - ], - "properties": { - "module": { - "type": "object", - "required": [ - "addr" - ], - "properties": { - "addr": { - "$ref": "#/definitions/Addr" - } - }, - "additionalProperties": false - } - }, - "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" - } - } - }, - "proposal_hooks": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "HooksResponse", - "type": "object", - "required": [ - "hooks" - ], - "properties": { - "hooks": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "reverse_proposals": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ProposalListResponse", - "description": "A list of proposals returned by `ListProposals` and `ReverseProposals`.", - "type": "object", - "required": [ - "proposals" - ], - "properties": { - "proposals": { - "type": "array", - "items": { - "$ref": "#/definitions/ProposalResponse" - } - } - }, - "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" - }, - "BankMsg": { - "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto", - "oneOf": [ - { - "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "send" - ], - "properties": { - "send": { - "type": "object", - "required": [ - "amount", - "to_address" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "to_address": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.", - "type": "object", - "required": [ - "burn" - ], - "properties": { - "burn": { - "type": "object", - "required": [ - "amount" - ], - "properties": { - "amount": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Binary": { - "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", - "type": "string" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "CosmosMsg_for_Empty": { - "oneOf": [ - { - "type": "object", - "required": [ - "bank" - ], - "properties": { - "bank": { - "$ref": "#/definitions/BankMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "custom" - ], - "properties": { - "custom": { - "$ref": "#/definitions/Empty" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "staking" - ], - "properties": { - "staking": { - "$ref": "#/definitions/StakingMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "distribution" - ], - "properties": { - "distribution": { - "$ref": "#/definitions/DistributionMsg" - } - }, - "additionalProperties": false - }, - { - "description": "A Stargate message encoded the same way as a protobuf [Any](https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/any.proto). This is the same structure as messages in `TxBody` from [ADR-020](https://github.com/cosmos/cosmos-sdk/blob/master/docs/architecture/adr-020-protobuf-transaction-encoding.md)", - "type": "object", - "required": [ - "stargate" - ], - "properties": { - "stargate": { - "type": "object", - "required": [ - "type_url", - "value" - ], - "properties": { - "type_url": { - "type": "string" - }, - "value": { - "$ref": "#/definitions/Binary" - } - } - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "ibc" - ], - "properties": { - "ibc": { - "$ref": "#/definitions/IbcMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "wasm" - ], - "properties": { - "wasm": { - "$ref": "#/definitions/WasmMsg" - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "gov" - ], - "properties": { - "gov": { - "$ref": "#/definitions/GovMsg" - } - }, - "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" - }, - "DistributionMsg": { - "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "set_withdraw_address" - ], - "properties": { - "set_withdraw_address": { - "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "description": "The `withdraw_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "withdraw_delegator_reward" - ], - "properties": { - "withdraw_delegator_reward": { - "type": "object", - "required": [ - "validator" - ], - "properties": { - "validator": { - "description": "The `validator_address`", - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Empty": { - "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", - "type": "object" - }, - "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 - } - ] - }, - "GovMsg": { - "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", - "oneOf": [ - { - "description": "This maps directly to [MsgVote](https://github.com/cosmos/cosmos-sdk/blob/v0.42.5/proto/cosmos/gov/v1beta1/tx.proto#L46-L56) in the Cosmos SDK with voter set to the contract address.", - "type": "object", - "required": [ - "vote" - ], - "properties": { - "vote": { - "type": "object", - "required": [ - "proposal_id", - "vote" - ], - "properties": { - "proposal_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "vote": { - "description": "The vote option.\n\nThis should be called \"option\" for consistency with Cosmos SDK. Sorry for that. See .", - "allOf": [ - { - "$ref": "#/definitions/VoteOption" - } - ] - } - } - } - }, - "additionalProperties": false - } - ] - }, - "IbcMsg": { - "description": "These are messages in the IBC lifecycle. Only usable by IBC-enabled contracts (contracts that directly speak the IBC protocol via 6 entry points)", - "oneOf": [ - { - "description": "Sends bank tokens owned by the contract to the given address on another chain. The channel must already be established between the ibctransfer module on this chain and a matching module on the remote chain. We cannot select the port_id, this is whatever the local chain has bound the ibctransfer module to.", - "type": "object", - "required": [ - "transfer" - ], - "properties": { - "transfer": { - "type": "object", - "required": [ - "amount", - "channel_id", - "timeout", - "to_address" - ], - "properties": { - "amount": { - "description": "packet data only supports one coin https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/ibc/applications/transfer/v1/transfer.proto#L11-L20", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "channel_id": { - "description": "exisiting channel to send the tokens over", - "type": "string" - }, - "timeout": { - "description": "when packet times out, measured on remote chain", - "allOf": [ - { - "$ref": "#/definitions/IbcTimeout" - } - ] - }, - "to_address": { - "description": "address on the remote chain to receive these tokens", - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Sends an IBC packet with given data over the existing channel. Data should be encoded in a format defined by the channel version, and the module on the other side should know how to parse this.", - "type": "object", - "required": [ - "send_packet" - ], - "properties": { - "send_packet": { - "type": "object", - "required": [ - "channel_id", - "data", - "timeout" - ], - "properties": { - "channel_id": { - "type": "string" - }, - "data": { - "$ref": "#/definitions/Binary" - }, - "timeout": { - "description": "when packet times out, measured on remote chain", - "allOf": [ - { - "$ref": "#/definitions/IbcTimeout" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This will close an existing channel that is owned by this contract. Port is auto-assigned to the contract's IBC port", - "type": "object", - "required": [ - "close_channel" - ], - "properties": { - "close_channel": { - "type": "object", - "required": [ - "channel_id" - ], - "properties": { - "channel_id": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "IbcTimeout": { - "description": "In IBC each package must set at least one type of timeout: the timestamp or the block height. Using this rather complex enum instead of two timeout fields we ensure that at least one timeout is set.", - "type": "object", - "properties": { - "block": { - "anyOf": [ - { - "$ref": "#/definitions/IbcTimeoutBlock" - }, - { - "type": "null" - } - ] - }, - "timestamp": { - "anyOf": [ - { - "$ref": "#/definitions/Timestamp" - }, - { - "type": "null" - } - ] - } - } - }, - "IbcTimeoutBlock": { - "description": "IBCTimeoutHeight Height is a monotonically increasing data type that can be compared against another Height for the purposes of updating and freezing clients. Ordering is (revision_number, timeout_height)", - "type": "object", - "required": [ - "height", - "revision" - ], - "properties": { - "height": { - "description": "block height after which the packet times out. the height within the given revision", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - }, - "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", - "oneOf": [ - { - "description": "The majority of voters must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "majority" - ], - "properties": { - "majority": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "A percentage of voting power >= percent must vote yes for the proposal to pass.", - "type": "object", - "required": [ - "percent" - ], - "properties": { - "percent": { - "$ref": "#/definitions/Decimal" - } - }, - "additionalProperties": false - } - ] - }, - "ProposalResponse": { - "description": "Information about a proposal returned by proposal queries.", - "type": "object", - "required": [ - "id", - "proposal" - ], - "properties": { - "id": { - "description": "The ID of the proposal being returned.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "proposal": { - "$ref": "#/definitions/SingleChoiceProposal" - } - }, - "additionalProperties": false - }, - "SingleChoiceProposal": { - "type": "object", - "required": [ - "allow_revoting", - "description", - "expiration", - "msgs", - "proposer", - "start_height", - "status", - "threshold", - "title", - "total_power", - "votes" - ], - "properties": { - "allow_revoting": { - "type": "boolean" - }, - "description": { - "type": "string" - }, - "expiration": { - "description": "The the time at which this proposal will expire and close for additional votes.", - "allOf": [ - { - "$ref": "#/definitions/Expiration" - } - ] - }, - "min_voting_period": { - "description": "The minimum amount of time this proposal must remain open for voting. The proposal may not pass unless this is expired or None.", - "anyOf": [ - { - "$ref": "#/definitions/Expiration" - }, - { - "type": "null" - } - ] - }, - "msgs": { - "description": "The messages that will be executed should this proposal pass.", - "type": "array", - "items": { - "$ref": "#/definitions/CosmosMsg_for_Empty" - } - }, - "proposer": { - "description": "The address that created this proposal.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "start_height": { - "description": "The block height at which this proposal was created. Voting power queries should query for voting power at this block height.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "status": { - "$ref": "#/definitions/Status" - }, - "threshold": { - "description": "The threshold at which this proposal will pass.", - "allOf": [ - { - "$ref": "#/definitions/Threshold" - } - ] - }, - "title": { - "type": "string" - }, - "total_power": { - "description": "The total amount of voting power at the time of this proposal's creation.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - }, - "votes": { - "$ref": "#/definitions/Votes" - } - }, - "additionalProperties": false - }, - "StakingMsg": { - "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", - "oneOf": [ - { - "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "delegate" - ], - "properties": { - "delegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "undelegate" - ], - "properties": { - "undelegate": { - "type": "object", - "required": [ - "amount", - "validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "redelegate" - ], - "properties": { - "redelegate": { - "type": "object", - "required": [ - "amount", - "dst_validator", - "src_validator" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Coin" - }, - "dst_validator": { - "type": "string" - }, - "src_validator": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - }, - "Status": { - "oneOf": [ - { - "description": "The proposal is open for voting.", - "type": "string", - "enum": [ - "open" - ] - }, - { - "description": "The proposal has been rejected.", - "type": "string", - "enum": [ - "rejected" - ] - }, - { - "description": "The proposal has been passed but has not been executed.", - "type": "string", - "enum": [ - "passed" - ] - }, - { - "description": "The proposal has been passed and executed.", - "type": "string", - "enum": [ - "executed" - ] - }, - { - "description": "The proposal has failed or expired and has been closed. A proposal deposit refund has been issued if applicable.", - "type": "string", - "enum": [ - "closed" - ] - }, - { - "description": "The proposal's execution failed.", - "type": "string", - "enum": [ - "execution_failed" - ] - } - ] - }, - "Threshold": { - "description": "The ways a proposal may reach its passing / failing threshold.", - "oneOf": [ - { - "description": "Declares a percentage of the total weight that must cast Yes votes in order for a proposal to pass. See `ThresholdResponse::AbsolutePercentage` in the cw3 spec for details.", - "type": "object", - "required": [ - "absolute_percentage" - ], - "properties": { - "absolute_percentage": { - "type": "object", - "required": [ - "percentage" - ], - "properties": { - "percentage": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Declares a `quorum` of the total votes that must participate in the election in order for the vote to be considered at all. See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for details.", - "type": "object", - "required": [ - "threshold_quorum" - ], - "properties": { - "threshold_quorum": { - "type": "object", - "required": [ - "quorum", - "threshold" - ], - "properties": { - "quorum": { - "$ref": "#/definitions/PercentageThreshold" - }, - "threshold": { - "$ref": "#/definitions/PercentageThreshold" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.", - "type": "object", - "required": [ - "absolute_count" - ], - "properties": { - "absolute_count": { - "type": "object", - "required": [ - "threshold" - ], - "properties": { - "threshold": { - "$ref": "#/definitions/Uint128" - } - }, - "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" - }, - "VoteOption": { - "type": "string", - "enum": [ - "yes", - "no", - "abstain", - "no_with_veto" - ] - }, - "Votes": { - "type": "object", - "required": [ - "abstain", - "no", - "yes" - ], - "properties": { - "abstain": { - "$ref": "#/definitions/Uint128" - }, - "no": { - "$ref": "#/definitions/Uint128" - }, - "yes": { - "$ref": "#/definitions/Uint128" - } - }, - "additionalProperties": false - }, - "WasmMsg": { - "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto", - "oneOf": [ - { - "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "execute" - ], - "properties": { - "execute": { - "type": "object", - "required": [ - "contract_addr", - "funds", - "msg" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "msg": { - "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThe contract address is non-predictable. But it is guaranteed that when emitting the same Instantiate message multiple times, multiple instances on different addresses will be generated. See also Instantiate2.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L53-L71). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "instantiate" - ], - "properties": { - "instantiate": { - "type": "object", - "required": [ - "code_id", - "funds", - "label", - "msg" - ], - "properties": { - "admin": { - "type": [ - "string", - "null" - ] - }, - "code_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "funds": { - "type": "array", - "items": { - "$ref": "#/definitions/Coin" - } - }, - "label": { - "description": "A human-readbale label for the contract", - "type": "string" - }, - "msg": { - "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.", - "type": "object", - "required": [ - "migrate" - ], - "properties": { - "migrate": { - "type": "object", - "required": [ - "contract_addr", - "msg", - "new_code_id" - ], - "properties": { - "contract_addr": { - "type": "string" - }, - "msg": { - "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code", - "allOf": [ - { - "$ref": "#/definitions/Binary" - } - ] - }, - "new_code_id": { - "description": "the code_id of the new logic to place in the given contract", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.", - "type": "object", - "required": [ - "update_admin" - ], - "properties": { - "update_admin": { - "type": "object", - "required": [ - "admin", - "contract_addr" - ], - "properties": { - "admin": { - "type": "string" - }, - "contract_addr": { - "type": "string" - } - } - } - }, - "additionalProperties": false - }, - { - "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.", - "type": "object", - "required": [ - "clear_admin" - ], - "properties": { - "clear_admin": { - "type": "object", - "required": [ - "contract_addr" - ], - "properties": { - "contract_addr": { - "type": "string" - } - } - } - }, - "additionalProperties": false - } - ] - } } - }, - "vote_hooks": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "HooksResponse", - "type": "object", - "required": [ - "hooks" - ], - "properties": { - "hooks": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false } - } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "type": "string", + "enum": [] + }, + "migrate": null, + "sudo": null, + "responses": {} } diff --git a/contracts/external/dao-migrator/src/contract.rs b/contracts/external/dao-migrator/src/contract.rs index ae91152d5..625d0276b 100644 --- a/contracts/external/dao-migrator/src/contract.rs +++ b/contracts/external/dao-migrator/src/contract.rs @@ -3,7 +3,7 @@ use std::{collections::HashSet, env}; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, + to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, WasmMsg, }; use cw2::set_contract_version; @@ -42,10 +42,10 @@ pub fn instantiate( CORE_ADDR.save(deps.storage, &info.sender)?; Ok( - Response::default().set_data(to_binary(&ModuleInstantiateCallback { + Response::default().set_data(to_json_binary(&ModuleInstantiateCallback { msgs: vec![WasmMsg::Execute { contract_addr: env.contract.address.to_string(), - msg: to_binary(&MigrateV1ToV2 { + msg: to_json_binary(&MigrateV1ToV2 { sub_daos: msg.sub_daos, migration_params: msg.migration_params, v1_code_ids: msg.v1_code_ids, @@ -115,6 +115,7 @@ fn execute_migration_v1_v2( close_proposal_on_execution_failure: proposal_params .close_proposal_on_execution_failure, pre_propose_info: proposal_params.pre_propose_info, + veto: proposal_params.veto, }, ), ), @@ -168,7 +169,7 @@ fn execute_migration_v1_v2( WasmMsg::Migrate { contract_addr: voting_module.to_string(), new_code_id: voting_pair.v2_code_id, - msg: to_binary(&voting_pair.migrate_msg).unwrap(), + msg: to_json_binary(&voting_pair.migrate_msg).unwrap(), } .into(), ); @@ -212,7 +213,7 @@ fn execute_migration_v1_v2( WasmMsg::Migrate { contract_addr: cw20_staked_addr.to_string(), new_code_id: staking_pair.v2_code_id, - msg: to_binary(&staking_pair.migrate_msg).unwrap(), + msg: to_json_binary(&staking_pair.migrate_msg).unwrap(), } .into(), ); @@ -276,7 +277,7 @@ fn execute_migration_v1_v2( WasmMsg::Migrate { contract_addr: module.address.to_string(), new_code_id: proposal_pair.v2_code_id, - msg: to_binary(&proposal_pair.migrate_msg).unwrap(), + msg: to_json_binary(&proposal_pair.migrate_msg).unwrap(), } .into(), ); @@ -305,7 +306,7 @@ fn execute_migration_v1_v2( msgs.push( WasmMsg::Execute { contract_addr: info.sender.to_string(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::UpdateSubDaos { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::UpdateSubDaos { to_add: sub_daos, to_remove: vec![], })?, @@ -318,7 +319,7 @@ fn execute_migration_v1_v2( let proposal_hook_msg = SubMsg::reply_on_success( WasmMsg::Execute { contract_addr: info.sender.to_string(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs })?, + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs })?, funds: vec![], }, V1_V2_REPLY_ID, @@ -345,13 +346,15 @@ pub fn reply(deps: DepsMut, env: Env, reply: Reply) -> Result (Addr, V let (voting_code_id, msg) = match voting_type { VotingType::Cw4 => ( code_ids.cw4_voting, - to_binary(&get_cw4_init_msg(code_ids.clone())).unwrap(), + to_json_binary(&get_cw4_init_msg(code_ids.clone())).unwrap(), ), VotingType::Cw20 => ( code_ids.cw20_voting, - to_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(), + to_json_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(), ), VotingType::Cw20V03 => { // The simple change we need to do is to swap the cw20_stake with the one in v0.3.0 @@ -35,7 +35,7 @@ pub fn init_v1(app: &mut App, sender: Addr, voting_type: VotingType) -> (Addr, V ( code_ids.cw20_voting, - to_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(), + to_json_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(), ) } }; @@ -59,7 +59,7 @@ pub fn init_v1(app: &mut App, sender: Addr, voting_type: VotingType) -> (Addr, V }, proposal_modules_instantiate_info: vec![cw_core_v1::msg::ModuleInstantiateInfo { code_id: code_ids.proposal_single, - msg: to_binary(&cw_proposal_single_v1::msg::InstantiateMsg { + msg: to_json_binary(&cw_proposal_single_v1::msg::InstantiateMsg { threshold: voting_v1::Threshold::AbsolutePercentage { percentage: voting_v1::PercentageThreshold::Majority {}, }, @@ -109,11 +109,11 @@ pub fn init_v1_with_multiple_proposals( let (voting_code_id, msg) = match voting_type { VotingType::Cw4 => ( code_ids.cw4_voting, - to_binary(&get_cw4_init_msg(code_ids.clone())).unwrap(), + to_json_binary(&get_cw4_init_msg(code_ids.clone())).unwrap(), ), VotingType::Cw20 => ( code_ids.cw20_voting, - to_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(), + to_json_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(), ), VotingType::Cw20V03 => { let v03_cw20_stake = app.store_code(stake_cw20_v03_contract()); @@ -122,7 +122,7 @@ pub fn init_v1_with_multiple_proposals( ( code_ids.cw20_voting, - to_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(), + to_json_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(), ) } }; @@ -147,7 +147,7 @@ pub fn init_v1_with_multiple_proposals( proposal_modules_instantiate_info: vec![ cw_core_v1::msg::ModuleInstantiateInfo { code_id: code_ids.proposal_single, - msg: to_binary(&cw_proposal_single_v1::msg::InstantiateMsg { + msg: to_json_binary(&cw_proposal_single_v1::msg::InstantiateMsg { threshold: voting_v1::Threshold::AbsolutePercentage { percentage: voting_v1::PercentageThreshold::Majority {}, }, @@ -163,7 +163,7 @@ pub fn init_v1_with_multiple_proposals( }, cw_core_v1::msg::ModuleInstantiateInfo { code_id: code_ids.proposal_single, - msg: to_binary(&cw_proposal_single_v1::msg::InstantiateMsg { + msg: to_json_binary(&cw_proposal_single_v1::msg::InstantiateMsg { threshold: voting_v1::Threshold::AbsolutePercentage { percentage: voting_v1::PercentageThreshold::Majority {}, }, @@ -275,6 +275,7 @@ pub fn execute_migration( close_proposal_on_execution_failure: true, pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, ) }) @@ -291,7 +292,7 @@ pub fn execute_migration( WasmMsg::Migrate { contract_addr: module_addrs.core.to_string(), new_code_id: new_code_ids.core, - msg: to_binary(&dao_interface::msg::MigrateMsg::FromV1 { + msg: to_json_binary(&dao_interface::msg::MigrateMsg::FromV1 { dao_uri: None, params: None, }) @@ -300,10 +301,10 @@ pub fn execute_migration( .into(), WasmMsg::Execute { contract_addr: module_addrs.core.to_string(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::UpdateProposalModules { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::UpdateProposalModules { to_add: vec![ModuleInstantiateInfo { code_id: migrator_code_id, - msg: to_binary(&crate::msg::InstantiateMsg { + msg: to_json_binary(&crate::msg::InstantiateMsg { sub_daos: params.sub_daos.unwrap(), migration_params: MigrationParams { migrate_stake_cw20_manager: params.migrate_cw20, @@ -314,6 +315,7 @@ pub fn execute_migration( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "migrator".to_string(), }], to_disable: vec![], @@ -397,7 +399,7 @@ pub fn execute_migration_from_core( msgs: vec![WasmMsg::Migrate { contract_addr: module_addrs.core.to_string(), new_code_id: new_code_ids.core, - msg: to_binary(&dao_interface::msg::MigrateMsg::FromV1 { + msg: to_json_binary(&dao_interface::msg::MigrateMsg::FromV1 { dao_uri: None, params: Some(dao_interface::migrate_msg::MigrateParams { migrator_code_id, diff --git a/contracts/external/dao-migrator/src/testing/state_helpers.rs b/contracts/external/dao-migrator/src/testing/state_helpers.rs index 451a28821..13dbb82ef 100644 --- a/contracts/external/dao-migrator/src/testing/state_helpers.rs +++ b/contracts/external/dao-migrator/src/testing/state_helpers.rs @@ -54,6 +54,7 @@ pub fn query_proposal_v1( status: v1_status_to_v2(proposal.status), votes: v1_votes_to_v2(proposal.votes), allow_revoting: proposal.allow_revoting, + veto: None, }; (proposal_count, proposal) diff --git a/contracts/external/dao-migrator/src/testing/test_migration.rs b/contracts/external/dao-migrator/src/testing/test_migration.rs index cf936f7e4..691e00c9f 100644 --- a/contracts/external/dao-migrator/src/testing/test_migration.rs +++ b/contracts/external/dao-migrator/src/testing/test_migration.rs @@ -189,6 +189,7 @@ fn test_duplicate_proposal_params() { ProposalParams { close_proposal_on_execution_failure: true, pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, ), ( @@ -196,6 +197,7 @@ fn test_duplicate_proposal_params() { ProposalParams { close_proposal_on_execution_failure: true, pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, ), ]; diff --git a/contracts/external/dao-migrator/src/types.rs b/contracts/external/dao-migrator/src/types.rs index 837ca736e..4663bdee2 100644 --- a/contracts/external/dao-migrator/src/types.rs +++ b/contracts/external/dao-migrator/src/types.rs @@ -1,5 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; +use dao_voting::veto::VetoConfig; use crate::ContractError; @@ -46,6 +47,7 @@ impl V2CodeIds { pub struct ProposalParams { pub close_proposal_on_execution_failure: bool, pub pre_propose_info: dao_voting::pre_propose::PreProposeInfo, + pub veto: Option, } #[cw_serde] diff --git a/contracts/external/dao-migrator/src/utils/state_queries.rs b/contracts/external/dao-migrator/src/utils/state_queries.rs index 52fcc8bfd..338281d93 100644 --- a/contracts/external/dao-migrator/src/utils/state_queries.rs +++ b/contracts/external/dao-migrator/src/utils/state_queries.rs @@ -83,6 +83,7 @@ pub fn query_proposal_v1( status: v1_status_to_v2(proposal.status), votes: v1_votes_to_v2(proposal.votes), allow_revoting: proposal.allow_revoting, + veto: None, }) }) .collect::, ContractError>>( diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/Cargo.toml b/contracts/pre-propose/dao-pre-propose-approval-single/Cargo.toml index 659b2d7d7..c38ce7f64 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/Cargo.toml +++ b/contracts/pre-propose/dao-pre-propose-approval-single/Cargo.toml @@ -35,7 +35,7 @@ cw4-group = { workspace = true } cw20 = { workspace = true } cw20-base = { workspace = true } dao-dao-core = { workspace = true } -dao-proposal-hooks = { workspace = true } +dao-hooks = { workspace = true } dao-testing = { workspace = true } dao-voting = { workspace = true } dao-voting-cw4 = { workspace = true } diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/README.md b/contracts/pre-propose/dao-pre-propose-approval-single/README.md index 84de2b614..62cc82e44 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/README.md +++ b/contracts/pre-propose/dao-pre-propose-approval-single/README.md @@ -1,5 +1,8 @@ # Single choice proposal approval contract +[![dao-pre-propose-approval-single on crates.io](https://img.shields.io/crates/v/dao-pre-propose-approval-single.svg?logo=rust)](https://crates.io/crates/dao-pre-propose-approval-single) +[![docs.rs](https://img.shields.io/docsrs/dao-pre-propose-approval-single?logo=docsdotrs)](https://docs.rs/dao-pre-propose-approval-single/latest/dao_pre_propose_approval_single/) + This contract implements an approval flow for proposals, it also handles deposit logic. It works with the `cwd-proposal-single` proposal module. ## Approval Logic diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json b/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json index c11084723..9a668eb2a 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json +++ b/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json @@ -1,6 +1,6 @@ { "contract_name": "dao-pre-propose-approval-single", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -751,6 +751,53 @@ } ] }, + "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 + } + ] + }, "GovMsg": { "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ @@ -816,7 +863,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -934,7 +981,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -1104,6 +1151,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -1273,7 +1349,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -1500,6 +1576,54 @@ }, "additionalProperties": false }, + { + "description": "Return whether or not the proposal is pending", + "type": "object", + "required": [ + "is_pending" + ], + "properties": { + "is_pending": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "A proposal, pending or completed.", + "type": "object", + "required": [ + "proposal" + ], + "properties": { + "proposal": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "A pending proposal", "type": "object", @@ -1586,6 +1710,117 @@ } }, "additionalProperties": false + }, + { + "description": "A completed proposal", + "type": "object", + "required": [ + "completed_proposal" + ], + "properties": { + "completed_proposal": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "List of completed proposals", + "type": "object", + "required": [ + "completed_proposals" + ], + "properties": { + "completed_proposals": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "reverse_completed_proposals" + ], + "properties": { + "reverse_completed_proposals": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_before": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The completed approval ID for a created proposal ID.", + "type": "object", + "required": [ + "completed_proposal_id_for_created_proposal_id" + ], + "properties": { + "completed_proposal_id_for_created_proposal_id": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] } diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs index a0ad2b764..215335d83 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs @@ -1,8 +1,8 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult, SubMsg, - WasmMsg, + to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult, + SubMsg, WasmMsg, }; use cw2::set_contract_version; use cw_paginate_storage::paginate_map_values; @@ -16,7 +16,10 @@ use crate::msg::{ ApproverProposeMessage, ExecuteExt, ExecuteMsg, InstantiateExt, InstantiateMsg, ProposeMessage, ProposeMessageInternal, QueryExt, QueryMsg, }; -use crate::state::{advance_approval_id, PendingProposal, APPROVER, PENDING_PROPOSALS}; +use crate::state::{ + advance_approval_id, Proposal, ProposalStatus, APPROVER, COMPLETED_PROPOSALS, + CREATED_PROPOSAL_TO_COMPLETED_PROPOSAL, PENDING_PROPOSALS, +}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-pre-propose-approval-single"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -107,7 +110,7 @@ pub fn execute_propose( .prepare_hooks(deps.storage, |a| { let execute_msg = WasmMsg::Execute { contract_addr: a.into_string(), - msg: to_binary(&ExecuteBase::::Propose { + msg: to_json_binary(&ExecuteBase::::Propose { msg: ApproverProposeMessage::Propose { title: propose_msg_internal.title.clone(), description: propose_msg_internal.description.clone(), @@ -123,7 +126,8 @@ pub fn execute_propose( PENDING_PROPOSALS.save( deps.storage, approval_id, - &PendingProposal { + &Proposal { + status: ProposalStatus::Pending {}, approval_id, proposer: info.sender, msg: propose_msg_internal, @@ -164,14 +168,29 @@ pub fn execute_approve( PrePropose::default().deposits.save( deps.storage, proposal_id, - &(proposal.deposit, proposal.proposer), + &(proposal.deposit.clone(), proposal.proposer.clone()), )?; let propose_messsage = WasmMsg::Execute { contract_addr: proposal_module.into_string(), - msg: to_binary(&ProposeMessageInternal::Propose(proposal.msg))?, + msg: to_json_binary(&ProposeMessageInternal::Propose(proposal.msg.clone()))?, funds: vec![], }; + + COMPLETED_PROPOSALS.save( + deps.storage, + id, + &Proposal { + status: ProposalStatus::Approved { + created_proposal_id: proposal_id, + }, + approval_id: proposal.approval_id, + proposer: proposal.proposer, + msg: proposal.msg, + deposit: proposal.deposit, + }, + )?; + CREATED_PROPOSAL_TO_COMPLETED_PROPOSAL.save(deps.storage, proposal_id, &id)?; PENDING_PROPOSALS.remove(deps.storage, id); Ok(Response::default() @@ -195,12 +214,27 @@ pub fn execute_reject( return Err(PreProposeError::Unauthorized {}); } - let PendingProposal { - deposit, proposer, .. + let Proposal { + approval_id, + proposer, + msg, + deposit, + .. } = PENDING_PROPOSALS .may_load(deps.storage, id)? .ok_or(PreProposeError::ProposalNotFound {})?; + COMPLETED_PROPOSALS.save( + deps.storage, + id, + &Proposal { + status: ProposalStatus::Rejected {}, + approval_id, + proposer: proposer.clone(), + msg: msg.clone(), + deposit: deposit.clone(), + }, + )?; PENDING_PROPOSALS.remove(deps.storage, id); let messages = if let Some(ref deposit_info) = deposit { @@ -221,7 +255,7 @@ pub fn execute_reject( Ok(Response::default() .add_attribute("method", "proposal_rejected") .add_attribute("proposal", id.to_string()) - .add_attribute("deposit_info", to_binary(&deposit)?.to_string()) + .add_attribute("deposit_info", to_json_binary(&deposit)?.to_string()) .add_messages(messages)) } @@ -296,27 +330,73 @@ pub fn execute_remove_approver_hook( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryExtension { msg } => match msg { - QueryExt::Approver {} => to_binary(&APPROVER.load(deps.storage)?), + QueryExt::Approver {} => to_json_binary(&APPROVER.load(deps.storage)?), + QueryExt::IsPending { id } => { + let pending = PENDING_PROPOSALS.may_load(deps.storage, id)?.is_some(); + // Force load completed proposal if not pending, throwing error + // if not found. + if !pending { + COMPLETED_PROPOSALS.load(deps.storage, id)?; + } + + to_json_binary(&pending) + } + QueryExt::Proposal { id } => { + if let Some(pending) = PENDING_PROPOSALS.may_load(deps.storage, id)? { + to_json_binary(&pending) + } else { + // Force load completed proposal if not pending, throwing + // error if not found. + to_json_binary(&COMPLETED_PROPOSALS.load(deps.storage, id)?) + } + } QueryExt::PendingProposal { id } => { - to_binary(&PENDING_PROPOSALS.load(deps.storage, id)?) + to_json_binary(&PENDING_PROPOSALS.load(deps.storage, id)?) } - QueryExt::PendingProposals { start_after, limit } => to_binary(&paginate_map_values( + QueryExt::PendingProposals { start_after, limit } => { + to_json_binary(&paginate_map_values( + deps, + &PENDING_PROPOSALS, + start_after, + limit, + Order::Ascending, + )?) + } + QueryExt::ReversePendingProposals { + start_before, + limit, + } => to_json_binary(&paginate_map_values( deps, &PENDING_PROPOSALS, - start_after, + start_before, limit, Order::Descending, )?), - QueryExt::ReversePendingProposals { + QueryExt::CompletedProposal { id } => { + to_json_binary(&COMPLETED_PROPOSALS.load(deps.storage, id)?) + } + QueryExt::CompletedProposals { start_after, limit } => { + to_json_binary(&paginate_map_values( + deps, + &COMPLETED_PROPOSALS, + start_after, + limit, + Order::Ascending, + )?) + } + QueryExt::ReverseCompletedProposals { start_before, limit, - } => to_binary(&paginate_map_values( + } => to_json_binary(&paginate_map_values( deps, - &PENDING_PROPOSALS, + &COMPLETED_PROPOSALS, start_before, limit, - Order::Ascending, + Order::Descending, )?), + QueryExt::CompletedProposalIdForCreatedProposalId { id } => { + to_json_binary(&CREATED_PROPOSAL_TO_COMPLETED_PROPOSAL.may_load(deps.storage, id)?) + } }, _ => PrePropose::default().query(deps, env, msg), } diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs index 8a4df00df..01b83e361 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs @@ -44,20 +44,43 @@ pub enum QueryExt { /// List the approver address #[returns(cosmwasm_std::Addr)] Approver {}, + /// Return whether or not the proposal is pending + #[returns(bool)] + IsPending { id: u64 }, + /// A proposal, pending or completed. + #[returns(crate::state::Proposal)] + Proposal { id: u64 }, /// A pending proposal - #[returns(crate::state::PendingProposal)] + #[returns(crate::state::Proposal)] PendingProposal { id: u64 }, /// List of proposals awaiting approval - #[returns(Vec)] + #[returns(Vec)] PendingProposals { start_after: Option, limit: Option, }, - #[returns(Vec)] + #[returns(Vec)] ReversePendingProposals { start_before: Option, limit: Option, }, + /// A completed proposal + #[returns(crate::state::Proposal)] + CompletedProposal { id: u64 }, + /// List of completed proposals + #[returns(Vec)] + CompletedProposals { + start_after: Option, + limit: Option, + }, + #[returns(Vec)] + ReverseCompletedProposals { + start_before: Option, + limit: Option, + }, + /// The completed approval ID for a created proposal ID. + #[returns(::std::option::Option)] + CompletedProposalIdForCreatedProposalId { id: u64 }, } pub type InstantiateMsg = InstantiateBase; diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/state.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/state.rs index 5c11aedf9..5ceb766dc 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/state.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/state.rs @@ -6,7 +6,22 @@ use dao_voting::deposit::CheckedDepositInfo; use dao_voting::proposal::SingleChoiceProposeMsg as ProposeMsg; #[cw_serde] -pub struct PendingProposal { +pub enum ProposalStatus { + /// The proposal is pending approval. + Pending {}, + /// The proposal has been approved. + Approved { + /// The created proposal ID. + created_proposal_id: u64, + }, + /// The proposal has been rejected. + Rejected {}, +} + +#[cw_serde] +pub struct Proposal { + /// The status of a completed proposal. + pub status: ProposalStatus, /// The approval ID used to identify this pending proposal. pub approval_id: u64, /// The address that created the proposal. @@ -20,7 +35,10 @@ pub struct PendingProposal { } pub const APPROVER: Item = Item::new("approver"); -pub const PENDING_PROPOSALS: Map = Map::new("pending_proposals"); +pub const PENDING_PROPOSALS: Map = Map::new("pending_proposals"); +pub const COMPLETED_PROPOSALS: Map = Map::new("completed_proposals"); +pub const CREATED_PROPOSAL_TO_COMPLETED_PROPOSAL: Map = + Map::new("created_to_completed_proposal"); /// Used internally to track the current approval_id. const CURRENT_ID: Item = Item::new("current_id"); diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs index 2127cebf5..07afdf1da 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{coins, from_slice, to_binary, Addr, Coin, Empty, Uint128}; +use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Coin, Empty, Uint128}; use cw2::ContractVersion; use cw20::Cw20Coin; use cw_denom::UncheckedDenom; @@ -17,7 +17,8 @@ use dao_voting::{ voting::Vote, }; -use crate::{contract::*, msg::*, state::PendingProposal}; +use crate::state::{Proposal, ProposalStatus}; +use crate::{contract::*, msg::*}; fn cw_dao_proposal_single_contract() -> Box> { let contract = ContractWrapper::new( @@ -62,7 +63,7 @@ fn get_default_proposal_module_instantiate( pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info, open_proposal_submission, extension: InstantiateExt { @@ -71,10 +72,12 @@ fn get_default_proposal_module_instantiate( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } } @@ -121,7 +124,7 @@ fn setup_default_test( let core_addr = instantiate_with_cw4_groups_governance( app, dao_proposal_single_id, - to_binary(&proposal_module_instantiate).unwrap(), + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -188,8 +191,8 @@ fn make_pre_proposal(app: &mut App, pre_propose: Addr, proposer: &str, funds: &[ ) .unwrap(); - // Query for pending proposal and return latest id - let mut pending: Vec = app + // Query for pending proposal and return latest id. + let mut pending: Vec = app .wrap() .query_wasm_smart( pre_propose, @@ -202,7 +205,7 @@ fn make_pre_proposal(app: &mut App, pre_propose: Addr, proposer: &str, funds: &[ ) .unwrap(); - // Return last item in list, id is first element of tuple + // Return last item in ascending list, id is first element of tuple pending.pop().unwrap().approval_id } @@ -871,7 +874,7 @@ fn test_pending_proposal_queries() { make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &coins(10, "ujuno")); // Query for individual proposal - let prop1: PendingProposal = app + let prop1: Proposal = app .wrap() .query_wasm_smart( pre_propose.clone(), @@ -881,9 +884,22 @@ fn test_pending_proposal_queries() { ) .unwrap(); assert_eq!(prop1.approval_id, 1); + assert_eq!(prop1.status, ProposalStatus::Pending {}); + + let prop1: Proposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::Proposal { id: 1 }, + }, + ) + .unwrap(); + assert_eq!(prop1.approval_id, 1); + assert_eq!(prop1.status, ProposalStatus::Pending {}); // Query for the pre-propose proposals - let pre_propose_props: Vec = app + let pre_propose_props: Vec = app .wrap() .query_wasm_smart( pre_propose.clone(), @@ -896,10 +912,10 @@ fn test_pending_proposal_queries() { ) .unwrap(); assert_eq!(pre_propose_props.len(), 2); - assert_eq!(pre_propose_props[0].approval_id, 2); + assert_eq!(pre_propose_props[0].approval_id, 1); // Query props in reverse - let reverse_pre_propose_props: Vec = app + let reverse_pre_propose_props: Vec = app .wrap() .query_wasm_smart( pre_propose, @@ -913,7 +929,149 @@ fn test_pending_proposal_queries() { .unwrap(); assert_eq!(reverse_pre_propose_props.len(), 2); - assert_eq!(reverse_pre_propose_props[0].approval_id, 1); + assert_eq!(reverse_pre_propose_props[0].approval_id, 2); +} + +#[test] +fn test_completed_proposal_queries() { + let mut app = App::default(); + + let DefaultTestSetup { + core_addr: _, + proposal_single: _, + pre_propose, + } = setup_default_test( + &mut app, + Some(UncheckedDepositInfo { + denom: DepositToken::Token { + denom: UncheckedDenom::Native("ujuno".to_string()), + }, + amount: Uint128::new(10), + refund_policy: DepositRefundPolicy::Always, + }), + false, + ); + + mint_natives(&mut app, "ekez", coins(20, "ujuno")); + let approve_id = make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &coins(10, "ujuno")); + let reject_id = make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &coins(10, "ujuno")); + + let is_pending: bool = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::IsPending { id: approve_id }, + }, + ) + .unwrap(); + assert!(is_pending); + + let created_approved_id = + approve_proposal(&mut app, pre_propose.clone(), "approver", approve_id); + reject_proposal(&mut app, pre_propose.clone(), "approver", reject_id); + + let is_pending: bool = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::IsPending { id: approve_id }, + }, + ) + .unwrap(); + assert!(!is_pending); + + // Query for individual proposals + let prop1: Proposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposal { id: approve_id }, + }, + ) + .unwrap(); + assert_eq!( + prop1.status, + ProposalStatus::Approved { + created_proposal_id: created_approved_id + } + ); + let prop1: Proposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::Proposal { id: approve_id }, + }, + ) + .unwrap(); + assert_eq!( + prop1.status, + ProposalStatus::Approved { + created_proposal_id: created_approved_id + } + ); + + let prop1_id: Option = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposalIdForCreatedProposalId { + id: created_approved_id, + }, + }, + ) + .unwrap(); + assert_eq!(prop1_id, Some(approve_id)); + + let prop2: Proposal = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposal { id: reject_id }, + }, + ) + .unwrap(); + assert_eq!(prop2.status, ProposalStatus::Rejected {}); + + // Query for the pre-propose proposals + let pre_propose_props: Vec = app + .wrap() + .query_wasm_smart( + pre_propose.clone(), + &QueryMsg::QueryExtension { + msg: QueryExt::CompletedProposals { + start_after: None, + limit: None, + }, + }, + ) + .unwrap(); + assert_eq!(pre_propose_props.len(), 2); + assert_eq!(pre_propose_props[0].approval_id, approve_id); + assert_eq!(pre_propose_props[1].approval_id, reject_id); + + // Query props in reverse + let reverse_pre_propose_props: Vec = app + .wrap() + .query_wasm_smart( + pre_propose, + &QueryMsg::QueryExtension { + msg: QueryExt::ReverseCompletedProposals { + start_before: None, + limit: None, + }, + }, + ) + .unwrap(); + + assert_eq!(reverse_pre_propose_props.len(), 2); + assert_eq!(reverse_pre_propose_props[0].approval_id, reject_id); + assert_eq!(reverse_pre_propose_props[1].approval_id, approve_id); } #[test] @@ -936,8 +1094,8 @@ fn test_set_version() { false, ); - let info: ContractVersion = from_slice( - &app.wrap() + let info: ContractVersion = from_json( + app.wrap() .query_wasm_raw(pre_propose, "contract_info".as_bytes()) .unwrap() .unwrap(), @@ -1185,7 +1343,7 @@ fn test_instantiate_with_zero_native_deposit() { pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: DepositToken::Token { denom: UncheckedDenom::Native("ujuno".to_string()), @@ -1200,10 +1358,12 @@ fn test_instantiate_with_zero_native_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } }; @@ -1211,7 +1371,7 @@ fn test_instantiate_with_zero_native_deposit() { instantiate_with_cw4_groups_governance( &mut app, dao_proposal_single_id, - to_binary(&proposal_module_instantiate).unwrap(), + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -1248,7 +1408,7 @@ fn test_instantiate_with_zero_cw20_deposit() { pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: DepositToken::Token { denom: UncheckedDenom::Cw20(cw20_addr.into_string()), @@ -1263,10 +1423,12 @@ fn test_instantiate_with_zero_cw20_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } }; @@ -1274,7 +1436,7 @@ fn test_instantiate_with_zero_cw20_deposit() { instantiate_with_cw4_groups_governance( &mut app, dao_proposal_single_id, - to_binary(&proposal_module_instantiate).unwrap(), + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), diff --git a/contracts/pre-propose/dao-pre-propose-approver/Cargo.toml b/contracts/pre-propose/dao-pre-propose-approver/Cargo.toml index 05bc50642..1f6e594fc 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/Cargo.toml +++ b/contracts/pre-propose/dao-pre-propose-approver/Cargo.toml @@ -35,7 +35,7 @@ cw4-group = { workspace = true } cw20 = { workspace = true } cw20-base = { workspace = true } dao-dao-core = { workspace = true } -dao-proposal-hooks = { workspace = true } +dao-hooks = { workspace = true } dao-proposal-single = { workspace = true, features = ["library"] } dao-testing = { workspace = true } dao-voting = { workspace = true } diff --git a/contracts/pre-propose/dao-pre-propose-approver/README.md b/contracts/pre-propose/dao-pre-propose-approver/README.md index 4d51bde8c..63bd72fd3 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/README.md +++ b/contracts/pre-propose/dao-pre-propose-approver/README.md @@ -1,5 +1,8 @@ # Proposal Approver Contract +[![dao-pre-propose-approver on crates.io](https://img.shields.io/crates/v/dao-pre-propose-approver.svg?logo=rust)](https://crates.io/crates/dao-pre-propose-approver) +[![docs.rs](https://img.shields.io/docsrs/dao-pre-propose-approver?logo=docsdotrs)](https://docs.rs/dao-pre-propose-approver/latest/dao_pre_propose_approver/) + This contract works in conjuction with `cwd-pre-propose-approval-single` and allows for automatically creating approval proposals when a proposal is submitted for approval. ## Approver Logic diff --git a/contracts/pre-propose/dao-pre-propose-approver/schema/dao-pre-propose-approver.json b/contracts/pre-propose/dao-pre-propose-approver/schema/dao-pre-propose-approver.json index dafcc738c..860039bcf 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/schema/dao-pre-propose-approver.json +++ b/contracts/pre-propose/dao-pre-propose-approver/schema/dao-pre-propose-approver.json @@ -1,6 +1,6 @@ { "contract_name": "dao-pre-propose-approver", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -302,6 +302,53 @@ "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" }, + "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 + } + ] + }, "Status": { "oneOf": [ { @@ -345,6 +392,43 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] + } + ] + }, + "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" } ] }, @@ -352,6 +436,10 @@ "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" + }, "UncheckedDenom": { "description": "A denom that has not been checked to confirm it points to a valid asset.", "oneOf": [ @@ -543,6 +631,52 @@ } }, "additionalProperties": false + }, + { + "type": "object", + "required": [ + "pre_propose_approval_id_for_approver_proposal_id" + ], + "properties": { + "pre_propose_approval_id_for_approver_proposal_id": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "approver_proposal_id_for_pre_propose_approval_id" + ], + "properties": { + "approver_proposal_id_for_pre_propose_approval_id": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] } diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs b/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs index c1e52ecd3..981cd30e6 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, + to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, WasmMsg, }; use cw2::set_contract_version; @@ -16,7 +16,9 @@ use dao_voting::status::Status; use crate::msg::{ BaseInstantiateMsg, ExecuteMsg, InstantiateMsg, ProposeMessageInternal, QueryExt, QueryMsg, }; -use crate::state::{PRE_PROPOSE_APPROVAL_CONTRACT, PROPOSAL_IDS}; +use crate::state::{ + PRE_PROPOSE_APPROVAL_CONTRACT, PRE_PROPOSE_ID_TO_PROPOSAL_ID, PROPOSAL_ID_TO_PRE_PROPOSE_ID, +}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-pre-propose-approver"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -50,18 +52,18 @@ pub fn instantiate( let addr = deps.api.addr_validate(&msg.pre_propose_approval_contract)?; PRE_PROPOSE_APPROVAL_CONTRACT.save(deps.storage, &addr)?; - Ok(resp.set_data(to_binary(&ModuleInstantiateCallback { + Ok(resp.set_data(to_json_binary(&ModuleInstantiateCallback { msgs: vec![ CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: addr.to_string(), - msg: to_binary(&PreProposeApprovalExecuteMsg::AddProposalSubmittedHook { + msg: to_json_binary(&PreProposeApprovalExecuteMsg::AddProposalSubmittedHook { address: env.contract.address.to_string(), })?, funds: vec![], }), CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: addr.to_string(), - msg: to_binary(&PreProposeApprovalExecuteMsg::Extension { + msg: to_json_binary(&PreProposeApprovalExecuteMsg::Extension { msg: ApprovalExt::UpdateApprover { address: env.contract.address.to_string(), }, @@ -124,11 +126,12 @@ pub fn execute_propose( &proposal_module, &dao_interface::proposal::Query::NextProposalId {}, )?; - PROPOSAL_IDS.save(deps.storage, proposal_id, &pre_propose_id)?; + PROPOSAL_ID_TO_PRE_PROPOSE_ID.save(deps.storage, proposal_id, &pre_propose_id)?; + PRE_PROPOSE_ID_TO_PROPOSAL_ID.save(deps.storage, pre_propose_id, &proposal_id)?; let propose_messsage = WasmMsg::Execute { contract_addr: proposal_module.into_string(), - msg: to_binary(&sanitized_msg)?, + msg: to_json_binary(&sanitized_msg)?, funds: vec![], }; Ok(Response::default().add_message(propose_messsage)) @@ -147,7 +150,7 @@ pub fn execute_proposal_completed( } // Get approval pre-propose id - let pre_propose_id = PROPOSAL_IDS.load(deps.storage, proposal_id)?; + let pre_propose_id = PROPOSAL_ID_TO_PRE_PROPOSE_ID.load(deps.storage, proposal_id)?; // Get approval contract address let approval_contract = PRE_PROPOSE_APPROVAL_CONTRACT.load(deps.storage)?; @@ -156,14 +159,14 @@ pub fn execute_proposal_completed( let msg = match new_status { Status::Closed => Some(WasmMsg::Execute { contract_addr: approval_contract.into_string(), - msg: to_binary(&PreProposeApprovalExecuteMsg::Extension { + msg: to_json_binary(&PreProposeApprovalExecuteMsg::Extension { msg: ApprovalExt::Reject { id: pre_propose_id }, })?, funds: vec![], }), Status::Executed => Some(WasmMsg::Execute { contract_addr: approval_contract.into_string(), - msg: to_binary(&PreProposeApprovalExecuteMsg::Extension { + msg: to_json_binary(&PreProposeApprovalExecuteMsg::Extension { msg: ApprovalExt::Approve { id: pre_propose_id }, })?, funds: vec![], @@ -177,7 +180,7 @@ pub fn execute_proposal_completed( .add_message(msg) .add_attribute("method", "execute_proposal_completed_hook") .add_attribute("proposal", proposal_id.to_string())), - None => Err(PreProposeError::NotClosedOrExecuted { status: new_status }), + None => Err(PreProposeError::NotCompleted { status: new_status }), } } @@ -186,7 +189,13 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryExtension { msg } => match msg { QueryExt::PreProposeApprovalContract {} => { - to_binary(&PRE_PROPOSE_APPROVAL_CONTRACT.load(deps.storage)?) + to_json_binary(&PRE_PROPOSE_APPROVAL_CONTRACT.load(deps.storage)?) + } + QueryExt::PreProposeApprovalIdForApproverProposalId { id } => { + to_json_binary(&PROPOSAL_ID_TO_PRE_PROPOSE_ID.may_load(deps.storage, id)?) + } + QueryExt::ApproverProposalIdForPreProposeApprovalId { id } => { + to_json_binary(&PRE_PROPOSE_ID_TO_PROPOSAL_ID.may_load(deps.storage, id)?) } }, _ => PrePropose::default().query(deps, env, msg), diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/msg.rs b/contracts/pre-propose/dao-pre-propose-approver/src/msg.rs index 249b45caa..a372cc22a 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/msg.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/msg.rs @@ -15,6 +15,10 @@ pub struct InstantiateMsg { pub enum QueryExt { #[returns(cosmwasm_std::Addr)] PreProposeApprovalContract {}, + #[returns(::std::option::Option)] + PreProposeApprovalIdForApproverProposalId { id: u64 }, + #[returns(::std::option::Option)] + ApproverProposalIdForPreProposeApprovalId { id: u64 }, } pub type BaseInstantiateMsg = InstantiateBase; diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/state.rs b/contracts/pre-propose/dao-pre-propose-approver/src/state.rs index b39187bf6..b09012e05 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/state.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/state.rs @@ -4,4 +4,6 @@ use cw_storage_plus::{Item, Map}; // Stores the address of the pre-propose approval contract pub const PRE_PROPOSE_APPROVAL_CONTRACT: Item = Item::new("pre_propose_approval_contract"); // Maps proposal ids to pre-propose ids -pub const PROPOSAL_IDS: Map = Map::new("proposal_ids"); +pub const PROPOSAL_ID_TO_PRE_PROPOSE_ID: Map = Map::new("proposal_to_pre_propose"); +// Maps pre-propose ids to proposal ids +pub const PRE_PROPOSE_ID_TO_PROPOSAL_ID: Map = Map::new("pre_propose_to_proposal"); diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs index 5aea7b2bc..630fcff33 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs @@ -1,9 +1,9 @@ -use cosmwasm_std::{coins, from_slice, to_binary, Addr, Coin, Empty, Uint128}; -use cps::query::{ProposalListResponse, ProposalResponse}; +use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Coin, Empty, Uint128}; use cw2::ContractVersion; use cw20::Cw20Coin; use cw_denom::UncheckedDenom; use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor}; +use dps::query::{ProposalListResponse, ProposalResponse}; use dao_interface::state::ProposalModule; use dao_interface::state::{Admin, ModuleInstantiateInfo}; @@ -11,10 +11,10 @@ use dao_pre_propose_approval_single::{ msg::{ ExecuteExt, ExecuteMsg, InstantiateExt, InstantiateMsg, ProposeMessage, QueryExt, QueryMsg, }, - state::PendingProposal, + state::Proposal, }; use dao_pre_propose_base::{error::PreProposeError, msg::DepositInfoResponse, state::Config}; -use dao_proposal_single as cps; +use dao_proposal_single as dps; use dao_testing::helpers::instantiate_with_cw4_groups_governance; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, @@ -26,18 +26,19 @@ use dao_voting::{ use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; use crate::msg::InstantiateMsg as ApproverInstantiateMsg; +use crate::msg::{QueryExt as ApproverQueryExt, QueryMsg as ApproverQueryMsg}; // The approver dao contract is the 6th contract instantiated const APPROVER: &str = "contract6"; fn cw_dao_proposal_single_contract() -> Box> { let contract = ContractWrapper::new( - cps::contract::execute, - cps::contract::instantiate, - cps::contract::query, + dps::contract::execute, + dps::contract::instantiate, + dps::contract::query, ) - .with_migrate(cps::contract::migrate) - .with_reply(cps::contract::reply); + .with_migrate(dps::contract::migrate) + .with_reply(dps::contract::reply); Box::new(contract) } @@ -72,10 +73,10 @@ fn get_proposal_module_approval_single_instantiate( app: &mut App, deposit_info: Option, open_proposal_submission: bool, -) -> cps::msg::InstantiateMsg { +) -> dps::msg::InstantiateMsg { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); - cps::msg::InstantiateMsg { + dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, }, @@ -86,7 +87,7 @@ fn get_proposal_module_approval_single_instantiate( pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info, open_proposal_submission, extension: InstantiateExt { @@ -95,10 +96,12 @@ fn get_proposal_module_approval_single_instantiate( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module, needs supervision".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } } @@ -107,10 +110,10 @@ fn get_proposal_module_approver_instantiate( _deposit_info: Option, _open_proposal_submission: bool, pre_propose_approval_contract: String, -) -> cps::msg::InstantiateMsg { +) -> dps::msg::InstantiateMsg { let pre_propose_id = app.store_code(pre_propose_approver_contract()); - cps::msg::InstantiateMsg { + dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, }, @@ -121,15 +124,17 @@ fn get_proposal_module_approver_instantiate( pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&ApproverInstantiateMsg { + msg: to_json_binary(&ApproverInstantiateMsg { pre_propose_approval_contract, }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "approver module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } } @@ -171,7 +176,7 @@ fn setup_default_test( deposit_info: Option, open_proposal_submission: bool, ) -> DefaultTestSetup { - let cps_id = app.store_code(cw_dao_proposal_single_contract()); + let dps_id = app.store_code(cw_dao_proposal_single_contract()); // Instantiate SubDAO with pre-propose-approval-single let proposal_module_instantiate = get_proposal_module_approval_single_instantiate( @@ -181,8 +186,8 @@ fn setup_default_test( ); let core_addr = instantiate_with_cw4_groups_governance( app, - cps_id, - to_binary(&proposal_module_instantiate).unwrap(), + dps_id, + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -212,7 +217,7 @@ fn setup_default_test( .wrap() .query_wasm_smart( proposal_single.clone(), - &cps::msg::QueryMsg::ProposalCreationPolicy {}, + &dps::msg::QueryMsg::ProposalCreationPolicy {}, ) .unwrap(); let pre_propose = match proposal_creation_policy { @@ -235,8 +240,8 @@ fn setup_default_test( let _approver_core_addr = instantiate_with_cw4_groups_governance( app, - cps_id, - to_binary(&proposal_module_instantiate).unwrap(), + dps_id, + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -266,7 +271,7 @@ fn setup_default_test( .wrap() .query_wasm_smart( proposal_single_approver.clone(), - &cps::msg::QueryMsg::ProposalCreationPolicy {}, + &dps::msg::QueryMsg::ProposalCreationPolicy {}, ) .unwrap(); let pre_propose_approver = match proposal_creation_policy { @@ -308,7 +313,7 @@ fn make_pre_proposal(app: &mut App, pre_propose: Addr, proposer: &str, funds: &[ .unwrap(); // Query for pending proposal and return latest id - let mut pending: Vec = app + let mut pending: Vec = app .wrap() .query_wasm_smart( pre_propose, @@ -369,7 +374,7 @@ fn vote(app: &mut App, module: Addr, sender: &str, id: u64, position: Vote) -> S app.execute_contract( Addr::unchecked(sender), module.clone(), - &cps::msg::ExecuteMsg::Vote { + &dps::msg::ExecuteMsg::Vote { proposal_id: id, vote: position, rationale: None, @@ -380,7 +385,7 @@ fn vote(app: &mut App, module: Addr, sender: &str, id: u64, position: Vote) -> S let proposal: ProposalResponse = app .wrap() - .query_wasm_smart(module, &cps::msg::QueryMsg::Proposal { proposal_id: id }) + .query_wasm_smart(module, &dps::msg::QueryMsg::Proposal { proposal_id: id }) .unwrap(); proposal.proposal.status @@ -414,7 +419,7 @@ fn get_proposals(app: &App, module: Addr) -> ProposalListResponse { app.wrap() .query_wasm_smart( module, - &cps::msg::QueryMsg::ListProposals { + &dps::msg::QueryMsg::ListProposals { start_after: None, limit: None, }, @@ -428,7 +433,7 @@ fn get_latest_proposal_id(app: &App, module: Addr) -> u64 { .wrap() .query_wasm_smart( module, - &cps::msg::QueryMsg::ListProposals { + &dps::msg::QueryMsg::ListProposals { start_after: None, limit: None, }, @@ -510,7 +515,7 @@ fn close_proposal(app: &mut App, module: Addr, sender: &str, proposal_id: u64) { app.execute_contract( Addr::unchecked(sender), module, - &cps::msg::ExecuteMsg::Close { proposal_id }, + &dps::msg::ExecuteMsg::Close { proposal_id }, &[], ) .unwrap(); @@ -520,7 +525,7 @@ fn execute_proposal(app: &mut App, module: Addr, sender: &str, proposal_id: u64) app.execute_contract( Addr::unchecked(sender), module, - &cps::msg::ExecuteMsg::Execute { proposal_id }, + &dps::msg::ExecuteMsg::Execute { proposal_id }, &[], ) .unwrap(); @@ -1075,8 +1080,8 @@ fn test_set_version() { false, ); - let info: ContractVersion = from_slice( - &app.wrap() + let info: ContractVersion = from_json( + app.wrap() .query_wasm_raw(pre_propose_approver, "contract_info".as_bytes()) .unwrap() .unwrap(), @@ -1232,7 +1237,7 @@ fn test_propose_open_proposal_submission() { pre_propose, _approver_core_addr: _, proposal_single_approver, - pre_propose_approver: _, + pre_propose_approver, } = setup_default_test( &mut app, Some(UncheckedDepositInfo { @@ -1247,11 +1252,36 @@ fn test_propose_open_proposal_submission() { // Non-member proposes. mint_natives(&mut app, "nonmember", coins(10, "ujuno")); - let _pre_propose_id = - make_pre_proposal(&mut app, pre_propose, "nonmember", &coins(10, "ujuno")); + let pre_propose_id = make_pre_proposal(&mut app, pre_propose, "nonmember", &coins(10, "ujuno")); - // Approver DAO votes to approves let approver_prop_id = get_latest_proposal_id(&app, proposal_single_approver.clone()); + let pre_propose_id_from_proposal: u64 = app + .wrap() + .query_wasm_smart( + pre_propose_approver.clone(), + &ApproverQueryMsg::QueryExtension { + msg: ApproverQueryExt::PreProposeApprovalIdForApproverProposalId { + id: approver_prop_id, + }, + }, + ) + .unwrap(); + assert_eq!(pre_propose_id_from_proposal, pre_propose_id); + + let proposal_id_from_pre_propose: u64 = app + .wrap() + .query_wasm_smart( + pre_propose_approver.clone(), + &ApproverQueryMsg::QueryExtension { + msg: ApproverQueryExt::ApproverProposalIdForPreProposeApprovalId { + id: pre_propose_id, + }, + }, + ) + .unwrap(); + assert_eq!(proposal_id_from_pre_propose, approver_prop_id); + + // Approver DAO votes to approves approve_proposal(&mut app, proposal_single_approver, "ekez", approver_prop_id); let id = get_latest_proposal_id(&app, proposal_single.clone()); @@ -1517,7 +1547,7 @@ fn test_withdraw() { .wrap() .query_wasm_smart( proposal_single.clone(), - &cps::msg::QueryMsg::ProposalCreationPolicy {}, + &dps::msg::QueryMsg::ProposalCreationPolicy {}, ) .unwrap(); diff --git a/contracts/pre-propose/dao-pre-propose-multiple/Cargo.toml b/contracts/pre-propose/dao-pre-propose-multiple/Cargo.toml index 7352139b4..670c0f281 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/Cargo.toml +++ b/contracts/pre-propose/dao-pre-propose-multiple/Cargo.toml @@ -37,4 +37,4 @@ dao-voting = { workspace = true } cw-denom = { workspace = true } dao-interface = { workspace = true } dao-testing = { workspace = true } -dao-proposal-hooks = { workspace = true } +dao-hooks = { workspace = true } diff --git a/contracts/pre-propose/dao-pre-propose-multiple/README.md b/contracts/pre-propose/dao-pre-propose-multiple/README.md index b599dd3a3..b8971c05e 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/README.md +++ b/contracts/pre-propose/dao-pre-propose-multiple/README.md @@ -1,5 +1,8 @@ # Multiple choice proposal deposit contract +[![dao-pre-propose-multiple on crates.io](https://img.shields.io/crates/v/dao-pre-propose-multiple.svg?logo=rust)](https://crates.io/crates/dao-pre-propose-multiple) +[![docs.rs](https://img.shields.io/docsrs/dao-pre-propose-multiple?logo=docsdotrs)](https://docs.rs/dao-pre-propose-multiple/latest/dao_pre_propose_multiple/) + This is a pre-propose module that manages proposal deposits for the `dao-proposal-multiple` proposal module. diff --git a/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json b/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json index d0fb4ab83..2cd69baa6 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json +++ b/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json @@ -1,6 +1,6 @@ { "contract_name": "dao-pre-propose-multiple", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -669,6 +669,53 @@ "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" }, + "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 + } + ] + }, "GovMsg": { "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ @@ -734,7 +781,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -852,7 +899,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -1059,6 +1106,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -1228,7 +1304,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs index e256a1f47..4a8825a5c 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{coins, from_slice, to_binary, Addr, Coin, Decimal, Empty, Uint128}; +use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Coin, Decimal, Empty, Uint128}; use cpm::query::ProposalResponse; use cw2::ContractVersion; use cw20::Cw20Coin; @@ -65,17 +65,19 @@ fn get_default_proposal_module_instantiate( pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info, open_proposal_submission, extension: Empty::default(), }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } } @@ -121,7 +123,7 @@ fn setup_default_test( let core_addr = instantiate_with_cw4_groups_governance( app, cpm_id, - to_binary(&proposal_module_instantiate).unwrap(), + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -799,8 +801,8 @@ fn test_set_version() { false, ); - let info: ContractVersion = from_slice( - &app.wrap() + let info: ContractVersion = from_json( + app.wrap() .query_wasm_raw(pre_propose, "contract_info".as_bytes()) .unwrap() .unwrap(), @@ -1021,7 +1023,7 @@ fn test_execute_extension_does_nothing() { assert_eq!(res.events[0].attributes.len(), 1); assert_eq!( res.events[0].attributes[0].key, - "_contract_addr".to_string() + "_contract_address".to_string() ) } @@ -1046,7 +1048,7 @@ fn test_instantiate_with_zero_native_deposit() { pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: DepositToken::Token { denom: UncheckedDenom::Native("ujuno".to_string()), @@ -1059,10 +1061,12 @@ fn test_instantiate_with_zero_native_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } }; @@ -1070,7 +1074,7 @@ fn test_instantiate_with_zero_native_deposit() { instantiate_with_cw4_groups_governance( &mut app, cpm_id, - to_binary(&proposal_module_instantiate).unwrap(), + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -1107,7 +1111,7 @@ fn test_instantiate_with_zero_cw20_deposit() { pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: DepositToken::Token { denom: UncheckedDenom::Cw20(cw20_addr.into_string()), @@ -1120,10 +1124,12 @@ fn test_instantiate_with_zero_cw20_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } }; @@ -1131,7 +1137,7 @@ fn test_instantiate_with_zero_cw20_deposit() { instantiate_with_cw4_groups_governance( &mut app, cpm_id, - to_binary(&proposal_module_instantiate).unwrap(), + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), diff --git a/contracts/pre-propose/dao-pre-propose-single/Cargo.toml b/contracts/pre-propose/dao-pre-propose-single/Cargo.toml index c7d1dd39a..ac1fea9d4 100644 --- a/contracts/pre-propose/dao-pre-propose-single/Cargo.toml +++ b/contracts/pre-propose/dao-pre-propose-single/Cargo.toml @@ -36,6 +36,6 @@ dao-voting = { workspace = true } cw-denom = { workspace = true } dao-interface = { workspace = true } dao-testing = { workspace = true } -dao-proposal-hooks = { workspace = true } +dao-hooks = { workspace = true } dao-proposal-single = { workspace = true } cw-hooks = { workspace = true } diff --git a/contracts/pre-propose/dao-pre-propose-single/README.md b/contracts/pre-propose/dao-pre-propose-single/README.md index 5028764b5..9c9d39752 100644 --- a/contracts/pre-propose/dao-pre-propose-single/README.md +++ b/contracts/pre-propose/dao-pre-propose-single/README.md @@ -1,5 +1,8 @@ # Single choice proposal deposit contract +[![dao-pre-propose-single on crates.io](https://img.shields.io/crates/v/dao-pre-propose-single.svg?logo=rust)](https://crates.io/crates/dao-pre-propose-single) +[![docs.rs](https://img.shields.io/docsrs/dao-pre-propose-single?logo=docsdotrs)](https://docs.rs/dao-pre-propose-single/latest/dao_pre_propose_single/) + This is a pre-propose module that manages proposal deposits for the `cwd-proposal-single` proposal module. diff --git a/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json b/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json index 9cf71147a..8a9eff35a 100644 --- a/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json +++ b/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json @@ -1,6 +1,6 @@ { "contract_name": "dao-pre-propose-single", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -669,6 +669,53 @@ "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" }, + "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 + } + ] + }, "GovMsg": { "description": "This message type allows the contract interact with the [x/gov] module in order to cast votes.\n\n[x/gov]: https://github.com/cosmos/cosmos-sdk/tree/v0.45.12/x/gov\n\n## Examples\n\nCast a simple vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); use cosmwasm_std::{GovMsg, VoteOption};\n\n#[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::Vote { proposal_id: 4, vote: VoteOption::Yes, })) } ```\n\nCast a weighted vote:\n\n``` # use cosmwasm_std::{ # HexBinary, # Storage, Api, Querier, DepsMut, Deps, entry_point, Env, StdError, MessageInfo, # Response, QueryResponse, # }; # type ExecuteMsg = (); # #[cfg(feature = \"cosmwasm_1_2\")] use cosmwasm_std::{Decimal, GovMsg, VoteOption, WeightedVoteOption};\n\n# #[cfg(feature = \"cosmwasm_1_2\")] #[entry_point] pub fn execute( deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { // ... Ok(Response::new().add_message(GovMsg::VoteWeighted { proposal_id: 4, options: vec![ WeightedVoteOption { option: VoteOption::Yes, weight: Decimal::percent(65), }, WeightedVoteOption { option: VoteOption::Abstain, weight: Decimal::percent(35), }, ], })) } ```", "oneOf": [ @@ -734,7 +781,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -852,7 +899,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -1023,6 +1070,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -1192,7 +1268,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { diff --git a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs index 6a8b48e9f..d766ce5cc 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs @@ -1,5 +1,4 @@ -use cosmwasm_std::{coins, from_slice, to_binary, Addr, Coin, Empty, Uint128}; -use cps::query::ProposalResponse; +use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Coin, Empty, Uint128}; use cw2::ContractVersion; use cw20::Cw20Coin; use cw_denom::UncheckedDenom; @@ -8,7 +7,7 @@ use cw_utils::Duration; use dao_interface::state::ProposalModule; use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_pre_propose_base::{error::PreProposeError, msg::DepositInfoResponse, state::Config}; -use dao_proposal_single as cps; +use dao_proposal_single as dps; use dao_testing::helpers::instantiate_with_cw4_groups_governance; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, @@ -17,17 +16,18 @@ use dao_voting::{ threshold::{PercentageThreshold, Threshold}, voting::Vote, }; +use dps::query::ProposalResponse; use crate::contract::*; fn cw_dao_proposal_single_contract() -> Box> { let contract = ContractWrapper::new( - cps::contract::execute, - cps::contract::instantiate, - cps::contract::query, + dps::contract::execute, + dps::contract::instantiate, + dps::contract::query, ) - .with_migrate(cps::contract::migrate) - .with_reply(cps::contract::reply); + .with_migrate(dps::contract::migrate) + .with_reply(dps::contract::reply); Box::new(contract) } @@ -49,10 +49,10 @@ fn get_default_proposal_module_instantiate( app: &mut App, deposit_info: Option, open_proposal_submission: bool, -) -> cps::msg::InstantiateMsg { +) -> dps::msg::InstantiateMsg { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); - cps::msg::InstantiateMsg { + dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, }, @@ -63,17 +63,19 @@ fn get_default_proposal_module_instantiate( pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info, open_proposal_submission, extension: Empty::default(), }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } } @@ -111,15 +113,15 @@ fn setup_default_test( deposit_info: Option, open_proposal_submission: bool, ) -> DefaultTestSetup { - let cps_id = app.store_code(cw_dao_proposal_single_contract()); + let dps_id = app.store_code(cw_dao_proposal_single_contract()); let proposal_module_instantiate = get_default_proposal_module_instantiate(app, deposit_info, open_proposal_submission); let core_addr = instantiate_with_cw4_groups_governance( app, - cps_id, - to_binary(&proposal_module_instantiate).unwrap(), + dps_id, + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -148,7 +150,7 @@ fn setup_default_test( .wrap() .query_wasm_smart( proposal_single.clone(), - &cps::msg::QueryMsg::ProposalCreationPolicy {}, + &dps::msg::QueryMsg::ProposalCreationPolicy {}, ) .unwrap(); @@ -194,7 +196,7 @@ fn make_proposal( let id: u64 = app .wrap() - .query_wasm_smart(&proposal_module, &cps::msg::QueryMsg::NextProposalId {}) + .query_wasm_smart(&proposal_module, &dps::msg::QueryMsg::NextProposalId {}) .unwrap(); let id = id - 1; @@ -202,7 +204,7 @@ fn make_proposal( .wrap() .query_wasm_smart( proposal_module, - &cps::msg::QueryMsg::Proposal { proposal_id: id }, + &dps::msg::QueryMsg::Proposal { proposal_id: id }, ) .unwrap(); @@ -282,7 +284,7 @@ fn vote(app: &mut App, module: Addr, sender: &str, id: u64, position: Vote) -> S app.execute_contract( Addr::unchecked(sender), module.clone(), - &cps::msg::ExecuteMsg::Vote { + &dps::msg::ExecuteMsg::Vote { rationale: None, proposal_id: id, vote: position, @@ -293,7 +295,7 @@ fn vote(app: &mut App, module: Addr, sender: &str, id: u64, position: Vote) -> S let proposal: ProposalResponse = app .wrap() - .query_wasm_smart(module, &cps::msg::QueryMsg::Proposal { proposal_id: id }) + .query_wasm_smart(module, &dps::msg::QueryMsg::Proposal { proposal_id: id }) .unwrap(); proposal.proposal.status @@ -402,7 +404,7 @@ fn close_proposal(app: &mut App, module: Addr, sender: &str, proposal_id: u64) { app.execute_contract( Addr::unchecked(sender), module, - &cps::msg::ExecuteMsg::Close { proposal_id }, + &dps::msg::ExecuteMsg::Close { proposal_id }, &[], ) .unwrap(); @@ -412,7 +414,7 @@ fn execute_proposal(app: &mut App, module: Addr, sender: &str, proposal_id: u64) app.execute_contract( Addr::unchecked(sender), module, - &cps::msg::ExecuteMsg::Execute { proposal_id }, + &dps::msg::ExecuteMsg::Execute { proposal_id }, &[], ) .unwrap(); @@ -765,8 +767,8 @@ fn test_set_version() { false, ); - let info: ContractVersion = from_slice( - &app.wrap() + let info: ContractVersion = from_json( + app.wrap() .query_wasm_raw(pre_propose, "contract_info".as_bytes()) .unwrap() .unwrap(), @@ -957,7 +959,7 @@ fn test_execute_extension_does_nothing() { assert_eq!(res.events[0].attributes.len(), 1); assert_eq!( res.events[0].attributes[0].key, - "_contract_addr".to_string() + "_contract_address".to_string() ) } @@ -966,12 +968,12 @@ fn test_execute_extension_does_nothing() { fn test_instantiate_with_zero_native_deposit() { let mut app = App::default(); - let cps_id = app.store_code(cw_dao_proposal_single_contract()); + let dps_id = app.store_code(cw_dao_proposal_single_contract()); let proposal_module_instantiate = { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); - cps::msg::InstantiateMsg { + dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, }, @@ -982,7 +984,7 @@ fn test_instantiate_with_zero_native_deposit() { pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: DepositToken::Token { denom: UncheckedDenom::Native("ujuno".to_string()), @@ -995,18 +997,20 @@ fn test_instantiate_with_zero_native_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } }; // Should panic. instantiate_with_cw4_groups_governance( &mut app, - cps_id, - to_binary(&proposal_module_instantiate).unwrap(), + dps_id, + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -1027,12 +1031,12 @@ fn test_instantiate_with_zero_cw20_deposit() { let cw20_addr = instantiate_cw20_base_default(&mut app); - let cps_id = app.store_code(cw_dao_proposal_single_contract()); + let dps_id = app.store_code(cw_dao_proposal_single_contract()); let proposal_module_instantiate = { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); - cps::msg::InstantiateMsg { + dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, }, @@ -1043,7 +1047,7 @@ fn test_instantiate_with_zero_cw20_deposit() { pre_propose_info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&InstantiateMsg { + msg: to_json_binary(&InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: DepositToken::Token { denom: UncheckedDenom::Cw20(cw20_addr.into_string()), @@ -1056,18 +1060,20 @@ fn test_instantiate_with_zero_cw20_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, close_proposal_on_execution_failure: false, + veto: None, } }; // Should panic. instantiate_with_cw4_groups_governance( &mut app, - cps_id, - to_binary(&proposal_module_instantiate).unwrap(), + dps_id, + to_json_binary(&proposal_module_instantiate).unwrap(), Some(vec![ cw20::Cw20Coin { address: "ekez".to_string(), @@ -1311,7 +1317,7 @@ fn test_withdraw() { .wrap() .query_wasm_smart( proposal_single.clone(), - &cps::msg::QueryMsg::ProposalCreationPolicy {}, + &dps::msg::QueryMsg::ProposalCreationPolicy {}, ) .unwrap(); 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-condorcet/README.md b/contracts/proposal/dao-proposal-condorcet/README.md index ab9e1bca4..703034a48 100644 --- a/contracts/proposal/dao-proposal-condorcet/README.md +++ b/contracts/proposal/dao-proposal-condorcet/README.md @@ -1,3 +1,8 @@ +# dao-proposal-condorcet + +[![dao-proposal-condorcet on crates.io](https://img.shields.io/crates/v/dao-proposal-condorcet.svg?logo=rust)](https://crates.io/crates/dao-proposal-condorcet) +[![docs.rs](https://img.shields.io/docsrs/dao-proposal-condorcet?logo=docsdotrs)](https://docs.rs/dao-proposal-condorcet/latest/dao_proposal_condorcet/) + This is a DAO DAO proposal module which implements The Condorcet Method. diff --git a/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json b/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json index 75f0440a9..1459cbdfa 100644 --- a/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json +++ b/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json @@ -1,6 +1,6 @@ { "contract_name": "dao-proposal-condorcet", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -73,7 +73,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -586,7 +586,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -704,7 +704,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -712,7 +712,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -956,7 +956,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -1217,7 +1217,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -1727,7 +1727,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -1845,7 +1845,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -1875,7 +1875,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -2250,7 +2250,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { diff --git a/contracts/proposal/dao-proposal-condorcet/src/contract.rs b/contracts/proposal/dao-proposal-condorcet/src/contract.rs index b436b76e4..f6b5406f3 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/contract.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, + to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, }; use cw2::set_contract_version; @@ -252,12 +252,12 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { let mut proposal = PROPOSAL.load(deps.storage, id)?; let tally = TALLY.load(deps.storage, id)?; proposal.update_status(&env.block, &tally); - to_binary(&ProposalResponse { proposal, tally }) + to_json_binary(&ProposalResponse { proposal, tally }) } - QueryMsg::Config {} => to_binary(&CONFIG.load(deps.storage)?), - QueryMsg::NextProposalId {} => to_binary(&next_proposal_id(deps.storage)?), - QueryMsg::Dao {} => to_binary(&DAO.load(deps.storage)?), - QueryMsg::Info {} => to_binary(&dao_interface::voting::InfoResponse { + QueryMsg::Config {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::NextProposalId {} => to_json_binary(&next_proposal_id(deps.storage)?), + QueryMsg::Dao {} => to_json_binary(&DAO.load(deps.storage)?), + QueryMsg::Info {} => to_json_binary(&dao_interface::voting::InfoResponse { info: cw2::get_contract_version(deps.storage)?, }), } @@ -271,8 +271,10 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result unimplemented!("pre-propose and hooks not yet supported"), } diff --git a/contracts/proposal/dao-proposal-condorcet/src/proposal.rs b/contracts/proposal/dao-proposal-condorcet/src/proposal.rs index 155b90cc2..bd2f82486 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/proposal.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/proposal.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{to_binary, Addr, BlockInfo, StdResult, SubMsg, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, BlockInfo, StdResult, SubMsg, Uint128, WasmMsg}; use cw_utils::Expiration; use dao_voting::{ reply::mask_proposal_execution_proposal_id, threshold::PercentageThreshold, @@ -161,7 +161,7 @@ impl Proposal { let msgs = self.choices[winner as usize].msgs.clone(); let core_exec = WasmMsg::Execute { contract_addr: dao.into_string(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs })?, + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs })?, funds: vec![], }; Ok(if self.close_on_execution_failure { diff --git a/contracts/proposal/dao-proposal-condorcet/src/testing/proposals.rs b/contracts/proposal/dao-proposal-condorcet/src/testing/proposals.rs index 151790ef8..6c39f4b6e 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/testing/proposals.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/testing/proposals.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_binary, WasmMsg}; +use cosmwasm_std::{to_json_binary, WasmMsg}; use cw_utils::Duration; use crate::{ @@ -171,7 +171,7 @@ fn test_proposal_set_config() { suite.sender(), vec![vec![WasmMsg::Execute { contract_addr: suite.condorcet.to_string(), - msg: to_binary(&ExecuteMsg::SetConfig(UncheckedConfig { + msg: to_json_binary(&ExecuteMsg::SetConfig(UncheckedConfig { quorum: config.quorum, voting_period: config.voting_period, min_voting_period: None, @@ -194,7 +194,7 @@ fn test_proposal_set_config() { suite.sender(), vec![vec![WasmMsg::Execute { contract_addr: suite.condorcet.to_string(), - msg: to_binary(&ExecuteMsg::SetConfig(UncheckedConfig { + msg: to_json_binary(&ExecuteMsg::SetConfig(UncheckedConfig { quorum: config.quorum, voting_period: config.voting_period, min_voting_period: Some(Duration::Height(10)), diff --git a/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs b/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs index d3c4779b3..79d18154f 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{coins, to_binary, Addr, BankMsg, CosmosMsg, Decimal}; +use cosmwasm_std::{coins, to_json_binary, Addr, BankMsg, CosmosMsg, Decimal}; use cw_multi_test::{next_block, App, Executor}; use cw_utils::Duration; use dao_interface::{ @@ -9,6 +9,7 @@ use dao_testing::contracts::{ cw4_group_contract, dao_dao_contract, dao_voting_cw4_contract, proposal_condorcet_contract, }; use dao_voting::threshold::PercentageThreshold; +use dao_voting_cw4::msg::GroupContract; use crate::{ config::{Config, UncheckedConfig}, @@ -87,18 +88,22 @@ impl SuiteBuilder { automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: cw4_voting_id, - msg: to_binary(&dao_voting_cw4::msg::InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members, + msg: to_json_binary(&dao_voting_cw4::msg::InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members, + }, }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: condorcet_id, - msg: to_binary(&self.instantiate).unwrap(), + msg: to_json_binary(&self.instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "condorcet module".to_string(), }], initial_items: None, diff --git a/contracts/proposal/dao-proposal-multiple/Cargo.toml b/contracts/proposal/dao-proposal-multiple/Cargo.toml index 94fe6cda0..92cc9004a 100644 --- a/contracts/proposal/dao-proposal-multiple/Cargo.toml +++ b/contracts/proposal/dao-proposal-multiple/Cargo.toml @@ -32,23 +32,23 @@ cw-utils = { workspace = true } cw2 = { workspace = true } cw20 = { workspace = true } cw3 = { workspace = true } -thiserror = { version = "1.0" } +thiserror = { workspace = true } dao-dao-macros = { workspace = true } dao-pre-propose-base = { workspace = true } dao-interface = { workspace = true } dao-voting = { workspace = true } cw-hooks = { workspace = true } -dao-proposal-hooks = { workspace = true } -dao-vote-hooks = { workspace = true } +dao-hooks = { workspace = true } dao-pre-propose-multiple = { workspace = true } voting-v1 = { workspace = true } [dev-dependencies] +anyhow = { workspace = true } cw-multi-test = { workspace = true } dao-voting-cw4 = { workspace = true } dao-voting-cw20-balance = { workspace = true } dao-voting-cw20-staked = { workspace = true } -dao-voting-native-staked = { workspace = true } +dao-voting-token-staked = { workspace = true } dao-voting-cw721-staked = { workspace = true } cw-denom = { workspace = true } dao-testing = { workspace = true } diff --git a/contracts/proposal/dao-proposal-multiple/README.md b/contracts/proposal/dao-proposal-multiple/README.md index 72520af7e..0d6f2c122 100644 --- a/contracts/proposal/dao-proposal-multiple/README.md +++ b/contracts/proposal/dao-proposal-multiple/README.md @@ -1,4 +1,7 @@ -## dao-proposal-multiple +# dao-proposal-multiple + +[![dao-proposal-multiple on crates.io](https://img.shields.io/crates/v/dao-proposal-multiple.svg?logo=rust)](https://crates.io/crates/dao-proposal-multiple) +[![docs.rs](https://img.shields.io/docsrs/dao-proposal-multiple?logo=docsdotrs)](https://docs.rs/dao-proposal-multiple/latest/dao_proposal_multiple/) A proposal module for a DAO DAO DAO which allows the users to select their voting choice(s) from an array of `MultipleChoiceOption`. @@ -49,3 +52,55 @@ handling a hook. The proposals may be configured to allow revoting. In such cases, users are able to change their vote as long as the proposal is still open. Revoting for the currently cast option will return an error. + +## Veto + +Proposals may be configured with an optional `VetoConfig` - a configuration describing +the veto flow. + +VetoConfig timelock period enables a party (such as an oversight committee DAO) +to hold the main DAO accountable by vetoing proposals once (and potentially +before) they are passed for a given timelock period. + +No actions from DAO members are allowed during the timelock period. + +After the timelock expires, the proposal can be executed normally. + +`VetoConfig` contains the following fields: + +### `timelock_duration` + +Timelock duration (`cw_utils::Duration`) describes the duration of timelock +in blocks or seconds. + +The delay duration is added to the proposal's expiration to get the timelock +expiration (`Expiration`) used for the new proposal state of `VetoTimelock { +expiration: Expiration }`. + +If the vetoer address is another DAO, this duration should be carefully +considered based on of the vetoer DAO's voting period. + +### `vetoer` + +Vetoer (`String`) is the address of the account allowed to veto the proposals +that are in `VetoTimelock` state. + +Vetoer address can be updated via a regular proposal config update. + +If you want the `vetoer` role to be shared between multiple organizations or +individuals, a +[cw1-whitelist](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw1-whitelist) +contract address can be used to allow multiple accounts to veto the prop. + +### `early_execute` + +Early execute (`bool`) is a flag used to indicate whether the vetoer can execute +the proposals before the timelock period is expired. The proposals still need to +be passed and in the `VetoTimelock` state in order for this to be possible. This +may prevent the veto flow from consistently lengthening the governance process. + +### `veto_before_passed` + +Veto before passed (`bool`) is a flag used to indicate whether the vetoer +can veto a proposal before it passes. Votes may still be cast until the +specified proposal expiration, even once vetoed. diff --git a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json index 085214aa4..59bdd0924 100644 --- a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json +++ b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json @@ -1,6 +1,6 @@ { "contract_name": "dao-proposal-multiple", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -54,6 +54,17 @@ } ] }, + "veto": { + "description": "Optional veto configuration for proposal execution. If set, proposals can only be executed after the timelock delay expiration. During this period an oversight account (`veto.vetoer`) can veto the proposal.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "voting_strategy": { "description": "Voting params configuration", "allOf": [ @@ -110,6 +121,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "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" @@ -153,6 +179,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -174,6 +201,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -190,7 +224,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -261,6 +295,42 @@ } ] }, + "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" + }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VotingStrategy": { "description": "Determines how many choices may be selected.", "oneOf": [ @@ -403,6 +473,31 @@ }, "additionalProperties": false }, + { + "description": "Callable only if veto is configured", + "type": "object", + "required": [ + "veto" + ], + "properties": { + "veto": { + "type": "object", + "required": [ + "proposal_id" + ], + "properties": { + "proposal_id": { + "description": "The ID of the proposal to veto.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Closes a proposal that has failed (either not passed or timed out). If applicable this will cause the proposal deposit associated wth said proposal to be returned.", "type": "object", @@ -481,6 +576,17 @@ "description": "If set to true only members may execute passed proposals. Otherwise, any address may execute a passed proposal. Applies to all outstanding and future proposals.", "type": "boolean" }, + "veto": { + "description": "Optional time delay on proposal execution, during which the proposal may be vetoed.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "voting_strategy": { "description": "The new proposal voting strategy. This will only apply to proposals created after the config update.", "allOf": [ @@ -1019,7 +1125,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -1137,7 +1243,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -1149,6 +1255,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -1170,6 +1277,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -1241,7 +1355,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -1412,6 +1526,38 @@ "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" }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VoteOption": { "type": "string", "enum": [ @@ -1521,7 +1667,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -1924,6 +2070,17 @@ "$ref": "#/definitions/PreProposeInfo" } ] + }, + "veto": { + "description": "This field was not present in DAO DAO v1. To migrate, a value must be specified.\n\noptional configuration for veto feature", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -1991,11 +2148,61 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "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 + } + ] + }, "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -2017,6 +2224,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2071,6 +2285,42 @@ "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" + }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false } } }, @@ -2129,6 +2379,17 @@ "description": "If set to true only members may execute passed proposals. Otherwise, any address may execute a passed proposal.", "type": "boolean" }, + "veto": { + "description": "Optional veto configuration. If set to `None`, veto option is disabled. Otherwise contains the configuration for veto flow.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "voting_strategy": { "description": "The threshold a proposal must reach to complete.", "allOf": [ @@ -2183,7 +2444,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -2214,6 +2475,38 @@ } ] }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VotingStrategy": { "description": "Determines how many choices may be selected.", "oneOf": [ @@ -2669,6 +2962,40 @@ } ] }, + "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 + } + ] + }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" @@ -2785,7 +3112,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -2903,7 +3230,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -2956,6 +3283,7 @@ } }, "description": { + "description": "The main body of the proposal text", "type": "string" }, "expiration": { @@ -2992,7 +3320,7 @@ "minimum": 0.0 }, "status": { - "description": "Prosal status (Open, rejected, executed, execution failed, closed, passed)", + "description": "The proposal status", "allOf": [ { "$ref": "#/definitions/Status" @@ -3000,6 +3328,7 @@ ] }, "title": { + "description": "The title of the proposal", "type": "string" }, "total_power": { @@ -3010,6 +3339,17 @@ } ] }, + "veto": { + "description": "Optional veto configuration. If set to `None`, veto option is disabled. Otherwise contains the configuration for veto flow.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "votes": { "description": "The vote tally.", "allOf": [ @@ -3045,7 +3385,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -3222,6 +3562,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -3241,6 +3610,38 @@ "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" }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VoteOption": { "type": "string", "enum": [ @@ -3350,7 +3751,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -3850,6 +4251,40 @@ } ] }, + "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 + } + ] + }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" @@ -3966,7 +4401,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -4084,7 +4519,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -4137,6 +4572,7 @@ } }, "description": { + "description": "The main body of the proposal text", "type": "string" }, "expiration": { @@ -4173,7 +4609,7 @@ "minimum": 0.0 }, "status": { - "description": "Prosal status (Open, rejected, executed, execution failed, closed, passed)", + "description": "The proposal status", "allOf": [ { "$ref": "#/definitions/Status" @@ -4181,6 +4617,7 @@ ] }, "title": { + "description": "The title of the proposal", "type": "string" }, "total_power": { @@ -4191,6 +4628,17 @@ } ] }, + "veto": { + "description": "Optional veto configuration. If set to `None`, veto option is disabled. Otherwise contains the configuration for veto flow.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "votes": { "description": "The vote tally.", "allOf": [ @@ -4226,7 +4674,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -4384,6 +4832,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -4403,6 +4880,38 @@ "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" }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VoteOption": { "type": "string", "enum": [ @@ -4512,7 +5021,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -4988,6 +5497,40 @@ } ] }, + "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 + } + ] + }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" @@ -5104,7 +5647,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -5222,7 +5765,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -5275,6 +5818,7 @@ } }, "description": { + "description": "The main body of the proposal text", "type": "string" }, "expiration": { @@ -5311,7 +5855,7 @@ "minimum": 0.0 }, "status": { - "description": "Prosal status (Open, rejected, executed, execution failed, closed, passed)", + "description": "The proposal status", "allOf": [ { "$ref": "#/definitions/Status" @@ -5319,6 +5863,7 @@ ] }, "title": { + "description": "The title of the proposal", "type": "string" }, "total_power": { @@ -5329,6 +5874,17 @@ } ] }, + "veto": { + "description": "Optional veto configuration. If set to `None`, veto option is disabled. Otherwise contains the configuration for veto flow.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "votes": { "description": "The vote tally.", "allOf": [ @@ -5364,7 +5920,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -5541,6 +6097,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -5560,6 +6145,38 @@ "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" }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VoteOption": { "type": "string", "enum": [ @@ -5669,7 +6286,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { diff --git a/contracts/proposal/dao-proposal-multiple/src/contract.rs b/contracts/proposal/dao-proposal-multiple/src/contract.rs index 35465ab50..632060f53 100644 --- a/contracts/proposal/dao-proposal-multiple/src/contract.rs +++ b/contracts/proposal/dao-proposal-multiple/src/contract.rs @@ -1,18 +1,20 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response, StdResult, - Storage, SubMsg, WasmMsg, + to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, Response, + StdResult, Storage, SubMsg, WasmMsg, }; use cw2::set_contract_version; use cw_hooks::Hooks; use cw_storage_plus::Bound; use cw_utils::{parse_reply_instantiate_data, Duration}; +use dao_hooks::proposal::{ + new_proposal_hooks, proposal_completed_hooks, proposal_status_changed_hooks, +}; +use dao_hooks::vote::new_vote_hooks; use dao_interface::voting::IsActiveResponse; -use dao_pre_propose_multiple::contract::ExecuteMsg as PreProposeMsg; -use dao_proposal_hooks::{new_proposal_hooks, proposal_status_changed_hooks}; -use dao_vote_hooks::new_vote_hooks; +use dao_voting::veto::{VetoConfig, VetoError}; use dao_voting::{ multiple_choice::{ MultipleChoiceOptions, MultipleChoiceVote, MultipleChoiceVotes, VotingStrategy, @@ -60,6 +62,11 @@ pub fn instantiate( .pre_propose_info .into_initial_policy_and_messages(dao.clone())?; + // if veto is configured, validate its fields + if let Some(veto_config) = &msg.veto { + veto_config.validate(&deps.as_ref(), &max_voting_period)?; + }; + let config = Config { voting_strategy: msg.voting_strategy, min_voting_period, @@ -68,6 +75,7 @@ pub fn instantiate( allow_revoting: msg.allow_revoting, dao, close_proposal_on_execution_failure: msg.close_proposal_on_execution_failure, + veto: msg.veto, }; // Initialize proposal count to zero so that queries return zero @@ -110,6 +118,7 @@ pub fn execute( rationale, } => execute_vote(deps, env, info, proposal_id, vote, rationale), ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id), + ExecuteMsg::Veto { proposal_id } => execute_veto(deps, env, info, proposal_id), ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id), ExecuteMsg::UpdateConfig { voting_strategy, @@ -119,6 +128,7 @@ pub fn execute( allow_revoting, dao, close_proposal_on_execution_failure, + veto, } => execute_update_config( deps, info, @@ -129,6 +139,7 @@ pub fn execute( allow_revoting, dao, close_proposal_on_execution_failure, + veto, ), ExecuteMsg::UpdatePreProposeInfo { info: new_info } => { execute_update_proposal_creation_policy(deps, info, new_info) @@ -217,6 +228,7 @@ pub fn execute_propose( votes: MultipleChoiceVotes::zero(checked_multiple_choice_options.len()), allow_revoting: config.allow_revoting, choices: checked_multiple_choice_options, + veto: config.veto, }; // Update the proposal's status. Addresses case where proposal // expires on the same block as it is created. @@ -239,7 +251,7 @@ pub fn execute_propose( // // `to_vec` is the method used by cosmwasm to convert a struct // into it's byte representation in storage. - let proposal_size = cosmwasm_std::to_vec(&proposal)?.len() as u64; + let proposal_size = cosmwasm_std::to_json_vec(&proposal)?.len() as u64; if proposal_size > MAX_PROPOSAL_SIZE { return Err(ContractError::ProposalTooLarge { size: proposal_size, @@ -259,6 +271,79 @@ pub fn execute_propose( .add_attribute("status", proposal.status.to_string())) } +pub fn execute_veto( + deps: DepsMut, + env: Env, + info: MessageInfo, + proposal_id: u64, +) -> Result { + let mut prop = PROPOSALS + .may_load(deps.storage, proposal_id)? + .ok_or(ContractError::NoSuchProposal { id: proposal_id })?; + + // ensure status is up to date + prop.update_status(&env.block)?; + let old_status = prop.status; + + let veto_config = prop + .veto + .as_ref() + .ok_or(VetoError::NoVetoConfiguration {})?; + + // Check sender is vetoer + veto_config.check_is_vetoer(&info)?; + + match prop.status { + Status::Open => { + // can only veto an open proposal if veto_before_passed is enabled. + veto_config.check_veto_before_passed_enabled()?; + } + Status::Passed => { + // if this proposal has veto configured but is in the passed state, + // the timelock already expired, so provide a more specific error. + return Err(ContractError::VetoError(VetoError::TimelockExpired {})); + } + Status::VetoTimelock { expiration } => { + // vetoer can veto the proposal iff the timelock is active/not + // expired. this should never happen since the status updates to + // passed after the timelock expires, but let's check anyway. + if expiration.is_expired(&env.block) { + return Err(ContractError::VetoError(VetoError::TimelockExpired {})); + } + } + // generic status error if the proposal has any other status. + _ => { + return Err(ContractError::VetoError(VetoError::InvalidProposalStatus { + status: prop.status.to_string(), + })); + } + } + + // Update proposal status to vetoed + prop.status = Status::Vetoed; + PROPOSALS.save(deps.storage, proposal_id, &prop)?; + + // Add proposal status change hooks + let proposal_status_changed_hooks = proposal_status_changed_hooks( + PROPOSAL_HOOKS, + deps.storage, + proposal_id, + old_status.to_string(), + prop.status.to_string(), + )?; + + // Add prepropose / deposit module hook which will handle deposit refunds. + let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?; + let proposal_completed_hooks = + proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?; + + Ok(Response::new() + .add_attribute("action", "veto") + .add_attribute("proposal_id", proposal_id.to_string()) + .add_submessages(proposal_status_changed_hooks) + .add_submessages(proposal_completed_hooks)) +} + pub fn execute_vote( deps: DepsMut, env: Env, @@ -375,18 +460,52 @@ pub fn execute_execute( &config.dao, Some(prop.start_height), )?; - if power.is_zero() { + + // if there is no veto config, then caller is not the vetoer + // if there is, we validate the caller addr + let vetoer_call = prop + .veto + .as_ref() + .map_or(false, |veto_config| veto_config.vetoer == info.sender); + + if power.is_zero() && !vetoer_call { return Err(ContractError::Unauthorized {}); } } - // Check here that the proposal is passed. Allow it to be - // executed even if it is expired so long as it passed during its - // voting period. + // Check here that the proposal is passed or timelocked. + // Allow it to be executed even if it is expired so long + // as it passed during its voting period. Allow it to be + // executed in timelock state if early_execute is enabled + // and the sender is the vetoer. prop.update_status(&env.block)?; let old_status = prop.status; - if prop.status != Status::Passed { - return Err(ContractError::NotPassed {}); + match &prop.status { + Status::Passed => (), + Status::VetoTimelock { expiration } => { + let veto_config = prop + .veto + .as_ref() + .ok_or(VetoError::NoVetoConfiguration {})?; + + // Check if the sender is the vetoer + match veto_config.vetoer == info.sender { + // if sender is the vetoer we validate the early exec flag + true => veto_config.check_early_execute_enabled()?, + // otherwise timelock must be expired in order to execute + false => { + // it should never be expired here since the status updates + // to passed after the timelock expires, but let's check + // anyway. i.e. this error should always be returned. + if !expiration.is_expired(&env.block) { + return Err(ContractError::VetoError(VetoError::Timelocked {})); + } + } + } + } + _ => { + return Err(ContractError::NotPassed {}); + } } prop.status = Status::Executed; @@ -400,7 +519,7 @@ pub fn execute_execute( let response = if !winning_choice.msgs.is_empty() { let execute_message = WasmMsg::Execute { contract_addr: config.dao.to_string(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs: winning_choice.msgs, })?, funds: vec![], @@ -419,7 +538,7 @@ pub fn execute_execute( Response::default() }; - let hooks = proposal_status_changed_hooks( + let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, proposal_id, @@ -429,28 +548,12 @@ pub fn execute_execute( // Add prepropose / deposit module hook which will handle deposit refunds. let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?; - let hooks = match proposal_creation_policy { - ProposalCreationPolicy::Anyone {} => hooks, - ProposalCreationPolicy::Module { addr } => { - let msg = to_binary(&PreProposeMsg::ProposalCompletedHook { - proposal_id, - new_status: prop.status, - })?; - let mut hooks = hooks; - hooks.push(SubMsg::reply_on_error( - WasmMsg::Execute { - contract_addr: addr.into_string(), - msg, - funds: vec![], - }, - failed_pre_propose_module_hook_id(), - )); - hooks - } - }; + let proposal_completed_hooks = + proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?; Ok(response - .add_submessages(hooks) + .add_submessages(proposal_status_changed_hooks) + .add_submessages(proposal_completed_hooks) .add_attribute("action", "execute") .add_attribute("sender", info.sender) .add_attribute("proposal_id", proposal_id.to_string()) @@ -478,7 +581,7 @@ pub fn execute_close( PROPOSALS.save(deps.storage, proposal_id, &prop)?; - let hooks = proposal_status_changed_hooks( + let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, proposal_id, @@ -488,27 +591,12 @@ pub fn execute_close( // Add prepropose / deposit module hook which will handle deposit refunds. let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?; - let hooks = match proposal_creation_policy { - ProposalCreationPolicy::Anyone {} => hooks, - ProposalCreationPolicy::Module { addr } => { - let msg = to_binary(&PreProposeMsg::ProposalCompletedHook { - proposal_id, - new_status: prop.status, - })?; - let mut hooks = hooks; - hooks.push(SubMsg::reply_on_error( - WasmMsg::Execute { - contract_addr: addr.into_string(), - msg, - funds: vec![], - }, - failed_pre_propose_module_hook_id(), - )); - hooks - } - }; + let proposal_completed_hooks = + proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?; + Ok(Response::default() - .add_submessages(hooks) + .add_submessages(proposal_status_changed_hooks) + .add_submessages(proposal_completed_hooks) .add_attribute("action", "close") .add_attribute("sender", info.sender) .add_attribute("proposal_id", proposal_id.to_string())) @@ -525,6 +613,7 @@ pub fn execute_update_config( allow_revoting: bool, dao: String, close_proposal_on_execution_failure: bool, + veto: Option, ) -> Result { let config = CONFIG.load(deps.storage)?; @@ -540,6 +629,11 @@ pub fn execute_update_config( let (min_voting_period, max_voting_period) = validate_voting_period(min_voting_period, max_voting_period)?; + // if veto is configured, validate its fields + if let Some(veto_config) = &veto { + veto_config.validate(&deps.as_ref(), &max_voting_period)?; + }; + CONFIG.save( deps.storage, &Config { @@ -550,6 +644,7 @@ pub fn execute_update_config( allow_revoting, dao, close_proposal_on_execution_failure, + veto, }, )?; @@ -746,30 +841,30 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { limit, } => query_reverse_proposals(deps, env, start_before, limit), QueryMsg::ProposalCreationPolicy {} => query_creation_policy(deps), - QueryMsg::ProposalHooks {} => to_binary(&PROPOSAL_HOOKS.query_hooks(deps)?), - QueryMsg::VoteHooks {} => to_binary(&VOTE_HOOKS.query_hooks(deps)?), + QueryMsg::ProposalHooks {} => to_json_binary(&PROPOSAL_HOOKS.query_hooks(deps)?), + QueryMsg::VoteHooks {} => to_json_binary(&VOTE_HOOKS.query_hooks(deps)?), QueryMsg::Dao {} => query_dao(deps), } } pub fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - to_binary(&config) + to_json_binary(&config) } pub fn query_dao(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - to_binary(&config.dao) + to_json_binary(&config.dao) } pub fn query_proposal(deps: Deps, env: Env, id: u64) -> StdResult { let proposal = PROPOSALS.load(deps.storage, id)?; - to_binary(&proposal.into_response(&env.block, id)?) + to_json_binary(&proposal.into_response(&env.block, id)?) } pub fn query_creation_policy(deps: Deps) -> StdResult { let policy = CREATION_POLICY.load(deps.storage)?; - to_binary(&policy) + to_json_binary(&policy) } pub fn query_list_proposals( @@ -788,7 +883,7 @@ pub fn query_list_proposals( .map(|(id, proposal)| proposal.into_response(&env.block, id)) .collect::>>()?; - to_binary(&ProposalListResponse { proposals: props }) + to_json_binary(&ProposalListResponse { proposals: props }) } pub fn query_reverse_proposals( @@ -807,16 +902,16 @@ pub fn query_reverse_proposals( .map(|(id, proposal)| proposal.into_response(&env.block, id)) .collect::>>()?; - to_binary(&ProposalListResponse { proposals: props }) + to_json_binary(&ProposalListResponse { proposals: props }) } pub fn query_next_proposal_id(deps: Deps) -> StdResult { - to_binary(&next_proposal_id(deps.storage)?) + to_json_binary(&next_proposal_id(deps.storage)?) } pub fn query_proposal_count(deps: Deps) -> StdResult { let proposal_count = PROPOSAL_COUNT.load(deps.storage)?; - to_binary(&proposal_count) + to_json_binary(&proposal_count) } pub fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult { @@ -828,7 +923,7 @@ pub fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult>>()?; - to_binary(&VoteListResponse { votes }) + to_json_binary(&VoteListResponse { votes }) } pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; - to_binary(&dao_interface::voting::InfoResponse { info }) + to_json_binary(&dao_interface::voting::InfoResponse { info }) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -878,7 +973,10 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Err(ContractError::NoSuchProposal { id: proposal_id }), })?; - Ok(Response::new().add_attribute("proposal execution failed", proposal_id.to_string())) + + Ok(Response::new() + .add_attribute("proposal execution failed", proposal_id.to_string()) + .add_attribute("error", msg.result.into_result().err().unwrap_or_default())) } TaggedReplyId::FailedProposalHook(idx) => { let addr = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; diff --git a/contracts/proposal/dao-proposal-multiple/src/error.rs b/contracts/proposal/dao-proposal-multiple/src/error.rs index a1d1df105..76fe05724 100644 --- a/contracts/proposal/dao-proposal-multiple/src/error.rs +++ b/contracts/proposal/dao-proposal-multiple/src/error.rs @@ -3,10 +3,10 @@ use std::u64; use cosmwasm_std::StdError; use cw_hooks::HookError; use cw_utils::ParseReplyError; -use dao_voting::{reply::error::TagError, threshold::ThresholdError}; +use dao_voting::{reply::error::TagError, threshold::ThresholdError, veto::VetoError}; use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), @@ -17,6 +17,9 @@ pub enum ContractError { #[error("{0}")] HookError(#[from] HookError), + #[error(transparent)] + VetoError(#[from] VetoError), + #[error("Unauthorized")] Unauthorized {}, diff --git a/contracts/proposal/dao-proposal-multiple/src/msg.rs b/contracts/proposal/dao-proposal-multiple/src/msg.rs index a79fca805..482ffadd7 100644 --- a/contracts/proposal/dao-proposal-multiple/src/msg.rs +++ b/contracts/proposal/dao-proposal-multiple/src/msg.rs @@ -4,6 +4,7 @@ use dao_dao_macros::proposal_module_query; use dao_voting::{ multiple_choice::{MultipleChoiceOptions, MultipleChoiceVote, VotingStrategy}, pre_propose::PreProposeInfo, + veto::VetoConfig, }; #[cw_serde] @@ -37,6 +38,12 @@ pub struct InstantiateMsg { /// remain open until the DAO's treasury was large enough for it to be /// executed. pub close_proposal_on_execution_failure: bool, + /// Optional veto configuration for proposal execution. + /// If set, proposals can only be executed after the timelock + /// delay expiration. + /// During this period an oversight account (`veto.vetoer`) can + /// veto the proposal. + pub veto: Option, } #[cw_serde] @@ -74,6 +81,11 @@ pub enum ExecuteMsg { /// The ID of the proposal to execute. proposal_id: u64, }, + /// Callable only if veto is configured + Veto { + /// The ID of the proposal to veto. + proposal_id: u64, + }, /// Closes a proposal that has failed (either not passed or timed /// out). If applicable this will cause the proposal deposit /// associated wth said proposal to be returned. @@ -116,6 +128,9 @@ pub enum ExecuteMsg { /// remain open until the DAO's treasury was large enough for it to be /// executed. close_proposal_on_execution_failure: bool, + /// Optional time delay on proposal execution, during which the + /// proposal may be vetoed. + veto: Option, }, /// Updates the sender's rationale for their vote on the specified /// proposal. Errors if no vote vote has been cast. @@ -217,6 +232,11 @@ pub enum MigrateMsg { /// no deposit or membership checks when submitting a proposal. The "ModuleMayPropose" /// option allows for instantiating a prepropose module which will handle deposit verification and return logic. pre_propose_info: PreProposeInfo, + /// This field was not present in DAO DAO v1. To migrate, a + /// value must be specified. + /// + /// optional configuration for veto feature + veto: Option, }, FromCompatible {}, } diff --git a/contracts/proposal/dao-proposal-multiple/src/proposal.rs b/contracts/proposal/dao-proposal-multiple/src/proposal.rs index 4852993db..454a60762 100644 --- a/contracts/proposal/dao-proposal-multiple/src/proposal.rs +++ b/contracts/proposal/dao-proposal-multiple/src/proposal.rs @@ -1,3 +1,5 @@ +use std::ops::Add; + use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, BlockInfo, StdError, StdResult, Uint128}; use cw_utils::Expiration; @@ -6,6 +8,7 @@ use dao_voting::{ CheckedMultipleChoiceOption, MultipleChoiceOptionType, MultipleChoiceVotes, VotingStrategy, }, status::Status, + veto::VetoConfig, voting::does_vote_count_pass, }; @@ -13,7 +16,9 @@ use crate::query::ProposalResponse; #[cw_serde] pub struct MultipleChoiceProposal { + /// The title of the proposal pub title: String, + /// The main body of the proposal text pub description: String, /// The address that created this proposal. pub proposer: Addr, @@ -30,7 +35,7 @@ pub struct MultipleChoiceProposal { pub expiration: Expiration, /// The options to be chosen from in the vote. pub choices: Vec, - /// Prosal status (Open, rejected, executed, execution failed, closed, passed) + /// The proposal status pub status: Status, /// Voting settings (threshold, quorum, etc.) pub voting_strategy: VotingStrategy, @@ -43,6 +48,9 @@ pub struct MultipleChoiceProposal { /// When enabled, proposals can only be executed after the voting /// perid has ended and the proposal passed. pub allow_revoting: bool, + /// Optional veto configuration. If set to `None`, veto option + /// is disabled. Otherwise contains the configuration for veto flow. + pub veto: Option, } pub enum VoteResult { @@ -65,14 +73,35 @@ impl MultipleChoiceProposal { /// Gets the current status of the proposal. pub fn current_status(&self, block: &BlockInfo) -> StdResult { - if self.status == Status::Open && self.is_passed(block)? { - Ok(Status::Passed) - } else if self.status == Status::Open - && (self.expiration.is_expired(block) || self.is_rejected(block)?) - { - Ok(Status::Rejected) - } else { - Ok(self.status) + match self.status { + Status::Open if self.is_passed(block)? => match &self.veto { + // if prop is passed and veto is configured, calculate timelock + // expiration. if it's expired, this proposal has passed. + // otherwise, set status to `VetoTimelock`. + Some(veto_config) => { + let expiration = self.expiration.add(veto_config.timelock_duration)?; + + if expiration.is_expired(block) { + Ok(Status::Passed) + } else { + Ok(Status::VetoTimelock { expiration }) + } + } + // Otherwise the proposal is simply passed + None => Ok(Status::Passed), + }, + Status::Open if self.expiration.is_expired(block) || self.is_rejected(block)? => { + Ok(Status::Rejected) + } + Status::VetoTimelock { expiration } => { + // if prop timelock expired, proposal is now passed. + if expiration.is_expired(block) { + Ok(Status::Passed) + } else { + Ok(self.status) + } + } + _ => Ok(self.status), } } @@ -306,6 +335,7 @@ mod tests { votes, allow_revoting, min_voting_period: None, + veto: None, } } diff --git a/contracts/proposal/dao-proposal-multiple/src/state.rs b/contracts/proposal/dao-proposal-multiple/src/state.rs index f9c6baa59..2261f6d03 100644 --- a/contracts/proposal/dao-proposal-multiple/src/state.rs +++ b/contracts/proposal/dao-proposal-multiple/src/state.rs @@ -7,6 +7,7 @@ use cw_utils::Duration; use dao_voting::{ multiple_choice::{MultipleChoiceVote, VotingStrategy}, pre_propose::ProposalCreationPolicy, + veto::VetoConfig, }; /// The proposal module's configuration. @@ -43,6 +44,9 @@ pub struct Config { /// remain open until the DAO's treasury was large enough for it to be /// executed. pub close_proposal_on_execution_failure: bool, + /// Optional veto configuration. If set to `None`, veto option + /// is disabled. Otherwise contains the configuration for veto flow. + pub veto: Option, } // Each ballot stores a chosen vote and corresponding voting power and rationale. diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/adversarial_tests.rs b/contracts/proposal/dao-proposal-multiple/src/testing/adversarial_tests.rs index fe09e69d1..3c27c98d1 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/adversarial_tests.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/adversarial_tests.rs @@ -9,7 +9,7 @@ use crate::testing::queries::{ }; use crate::testing::tests::{get_pre_propose_info, ALTERNATIVE_ADDR, CREATOR_ADDR}; use crate::ContractError; -use cosmwasm_std::{to_binary, Addr, CosmosMsg, Decimal, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, Decimal, Uint128, WasmMsg}; use cw20::Cw20Coin; use cw_multi_test::{next_block, App, Executor}; use cw_utils::Duration; @@ -280,6 +280,7 @@ pub fn test_allow_voting_after_proposal_execution_pre_expiration_cw20() { false, ), close_proposal_on_execution_failure: true, + veto: None, }; let core_addr = instantiate_with_multiple_staked_balances_governance( @@ -307,7 +308,7 @@ pub fn test_allow_voting_after_proposal_execution_pre_expiration_cw20() { recipient: CREATOR_ADDR.to_string(), amount: Uint128::new(100_000_000), }; - let binary_msg = to_binary(&msg).unwrap(); + let binary_msg = to_json_binary(&msg).unwrap(); let options = vec![ MultipleChoiceOption { diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/do_votes.rs b/contracts/proposal/dao-proposal-multiple/src/testing/do_votes.rs index 32eb43168..e8b5744bf 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/do_votes.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/do_votes.rs @@ -130,6 +130,7 @@ where voting_strategy, close_proposal_on_execution_failure: true, pre_propose_info, + veto: None, }; let governance_addr = setup_governance(&mut app, instantiate, Some(initial_balances)); @@ -240,7 +241,6 @@ where match should_execute { ShouldExecute::Yes => { if res.is_err() { - println!("{:?}", res.err()); panic!() } // Check that the vote was recorded correctly. @@ -702,7 +702,7 @@ where let one_sum: u64 = one.iter().sum(); let none_sum: u64 = none.iter().sum(); - let mut sums = vec![zero_sum, one_sum, none_sum]; + let mut sums = [zero_sum, one_sum, none_sum]; sums.sort_unstable(); // If none of the above wins or there is a tie between second and first choice. diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs index f27d06c7b..bd954913b 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 cosmwasm_std::{to_json_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, @@ -15,10 +13,9 @@ use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo}, multiple_choice::VotingStrategy, pre_propose::PreProposeInfo, - threshold::PercentageThreshold, + threshold::{ActiveThreshold, ActiveThreshold::AbsoluteCount, PercentageThreshold}, }; -use dao_voting_cw20_staked::msg::ActiveThreshold; -use dao_voting_cw20_staked::msg::ActiveThreshold::AbsoluteCount; +use dao_voting_cw4::msg::GroupContract; use crate::testing::tests::ALTERNATIVE_ADDR; use crate::{ @@ -35,13 +32,14 @@ fn get_pre_propose_info( PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, - msg: to_binary(&cppm::InstantiateMsg { + msg: to_json_binary(&cppm::InstantiateMsg { deposit_info, open_proposal_submission, extension: Empty::default(), }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "pre_propose_contract".to_string(), }, } @@ -67,6 +65,7 @@ pub fn _get_default_token_dao_proposal_module_instantiate(app: &mut App) -> Inst false, ), close_proposal_on_execution_failure: true, + veto: None, } } @@ -83,6 +82,7 @@ fn _get_default_non_token_dao_proposal_module_instantiate(app: &mut App) -> Inst allow_revoting: false, pre_propose_info: get_pre_propose_info(app, None, false), close_proposal_on_execution_failure: true, + veto: None, } } @@ -150,20 +150,24 @@ pub fn _instantiate_with_staked_cw721_governance( automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: cw721_stake_id, - msg: to_binary(&dao_voting_cw721_staked::msg::InstantiateMsg { - owner: Some(Admin::CoreModule {}), + msg: to_json_binary(&dao_voting_cw721_staked::msg::InstantiateMsg { unstaking_duration: None, - nft_address: nft_address.to_string(), + nft_contract: dao_voting_cw721_staked::msg::NftContract::Existing { + address: nft_address.to_string(), + }, + active_threshold: None, }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), - msg: to_binary(&proposal_module_instantiate).unwrap(), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -194,14 +198,12 @@ pub fn _instantiate_with_staked_cw721_governance( app.execute_contract( Addr::unchecked("ekez"), nft_address.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint( - cw721_base::msg::MintMsg::> { - token_id: format!("{address}_{i}"), - owner: address.clone(), - token_uri: None, - extension: None, - }, - ), + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { + token_id: format!("{address}_{i}"), + owner: address.clone(), + token_uri: None, + extension: None, + }, &[], ) .unwrap(); @@ -211,7 +213,7 @@ pub fn _instantiate_with_staked_cw721_governance( &cw721_base::msg::ExecuteMsg::, Empty>::SendNft { contract: staking_addr.to_string(), token_id: format!("{address}_{i}"), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -267,21 +269,24 @@ pub fn _instantiate_with_native_staked_balances_governance( automatically_add_cw721s: false, 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(), + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: dao_voting_token_staked::msg::TokenInfo::Existing { + denom: "ujuno".to_string(), + }, unstaking_duration: None, + active_threshold: None, }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), - msg: to_binary(&proposal_module_instantiate).unwrap(), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -319,7 +324,7 @@ pub fn _instantiate_with_native_staked_balances_governance( app.execute_contract( Addr::unchecked(&address), native_staking_addr.clone(), - &dao_voting_native_staked::msg::ExecuteMsg::Stake {}, + &dao_voting_token_staked::msg::ExecuteMsg::Stake {}, &[Coin { amount, denom: "ujuno".to_string(), @@ -376,7 +381,7 @@ pub fn instantiate_with_cw20_balances_governance( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw20_balance::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_balance::msg::InstantiateMsg { token_info: dao_voting_cw20_balance::msg::TokenInfo::New { code_id: cw20_id, label: "DAO DAO governance token".to_string(), @@ -389,13 +394,15 @@ pub fn instantiate_with_cw20_balances_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), - msg: to_binary(&proposal_module_instantiate).unwrap(), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -456,7 +463,7 @@ pub fn instantiate_with_staked_balances_governance( automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: staked_balances_voting_id, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { active_threshold: None, token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: cw20_id, @@ -473,13 +480,15 @@ pub fn instantiate_with_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), - msg: to_binary(&proposal_module_instantiate).unwrap(), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -528,7 +537,7 @@ pub fn instantiate_with_staked_balances_governance( &cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount, - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -591,7 +600,7 @@ pub fn instantiate_with_multiple_staked_balances_governance( automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: staked_balances_voting_id, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { active_threshold: Some(AbsoluteCount { count: Uint128::one(), }), @@ -610,13 +619,15 @@ pub fn instantiate_with_multiple_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), - msg: to_binary(&proposal_module_instantiate).unwrap(), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -665,7 +676,7 @@ pub fn instantiate_with_multiple_staked_balances_governance( &cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount, - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -706,7 +717,7 @@ pub fn instantiate_with_staking_active_threshold( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: cw20_id, label: "DAO DAO governance token".to_string(), @@ -723,12 +734,14 @@ pub fn instantiate_with_staking_active_threshold( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - msg: to_binary(&proposal_module_instantiate).unwrap(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -792,18 +805,22 @@ pub fn _instantiate_with_cw4_groups_governance( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw4::msg::InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members: initial_weights, + msg: to_json_binary(&dao_voting_cw4::msg::InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: initial_weights, + }, }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - msg: to_binary(&proposal_module_instantiate).unwrap(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs index 2d3fd6e37..8064733d1 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{to_binary, Addr, Coin, CosmosMsg, Decimal, Empty, Timestamp, Uint128, WasmMsg}; +use cosmwasm_std::{ + to_json_binary, Addr, Coin, CosmosMsg, Decimal, Empty, Timestamp, Uint128, WasmMsg, +}; use cw20::Cw20Coin; use cw_denom::{CheckedDenom, UncheckedDenom}; use cw_hooks::HooksResponse; @@ -6,6 +8,7 @@ use cw_multi_test::{next_block, App, BankSudo, Contract, ContractWrapper, Execut use cw_utils::Duration; use dao_interface::state::ProposalModule; use dao_interface::state::{Admin, ModuleInstantiateInfo}; +use dao_voting::veto::{VetoConfig, VetoError}; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, multiple_choice::{ @@ -15,9 +18,9 @@ use dao_voting::{ }, pre_propose::PreProposeInfo, status::Status, - threshold::{PercentageThreshold, Threshold}, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, }; -use dao_voting_cw20_staked::msg::ActiveThreshold; +use std::ops::Add; use std::panic; use crate::{ @@ -90,13 +93,14 @@ pub fn get_pre_propose_info( PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, - msg: to_binary(&cppm::InstantiateMsg { + msg: to_json_binary(&cppm::InstantiateMsg { deposit_info, open_proposal_submission, extension: Empty::default(), }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "pre_propose_contract".to_string(), }, } @@ -120,6 +124,7 @@ fn test_propose() { min_voting_period: None, close_proposal_on_execution_failure: true, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); @@ -135,6 +140,7 @@ fn test_propose() { voting_strategy: voting_strategy.clone(), min_voting_period: None, close_proposal_on_execution_failure: true, + veto: None, }; assert_eq!(config, expected); @@ -175,6 +181,7 @@ fn test_propose() { }, allow_revoting: false, min_voting_period: None, + veto: None, }; assert_eq!(created.proposal, expected); @@ -199,6 +206,7 @@ fn test_propose_wrong_num_choices() { allow_revoting: false, voting_strategy: voting_strategy.clone(), pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); @@ -214,6 +222,7 @@ fn test_propose_wrong_num_choices() { allow_revoting: false, dao: core_addr, voting_strategy, + veto: None, }; assert_eq!(config, expected); @@ -275,6 +284,7 @@ fn test_proposal_count_initialized_to_zero() { only_members_execute: true, allow_revoting: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance(&mut app, msg, None); @@ -309,6 +319,7 @@ fn test_no_early_pass_with_min_duration() { allow_revoting: false, close_proposal_on_execution_failure: true, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance( @@ -403,6 +414,7 @@ fn test_propose_with_messages() { only_members_execute: true, allow_revoting: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance( @@ -439,11 +451,12 @@ fn test_propose_with_messages() { only_members_execute: false, allow_revoting: false, dao: "dao".to_string(), + veto: None, }; let wasm_msg = WasmMsg::Execute { contract_addr: govmod.to_string(), - msg: to_binary(&config_msg).unwrap(), + msg: to_json_binary(&config_msg).unwrap(), funds: vec![], }; @@ -522,6 +535,7 @@ fn test_min_duration_units_missmatch() { allow_revoting: false, close_proposal_on_execution_failure: true, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; instantiate_with_staked_balances_governance( &mut app, @@ -554,6 +568,7 @@ fn test_min_duration_larger_than_proposal_duration() { allow_revoting: false, close_proposal_on_execution_failure: true, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; instantiate_with_staked_balances_governance( &mut app, @@ -585,6 +600,7 @@ fn test_min_duration_same_as_proposal_duration() { allow_revoting: false, close_proposal_on_execution_failure: true, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance( @@ -705,6 +721,7 @@ fn test_voting_module_token_proposal_deposit_instantiate() { }), false, ), + veto: None, }; let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); @@ -780,6 +797,7 @@ fn test_different_token_proposal_deposit() { }), false, ), + veto: None, }; instantiate_with_staked_balances_governance(&mut app, instantiate, None); @@ -841,6 +859,7 @@ fn test_bad_token_proposal_deposit() { }), false, ), + veto: None, }; instantiate_with_staked_balances_governance(&mut app, instantiate, None); @@ -871,6 +890,7 @@ fn test_take_proposal_deposit() { }), false, ), + veto: None, }; let core_addr = instantiate_with_cw20_balances_governance( @@ -976,6 +996,7 @@ fn test_native_proposal_deposit() { }), false, ), + veto: None, }; let core_addr = instantiate_with_staked_balances_governance( @@ -1401,6 +1422,7 @@ fn test_cant_propose_zero_power() { }), false, ), + veto: None, }; let core_addr = instantiate_with_cw20_balances_governance( @@ -1565,6 +1587,7 @@ fn test_cant_execute_not_member() { allow_revoting: false, voting_strategy, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance( @@ -1655,6 +1678,7 @@ fn test_cant_execute_not_member_when_proposal_created() { allow_revoting: false, voting_strategy, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance( @@ -1728,7 +1752,7 @@ fn test_cant_execute_not_member_when_proposal_created() { &cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(10), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -1772,6 +1796,7 @@ fn test_open_proposal_submission() { allow_revoting: false, close_proposal_on_execution_failure: true, pre_propose_info: get_pre_propose_info(&mut app, None, true), + veto: None, }; let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); let govmod = query_multiple_proposal_module(&app, &core_addr); @@ -1840,6 +1865,7 @@ fn test_open_proposal_submission() { votes: MultipleChoiceVotes { vote_weights: vec![Uint128::zero(); 3], }, + veto: None, }; assert_eq!(created.proposal, expected); @@ -2065,6 +2091,7 @@ fn test_execute_expired_proposal() { allow_revoting: false, voting_strategy, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance( @@ -2227,6 +2254,7 @@ fn test_update_config() { only_members_execute: false, allow_revoting: false, dao: dao.to_string(), + veto: None, }, &[], ) @@ -2246,6 +2274,7 @@ fn test_update_config() { only_members_execute: false, allow_revoting: false, dao: Addr::unchecked(CREATOR_ADDR).to_string(), + veto: None, }, &[], ) @@ -2263,6 +2292,7 @@ fn test_update_config() { only_members_execute: false, allow_revoting: false, dao: Addr::unchecked(CREATOR_ADDR), + veto: None, }; assert_eq!(govmod_config, expected); @@ -2281,6 +2311,7 @@ fn test_update_config() { only_members_execute: false, allow_revoting: false, dao: Addr::unchecked(CREATOR_ADDR).to_string(), + veto: None, }, &[], ) @@ -2356,6 +2387,7 @@ fn test_query_list_proposals() { allow_revoting: false, voting_strategy: voting_strategy.clone(), pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let gov_addr = instantiate_with_staked_balances_governance( &mut app, @@ -2436,6 +2468,7 @@ fn test_query_list_proposals() { }, allow_revoting: false, min_voting_period: None, + veto: None, }, }; assert_eq!(proposals_forward.proposals[0], expected); @@ -2464,6 +2497,7 @@ fn test_query_list_proposals() { }, allow_revoting: false, min_voting_period: None, + veto: None, }, }; assert_eq!(proposals_forward.proposals[0], expected); @@ -2489,6 +2523,7 @@ fn test_hooks() { allow_revoting: false, voting_strategy, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); @@ -2615,6 +2650,7 @@ fn test_active_threshold_absolute() { allow_revoting: false, voting_strategy, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staking_active_threshold( @@ -2682,7 +2718,7 @@ fn test_active_threshold_absolute() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(100), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract(Addr::unchecked(CREATOR_ADDR), token_contract, &msg, &[]) .unwrap(); @@ -2742,6 +2778,7 @@ fn test_active_threshold_percent() { allow_revoting: false, voting_strategy, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; // 20% needed to be active, 20% of 100000000 is 20000000 @@ -2810,7 +2847,7 @@ fn test_active_threshold_percent() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(20000000), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract(Addr::unchecked(CREATOR_ADDR), token_contract, &msg, &[]) .unwrap(); @@ -2870,6 +2907,7 @@ fn test_active_threshold_none() { allow_revoting: false, voting_strategy, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = @@ -2901,7 +2939,7 @@ fn test_active_threshold_none() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(2000), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract(Addr::unchecked(CREATOR_ADDR), token_contract, &msg, &[]) .unwrap(); @@ -2980,6 +3018,7 @@ fn test_revoting() { }, close_proposal_on_execution_failure: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, Some(vec![ Cw20Coin { @@ -3112,6 +3151,7 @@ fn test_allow_revoting_config_changes() { }, close_proposal_on_execution_failure: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, Some(vec![ Cw20Coin { @@ -3169,6 +3209,7 @@ fn test_allow_revoting_config_changes() { quorum: PercentageThreshold::Majority {}, }, close_proposal_on_execution_failure: false, + veto: None, }, &[], ) @@ -3263,6 +3304,7 @@ fn test_revoting_same_vote_twice() { }, close_proposal_on_execution_failure: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, Some(vec![ Cw20Coin { @@ -3357,6 +3399,7 @@ fn test_invalid_revote_does_not_invalidate_initial_vote() { }, close_proposal_on_execution_failure: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, Some(vec![ Cw20Coin { @@ -3548,6 +3591,7 @@ fn test_close_failed_proposal() { allow_revoting: false, close_proposal_on_execution_failure: true, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staking_active_threshold(&mut app, instantiate, None, None); @@ -3578,7 +3622,7 @@ fn test_close_failed_proposal() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(2000), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract( Addr::unchecked(CREATOR_ADDR), @@ -3592,7 +3636,7 @@ fn test_close_failed_proposal() { let msg = cw20::Cw20ExecuteMsg::Burn { amount: Uint128::new(2000), }; - let binary_msg = to_binary(&msg).unwrap(); + let binary_msg = to_json_binary(&msg).unwrap(); let options = vec![ MultipleChoiceOption { @@ -3674,7 +3718,7 @@ fn test_close_failed_proposal() { description: "Disable closing failed proposals".to_string(), msgs: vec![WasmMsg::Execute { contract_addr: govmod.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig { voting_strategy: VotingStrategy::SingleChoice { quorum }, max_voting_period: original.max_voting_period, min_voting_period: original.min_voting_period, @@ -3682,6 +3726,7 @@ fn test_close_failed_proposal() { allow_revoting: false, dao: original.dao.to_string(), close_proposal_on_execution_failure: false, + veto: None, }) .unwrap(), funds: vec![], @@ -3796,6 +3841,7 @@ fn test_no_double_refund_on_execute_fail_and_close() { }), false, ), + veto: None, }; let core_addr = instantiate_with_staking_active_threshold( @@ -3836,7 +3882,7 @@ fn test_no_double_refund_on_execute_fail_and_close() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(1), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract( Addr::unchecked(CREATOR_ADDR), @@ -3864,7 +3910,7 @@ fn test_no_double_refund_on_execute_fail_and_close() { let msg = cw20::Cw20ExecuteMsg::Burn { amount: Uint128::new(2000), }; - let binary_msg = to_binary(&msg).unwrap(); + let binary_msg = to_json_binary(&msg).unwrap(); // Increase allowance to pay the proposal deposit. app.execute_contract( @@ -3974,6 +4020,7 @@ pub fn test_not_allow_voting_on_expired_proposal() { min_voting_period: None, close_proposal_on_execution_failure: true, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }; let core_addr = instantiate_with_staked_balances_governance( &mut app, @@ -4065,6 +4112,7 @@ fn test_next_proposal_id() { }, close_proposal_on_execution_failure: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, Some(vec![ Cw20Coin { @@ -4136,6 +4184,7 @@ fn test_vote_with_rationale() { }, close_proposal_on_execution_failure: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, Some(vec![ Cw20Coin { @@ -4232,6 +4281,7 @@ fn test_revote_with_rationale() { }, close_proposal_on_execution_failure: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, Some(vec![ Cw20Coin { @@ -4386,6 +4436,7 @@ fn test_update_rationale() { }, close_proposal_on_execution_failure: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, }, Some(vec![ Cw20Coin { @@ -4499,3 +4550,934 @@ fn test_update_rationale() { Some("This may be a good idea, but I'm not sure. YOLO".to_string()) ); } + +#[test] +fn test_open_proposal_passes_with_zero_timelock_veto_duration() { + let mut app = App::default(); + let timelock_duration = 0; + let veto_config = VetoConfig { + timelock_duration: Duration::Height(timelock_duration), + vetoer: "vetoer".to_string(), + early_execute: false, + veto_before_passed: true, + }; + + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: false, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: Some(veto_config), + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + let next_proposal_id: u64 = app + .wrap() + .query_wasm_smart(&proposal_module, &QueryMsg::NextProposalId {}) + .unwrap(); + assert_eq!(next_proposal_id, 1); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + let mc_options = MultipleChoiceOptions { options }; + + // Create a basic proposal with 2 options + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Propose { + title: "A simple text proposal".to_string(), + description: "A simple text proposal".to_string(), + choices: mc_options, + proposer: None, + }, + &[], + ) + .unwrap(); + + // zero duration timelock goes straight to passed status + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Vote { + proposal_id: 1, + vote: MultipleChoiceVote { option_id: 0 }, + rationale: None, + }, + &[], + ) + .unwrap(); + + // pass enough time to expire the proposal voting + app.update_block(|b| b.height += 7); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + + assert_eq!(proposal.proposal.status, Status::Passed {},); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id: 1 }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::TimelockExpired {})); +} + +#[test] +fn test_veto_non_existing_prop_id() { + let mut app = App::default(); + let timelock_duration = 0; + let veto_config = VetoConfig { + timelock_duration: Duration::Height(timelock_duration), + vetoer: "vetoer".to_string(), + early_execute: false, + veto_before_passed: true, + }; + + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: false, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: Some(veto_config), + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + // veto from non open/passed/veto state should return an error + let err: ContractError = app + .execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id: 69 }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::NoSuchProposal { id: 69 }); +} + +#[test] +fn test_veto_with_no_veto_configuration() { + let mut app = App::default(); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: false, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + let next_proposal_id: u64 = app + .wrap() + .query_wasm_smart(&proposal_module, &QueryMsg::NextProposalId {}) + .unwrap(); + assert_eq!(next_proposal_id, 1); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + let mc_options = MultipleChoiceOptions { options }; + + // Create a basic proposal with 2 options + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Propose { + title: "A simple text proposal".to_string(), + description: "A simple text proposal".to_string(), + choices: mc_options, + proposer: None, + }, + &[], + ) + .unwrap(); + + // veto from non open/passed/veto state should return an error + let err: ContractError = app + .execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id: 1 }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!( + err, + ContractError::VetoError(VetoError::NoVetoConfiguration {}) + ); +} + +#[test] +fn test_veto_open_prop_with_veto_before_passed_disabled() { + let mut app = App::default(); + let timelock_duration = 10; + let veto_config = VetoConfig { + timelock_duration: Duration::Height(timelock_duration), + vetoer: "vetoer".to_string(), + early_execute: false, + veto_before_passed: false, + }; + + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: false, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: Some(veto_config), + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + let next_proposal_id: u64 = app + .wrap() + .query_wasm_smart(&proposal_module, &QueryMsg::NextProposalId {}) + .unwrap(); + assert_eq!(next_proposal_id, 1); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + let mc_options = MultipleChoiceOptions { options }; + + // Create a basic proposal with 2 options + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Propose { + title: "A simple text proposal".to_string(), + description: "A simple text proposal".to_string(), + choices: mc_options, + proposer: None, + }, + &[], + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked("a-2"), + proposal_module.clone(), + &ExecuteMsg::Vote { + proposal_id: 1, + vote: MultipleChoiceVote { option_id: 0 }, + rationale: None, + }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + + assert_eq!(proposal.proposal.status, Status::Open {},); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id: 1 }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!( + err, + ContractError::VetoError(VetoError::NoVetoBeforePassed {}) + ); +} + +#[test] +fn test_veto_when_veto_timelock_expired() -> anyhow::Result<()> { + let mut app = App::default(); + let timelock_duration = Duration::Height(3); + let veto_config = VetoConfig { + timelock_duration, + vetoer: "vetoer".to_string(), + early_execute: false, + veto_before_passed: false, + }; + + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: false, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: Some(veto_config), + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + let next_proposal_id: u64 = app + .wrap() + .query_wasm_smart(&proposal_module, &QueryMsg::NextProposalId {}) + .unwrap(); + assert_eq!(next_proposal_id, 1); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + let mc_options = MultipleChoiceOptions { options }; + + // Create a basic proposal with 2 options + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Propose { + title: "A simple text proposal".to_string(), + description: "A simple text proposal".to_string(), + choices: mc_options, + proposer: None, + }, + &[], + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Vote { + proposal_id: 1, + vote: MultipleChoiceVote { option_id: 0 }, + rationale: None, + }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal.proposal.expiration.add(timelock_duration)?, + }, + ); + + // pass enough time to expire the timelock + app.update_block(|b| b.height += 10); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id: 1 }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::TimelockExpired {}),); + + Ok(()) +} + +#[test] +fn test_veto_sets_prop_status_to_vetoed() -> anyhow::Result<()> { + let mut app = App::default(); + let timelock_duration = Duration::Height(3); + let veto_config = VetoConfig { + timelock_duration, + vetoer: "vetoer".to_string(), + early_execute: false, + veto_before_passed: false, + }; + + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: false, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: Some(veto_config), + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + let next_proposal_id: u64 = app + .wrap() + .query_wasm_smart(&proposal_module, &QueryMsg::NextProposalId {}) + .unwrap(); + assert_eq!(next_proposal_id, 1); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + let mc_options = MultipleChoiceOptions { options }; + + // Create a basic proposal with 2 options + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Propose { + title: "A simple text proposal".to_string(), + description: "A simple text proposal".to_string(), + choices: mc_options, + proposer: None, + }, + &[], + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Vote { + proposal_id: 1, + vote: MultipleChoiceVote { option_id: 0 }, + rationale: None, + }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal.proposal.expiration.add(timelock_duration)?, + }, + ); + + app.execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + + assert_eq!(proposal.proposal.status, Status::Vetoed {},); + + Ok(()) +} + +#[test] +fn test_veto_from_catchall_state() { + let mut app = App::default(); + let timelock_duration = 3; + let veto_config = VetoConfig { + timelock_duration: Duration::Height(timelock_duration), + vetoer: "vetoer".to_string(), + early_execute: true, + veto_before_passed: false, + }; + + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: false, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: Some(veto_config), + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + let next_proposal_id: u64 = app + .wrap() + .query_wasm_smart(&proposal_module, &QueryMsg::NextProposalId {}) + .unwrap(); + assert_eq!(next_proposal_id, 1); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + let mc_options = MultipleChoiceOptions { options }; + + // Create a basic proposal with 2 options + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Propose { + title: "A simple text proposal".to_string(), + description: "A simple text proposal".to_string(), + choices: mc_options, + proposer: None, + }, + &[], + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Vote { + proposal_id: 1, + vote: MultipleChoiceVote { option_id: 0 }, + rationale: None, + }, + &[], + ) + .unwrap(); + + // pass enough time to expire the timelock + app.update_block(|b| b.height += 10); + + app.execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + assert_eq!(proposal.proposal.status, Status::Executed {},); + + // veto from non open/passed/veto state should return an error + let err: ContractError = app + .execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id: 1 }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!( + err, + ContractError::VetoError(VetoError::InvalidProposalStatus { + status: "executed".to_string(), + }) + ); +} + +#[test] +fn test_veto_timelock_early_execute_happy() -> anyhow::Result<()> { + let mut app = App::default(); + let timelock_duration = Duration::Height(3); + let veto_config = VetoConfig { + timelock_duration, + vetoer: "vetoer".to_string(), + early_execute: true, + veto_before_passed: false, + }; + + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: true, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: Some(veto_config), + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + let next_proposal_id: u64 = app + .wrap() + .query_wasm_smart(&proposal_module, &QueryMsg::NextProposalId {}) + .unwrap(); + assert_eq!(next_proposal_id, 1); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + let mc_options = MultipleChoiceOptions { options }; + + // Create a basic proposal with 2 options + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Propose { + title: "A simple text proposal".to_string(), + description: "A simple text proposal".to_string(), + choices: mc_options, + proposer: None, + }, + &[], + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Vote { + proposal_id: 1, + vote: MultipleChoiceVote { option_id: 0 }, + rationale: None, + }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal.proposal.expiration.add(timelock_duration)?, + }, + ); + + // first we try unauthorized early execution + let err: ContractError = app + .execute_contract( + Addr::unchecked("not-the-vetoer"), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::Unauthorized {}); + + app.execute_contract( + Addr::unchecked("vetoer"), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + assert_eq!(proposal.proposal.status, Status::Executed {},); + + Ok(()) +} + +#[test] +fn test_veto_timelock_expires_happy() -> anyhow::Result<()> { + let mut app = App::default(); + let timelock_duration = Duration::Height(3); + let veto_config = VetoConfig { + timelock_duration, + vetoer: "vetoer".to_string(), + early_execute: false, + veto_before_passed: false, + }; + + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + InstantiateMsg { + min_voting_period: None, + max_voting_period: Duration::Height(6), + only_members_execute: false, + allow_revoting: false, + voting_strategy: VotingStrategy::SingleChoice { + quorum: PercentageThreshold::Majority {}, + }, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: Some(veto_config), + }, + Some(vec![ + Cw20Coin { + address: "a-1".to_string(), + amount: Uint128::new(110_000_000), + }, + Cw20Coin { + address: "a-2".to_string(), + amount: Uint128::new(100_000_000), + }, + ]), + ); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + let proposal_module = query_multiple_proposal_module(&app, &core_addr); + + let next_proposal_id: u64 = app + .wrap() + .query_wasm_smart(&proposal_module, &QueryMsg::NextProposalId {}) + .unwrap(); + assert_eq!(next_proposal_id, 1); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + let mc_options = MultipleChoiceOptions { options }; + + // Create a basic proposal with 2 options + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Propose { + title: "A simple text proposal".to_string(), + description: "A simple text proposal".to_string(), + choices: mc_options, + proposer: None, + }, + &[], + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Vote { + proposal_id: 1, + vote: MultipleChoiceVote { option_id: 0 }, + rationale: None, + }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal.proposal.expiration.add(timelock_duration)?, + }, + ); + + // pass enough time to expire the timelock + app.update_block(|b| b.height += 10); + + app.execute_contract( + Addr::unchecked("a-1"), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: ProposalResponse = query_proposal(&app, &govmod, 1); + assert_eq!(proposal.proposal.status, Status::Executed {},); + + Ok(()) +} diff --git a/contracts/proposal/dao-proposal-single/Cargo.toml b/contracts/proposal/dao-proposal-single/Cargo.toml index fdf6e842d..af3d872ce 100644 --- a/contracts/proposal/dao-proposal-single/Cargo.toml +++ b/contracts/proposal/dao-proposal-single/Cargo.toml @@ -32,21 +32,21 @@ dao-pre-propose-base = { workspace = true } dao-interface = { workspace = true } dao-voting = { workspace = true } cw-hooks = { workspace = true } -dao-proposal-hooks = { workspace = true } -dao-vote-hooks = { workspace = true } +dao-hooks = { workspace = true } cw-utils-v1 = { workspace = true} voting-v1 = { workspace = true } cw-proposal-single-v1 = { workspace = true, features = ["library"] } [dev-dependencies] +anyhow = { workspace = true } cosmwasm-schema = { workspace = true } cw-multi-test = { workspace = true } dao-dao-core = { workspace = true } dao-voting-cw4 = { workspace = true } dao-voting-cw20-balance = { workspace = true } dao-voting-cw20-staked = { workspace = true } -dao-voting-native-staked = { workspace = true } +dao-voting-token-staked = { workspace = true } dao-voting-cw721-staked = { workspace = true } dao-pre-propose-single = { workspace = true } cw-denom = { workspace = true } diff --git a/contracts/proposal/dao-proposal-single/README.md b/contracts/proposal/dao-proposal-single/README.md index 78c547f1f..1eb27dc30 100644 --- a/contracts/proposal/dao-proposal-single/README.md +++ b/contracts/proposal/dao-proposal-single/README.md @@ -1,5 +1,8 @@ # dao-proposal-single +[![dao-proposal-single on crates.io](https://img.shields.io/crates/v/dao-proposal-single.svg?logo=rust)](https://crates.io/crates/dao-proposal-single) +[![docs.rs](https://img.shields.io/docsrs/dao-proposal-single?logo=docsdotrs)](https://docs.rs/dao-proposal-single/latest/dao_proposal_single/) + A proposal module for a DAO DAO DAO which supports simple "yes", "no", "abstain" voting. Proposals may have associated messages which will be executed by the core module upon the proposal being passed and @@ -56,3 +59,55 @@ handling a hook. The proposals may be configured to allow revoting. In such cases, users are able to change their vote as long as the proposal is still open. Revoting for the currently cast option will return an error. + +## Veto + +Proposals may be configured with an optional `VetoConfig` - a configuration describing +the veto flow. + +VetoConfig timelock period enables a party (such as an oversight committee DAO) +to hold the main DAO accountable by vetoing proposals once (and potentially +before) they are passed for a given timelock period. + +No actions from DAO members are allowed during the timelock period. + +After the timelock expires, the proposal can be executed normally. + +`VetoConfig` contains the following fields: + +### `timelock_duration` + +Timelock duration (`cw_utils::Duration`) describes the duration of timelock +in blocks or seconds. + +The delay duration is added to the proposal's expiration to get the timelock +expiration (`Expiration`) used for the new proposal state of `VetoTimelock { +expiration: Expiration }`. + +If the vetoer address is another DAO, this duration should be carefully +considered based on of the vetoer DAO's voting period. + +### `vetoer` + +Vetoer (`String`) is the address of the account allowed to veto the proposals +that are in `VetoTimelock` state. + +Vetoer address can be updated via a regular proposal config update. + +If you want the `vetoer` role to be shared between multiple organizations or +individuals, a +[cw1-whitelist](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw1-whitelist) +contract address can be used to allow multiple accounts to veto the prop. + +### `early_execute` + +Early execute (`bool`) is a flag used to indicate whether the vetoer can execute +the proposals before the timelock period is expired. The proposals still need to +be passed and in the `VetoTimelock` state in order for this to be possible. This +may prevent the veto flow from consistently lengthening the governance process. + +### `veto_before_passed` + +Veto before passed (`bool`) is a flag used to indicate whether the vetoer +can veto a proposal before it passes. Votes may still be cast until the +specified proposal expiration, even once vetoed. diff --git a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json index 37495bf6c..6b2c48888 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -1,6 +1,6 @@ { "contract_name": "dao-proposal-single", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -61,6 +61,17 @@ "$ref": "#/definitions/Threshold" } ] + }, + "veto": { + "description": "Optional veto configuration for proposal execution. If set, proposals can only be executed after the timelock delay expiration. During this period an oversight account (`veto.vetoer`) can veto the proposal.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false, @@ -110,6 +121,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "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" @@ -153,6 +179,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -174,6 +201,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -190,7 +224,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -339,6 +373,38 @@ "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" + }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false } } }, @@ -455,6 +521,31 @@ }, "additionalProperties": false }, + { + "description": "Callable only if veto is configured", + "type": "object", + "required": [ + "veto" + ], + "properties": { + "veto": { + "type": "object", + "required": [ + "proposal_id" + ], + "properties": { + "proposal_id": { + "description": "The ID of the proposal to veto.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Closes a proposal that has failed (either not passed or timed out). If applicable this will cause the proposal deposit associated wth said proposal to be returned.", "type": "object", @@ -540,6 +631,17 @@ "$ref": "#/definitions/Threshold" } ] + }, + "veto": { + "description": "Optional time delay on proposal execution, during which the proposal may be vetoed.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -1045,7 +1147,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -1163,7 +1265,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -1175,6 +1277,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -1196,6 +1299,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -1212,7 +1322,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -1492,6 +1602,38 @@ "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" }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "Vote": { "oneOf": [ { @@ -1600,7 +1742,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -2010,6 +2152,17 @@ "$ref": "#/definitions/PreProposeInfo" } ] + }, + "veto": { + "description": "This field was not present in DAO DAO v1. To migrate, a value must be specified.\n\noptional configuration for veto feature", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -2077,11 +2230,61 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "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 + } + ] + }, "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -2103,6 +2306,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2157,6 +2367,42 @@ "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" + }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false } } }, @@ -2222,6 +2468,17 @@ "$ref": "#/definitions/Threshold" } ] + }, + "veto": { + "description": "Optional veto configuration. If set to `None`, veto option is disabled. Otherwise contains the configuration for veto flow.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false, @@ -2269,7 +2526,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -2378,6 +2635,38 @@ "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" + }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false } } }, @@ -2783,6 +3072,40 @@ } ] }, + "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 + } + ] + }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" @@ -2899,7 +3222,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -3017,7 +3340,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -3025,7 +3348,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -3093,9 +3416,11 @@ ], "properties": { "allow_revoting": { + "description": "Whether or not revoting is enabled. If revoting is enabled, a proposal cannot pass until the voting period has elapsed.", "type": "boolean" }, "description": { + "description": "The main body of the proposal text", "type": "string" }, "expiration": { @@ -3139,7 +3464,12 @@ "minimum": 0.0 }, "status": { - "$ref": "#/definitions/Status" + "description": "The proposal status", + "allOf": [ + { + "$ref": "#/definitions/Status" + } + ] }, "threshold": { "description": "The threshold at which this proposal will pass.", @@ -3150,6 +3480,7 @@ ] }, "title": { + "description": "The title of the proposal", "type": "string" }, "total_power": { @@ -3160,8 +3491,24 @@ } ] }, + "veto": { + "description": "Optional veto configuration. If set to `None`, veto option is disabled. Otherwise contains the configuration for veto flow.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "votes": { - "$ref": "#/definitions/Votes" + "description": "Votes on a particular proposal", + "allOf": [ + { + "$ref": "#/definitions/Votes" + } + ] } }, "additionalProperties": false @@ -3293,6 +3640,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -3387,6 +3763,38 @@ "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" }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VoteOption": { "type": "string", "enum": [ @@ -3490,7 +3898,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -3964,6 +4372,40 @@ } ] }, + "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 + } + ] + }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" @@ -4080,7 +4522,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -4198,7 +4640,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -4206,7 +4648,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -4254,9 +4696,11 @@ ], "properties": { "allow_revoting": { + "description": "Whether or not revoting is enabled. If revoting is enabled, a proposal cannot pass until the voting period has elapsed.", "type": "boolean" }, "description": { + "description": "The main body of the proposal text", "type": "string" }, "expiration": { @@ -4300,7 +4744,12 @@ "minimum": 0.0 }, "status": { - "$ref": "#/definitions/Status" + "description": "The proposal status", + "allOf": [ + { + "$ref": "#/definitions/Status" + } + ] }, "threshold": { "description": "The threshold at which this proposal will pass.", @@ -4311,6 +4760,7 @@ ] }, "title": { + "description": "The title of the proposal", "type": "string" }, "total_power": { @@ -4321,8 +4771,24 @@ } ] }, + "veto": { + "description": "Optional veto configuration. If set to `None`, veto option is disabled. Otherwise contains the configuration for veto flow.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "votes": { - "$ref": "#/definitions/Votes" + "description": "Votes on a particular proposal", + "allOf": [ + { + "$ref": "#/definitions/Votes" + } + ] } }, "additionalProperties": false @@ -4454,6 +4920,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -4548,6 +5043,38 @@ "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" }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VoteOption": { "type": "string", "enum": [ @@ -4651,7 +5178,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { @@ -5090,6 +5617,40 @@ } ] }, + "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 + } + ] + }, "Empty": { "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", "type": "object" @@ -5206,7 +5767,7 @@ ] }, "channel_id": { - "description": "exisiting channel to send the tokens over", + "description": "existing channel to send the tokens over", "type": "string" }, "timeout": { @@ -5324,7 +5885,7 @@ "minimum": 0.0 }, "revision": { - "description": "the version that the client is currently on (eg. after reseting the chain this could increment 1 as height drops to 0)", + "description": "the version that the client is currently on (e.g. after resetting the chain this could increment 1 as height drops to 0)", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -5332,7 +5893,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -5400,9 +5961,11 @@ ], "properties": { "allow_revoting": { + "description": "Whether or not revoting is enabled. If revoting is enabled, a proposal cannot pass until the voting period has elapsed.", "type": "boolean" }, "description": { + "description": "The main body of the proposal text", "type": "string" }, "expiration": { @@ -5446,7 +6009,12 @@ "minimum": 0.0 }, "status": { - "$ref": "#/definitions/Status" + "description": "The proposal status", + "allOf": [ + { + "$ref": "#/definitions/Status" + } + ] }, "threshold": { "description": "The threshold at which this proposal will pass.", @@ -5457,6 +6025,7 @@ ] }, "title": { + "description": "The title of the proposal", "type": "string" }, "total_power": { @@ -5467,8 +6036,24 @@ } ] }, + "veto": { + "description": "Optional veto configuration. If set to `None`, veto option is disabled. Otherwise contains the configuration for veto flow.", + "anyOf": [ + { + "$ref": "#/definitions/VetoConfig" + }, + { + "type": "null" + } + ] + }, "votes": { - "$ref": "#/definitions/Votes" + "description": "Votes on a particular proposal", + "allOf": [ + { + "$ref": "#/definitions/Votes" + } + ] } }, "additionalProperties": false @@ -5600,6 +6185,35 @@ "enum": [ "execution_failed" ] + }, + { + "description": "The proposal is timelocked. Only the configured vetoer can execute or veto until the timelock expires.", + "type": "object", + "required": [ + "veto_timelock" + ], + "properties": { + "veto_timelock": { + "type": "object", + "required": [ + "expiration" + ], + "properties": { + "expiration": { + "$ref": "#/definitions/Expiration" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The proposal has been vetoed.", + "type": "string", + "enum": [ + "vetoed" + ] } ] }, @@ -5694,6 +6308,38 @@ "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" }, + "VetoConfig": { + "type": "object", + "required": [ + "early_execute", + "timelock_duration", + "veto_before_passed", + "vetoer" + ], + "properties": { + "early_execute": { + "description": "Whether or not the vetoer can execute a proposal early before the timelock duration has expired", + "type": "boolean" + }, + "timelock_duration": { + "description": "The time duration to lock a proposal for after its expiration to allow the vetoer to veto.", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + }, + "veto_before_passed": { + "description": "Whether or not the vetoer can veto a proposal before it passes.", + "type": "boolean" + }, + "vetoer": { + "description": "The address able to veto proposals.", + "type": "string" + } + }, + "additionalProperties": false + }, "VoteOption": { "type": "string", "enum": [ @@ -5797,7 +6443,7 @@ } }, "label": { - "description": "A human-readbale label for the contract", + "description": "A human-readable label for the contract.\n\nValid values should: - not be empty - not be bigger than 128 bytes (or some chain-specific limit) - not start / end with whitespace", "type": "string" }, "msg": { diff --git a/contracts/proposal/dao-proposal-single/src/contract.rs b/contracts/proposal/dao-proposal-single/src/contract.rs index 5f30030f5..d9ac7c07f 100644 --- a/contracts/proposal/dao-proposal-single/src/contract.rs +++ b/contracts/proposal/dao-proposal-single/src/contract.rs @@ -1,17 +1,18 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, + to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, Response, StdResult, Storage, SubMsg, WasmMsg, }; use cw2::{get_contract_version, set_contract_version, ContractVersion}; use cw_hooks::Hooks; -use cw_proposal_single_v1 as v1; use cw_storage_plus::Bound; use cw_utils::{parse_reply_instantiate_data, Duration}; +use dao_hooks::proposal::{ + new_proposal_hooks, proposal_completed_hooks, proposal_status_changed_hooks, +}; +use dao_hooks::vote::new_vote_hooks; use dao_interface::voting::IsActiveResponse; -use dao_proposal_hooks::{new_proposal_hooks, proposal_status_changed_hooks}; -use dao_vote_hooks::new_vote_hooks; use dao_voting::pre_propose::{PreProposeInfo, ProposalCreationPolicy}; use dao_voting::proposal::{ SingleChoiceProposeMsg as ProposeMsg, DEFAULT_LIMIT, MAX_PROPOSAL_SIZE, @@ -21,12 +22,12 @@ use dao_voting::reply::{ }; use dao_voting::status::Status; use dao_voting::threshold::Threshold; +use dao_voting::veto::{VetoConfig, VetoError}; use dao_voting::voting::{get_total_power, get_voting_power, validate_voting_period, Vote, Votes}; use crate::msg::MigrateMsg; use crate::proposal::{next_proposal_id, SingleChoiceProposal}; use crate::state::{Config, CREATION_POLICY}; - use crate::v1_state::{ v1_duration_to_v2, v1_expiration_to_v2, v1_status_to_v2, v1_threshold_to_v2, v1_votes_to_v2, }; @@ -38,14 +39,10 @@ use crate::{ query::{ProposalResponse, VoteInfo, VoteListResponse, VoteResponse}, state::{Ballot, BALLOTS, CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_HOOKS, VOTE_HOOKS}, }; - +use cw_proposal_single_v1 as v1; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-proposal-single"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -/// Message type used for firing hooks to this module's pre-propose -/// module, if one is installed. -type PreProposeHookMsg = dao_pre_propose_base::msg::ExecuteMsg; - #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -66,6 +63,11 @@ pub fn instantiate( .pre_propose_info .into_initial_policy_and_messages(dao.clone())?; + // if veto is configured, validate its fields + if let Some(veto_config) = &msg.veto { + veto_config.validate(&deps.as_ref(), &max_voting_period)?; + }; + let config = Config { threshold: msg.threshold, max_voting_period, @@ -74,6 +76,7 @@ pub fn instantiate( dao: dao.clone(), allow_revoting: msg.allow_revoting, close_proposal_on_execution_failure: msg.close_proposal_on_execution_failure, + veto: msg.veto, }; // Initialize proposal count to zero so that queries return zero @@ -121,6 +124,7 @@ pub fn execute( allow_revoting, dao, close_proposal_on_execution_failure, + veto, } => execute_update_config( deps, info, @@ -131,6 +135,7 @@ pub fn execute( allow_revoting, dao, close_proposal_on_execution_failure, + veto, ), ExecuteMsg::UpdatePreProposeInfo { info: new_info } => { execute_update_proposal_creation_policy(deps, info, new_info) @@ -145,6 +150,7 @@ pub fn execute( ExecuteMsg::RemoveVoteHook { address } => { execute_remove_vote_hook(deps, env, info, address) } + ExecuteMsg::Veto { proposal_id } => execute_veto(deps, env, info, proposal_id), } } @@ -213,10 +219,11 @@ pub fn execute_propose( status: Status::Open, votes: Votes::zero(), allow_revoting: config.allow_revoting, + veto: config.veto, }; // Update the proposal's status. Addresses case where proposal // expires on the same block as it is created. - proposal.update_status(&env.block); + proposal.update_status(&env.block)?; proposal }; let id = advance_proposal_id(deps.storage)?; @@ -235,7 +242,7 @@ pub fn execute_propose( // // `to_vec` is the method used by cosmwasm to convert a struct // into it's byte representation in storage. - let proposal_size = cosmwasm_std::to_vec(&proposal)?.len() as u64; + let proposal_size = cosmwasm_std::to_json_vec(&proposal)?.len() as u64; if proposal_size > MAX_PROPOSAL_SIZE { return Err(ContractError::ProposalTooLarge { size: proposal_size, @@ -255,6 +262,79 @@ pub fn execute_propose( .add_attribute("status", proposal.status.to_string())) } +pub fn execute_veto( + deps: DepsMut, + env: Env, + info: MessageInfo, + proposal_id: u64, +) -> Result { + let mut prop = PROPOSALS + .may_load(deps.storage, proposal_id)? + .ok_or(ContractError::NoSuchProposal { id: proposal_id })?; + + // ensure status is up to date + prop.update_status(&env.block)?; + let old_status = prop.status; + + let veto_config = prop + .veto + .as_ref() + .ok_or(VetoError::NoVetoConfiguration {})?; + + // Check sender is vetoer + veto_config.check_is_vetoer(&info)?; + + match prop.status { + Status::Open => { + // can only veto an open proposal if veto_before_passed is enabled. + veto_config.check_veto_before_passed_enabled()?; + } + Status::Passed => { + // if this proposal has veto configured but is in the passed state, + // the timelock already expired, so provide a more specific error. + return Err(ContractError::VetoError(VetoError::TimelockExpired {})); + } + Status::VetoTimelock { expiration } => { + // vetoer can veto the proposal iff the timelock is active/not + // expired. this should never happen since the status updates to + // passed after the timelock expires, but let's check anyway. + if expiration.is_expired(&env.block) { + return Err(ContractError::VetoError(VetoError::TimelockExpired {})); + } + } + // generic status error if the proposal has any other status. + _ => { + return Err(ContractError::VetoError(VetoError::InvalidProposalStatus { + status: prop.status.to_string(), + })); + } + } + + // Update proposal status to vetoed + prop.status = Status::Vetoed; + PROPOSALS.save(deps.storage, proposal_id, &prop)?; + + // Add proposal status change hooks + let proposal_status_changed_hooks = proposal_status_changed_hooks( + PROPOSAL_HOOKS, + deps.storage, + proposal_id, + old_status.to_string(), + prop.status.to_string(), + )?; + + // Add prepropose / deposit module hook which will handle deposit refunds. + let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?; + let proposal_completed_hooks = + proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?; + + Ok(Response::new() + .add_attribute("action", "veto") + .add_attribute("proposal_id", proposal_id.to_string()) + .add_submessages(proposal_status_changed_hooks) + .add_submessages(proposal_completed_hooks)) +} + pub fn execute_execute( deps: DepsMut, env: Env, @@ -273,18 +353,52 @@ pub fn execute_execute( &config.dao, Some(prop.start_height), )?; - if power.is_zero() { + + // if there is no veto config, then caller is not the vetoer + // if there is, we validate the caller addr + let vetoer_call = prop + .veto + .as_ref() + .map_or(false, |veto_config| veto_config.vetoer == info.sender); + + if power.is_zero() && !vetoer_call { return Err(ContractError::Unauthorized {}); } } - // Check here that the proposal is passed. Allow it to be executed - // even if it is expired so long as it passed during its voting - // period. + // Check here that the proposal is passed or timelocked. + // Allow it to be executed even if it is expired so long + // as it passed during its voting period. Allow it to be + // executed in timelock state if early_execute is enabled + // and the sender is the vetoer. + prop.update_status(&env.block)?; let old_status = prop.status; - prop.update_status(&env.block); - if prop.status != Status::Passed { - return Err(ContractError::NotPassed {}); + match &prop.status { + Status::Passed => (), + Status::VetoTimelock { expiration } => { + let veto_config = prop + .veto + .as_ref() + .ok_or(VetoError::NoVetoConfiguration {})?; + + // Check if the sender is the vetoer + match veto_config.vetoer == info.sender { + // if sender is the vetoer we validate the early exec flag + true => veto_config.check_early_execute_enabled()?, + // otherwise timelock must be expired in order to execute + false => { + // it should never be expired here since the status updates + // to passed after the timelock expires, but let's check + // anyway. i.e. this error should always be returned. + if !expiration.is_expired(&env.block) { + return Err(ContractError::VetoError(VetoError::Timelocked {})); + } + } + } + } + _ => { + return Err(ContractError::NotPassed {}); + } } prop.status = Status::Executed; @@ -295,7 +409,7 @@ pub fn execute_execute( if !prop.msgs.is_empty() { let execute_message = WasmMsg::Execute { contract_addr: config.dao.to_string(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs: prop.msgs, })?, funds: vec![], @@ -313,7 +427,8 @@ pub fn execute_execute( } }; - let hooks = proposal_status_changed_hooks( + // Add proposal status change hooks + let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, proposal_id, @@ -323,28 +438,12 @@ pub fn execute_execute( // Add prepropose / deposit module hook which will handle deposit refunds. let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?; - let hooks = match proposal_creation_policy { - ProposalCreationPolicy::Anyone {} => hooks, - ProposalCreationPolicy::Module { addr } => { - let msg = to_binary(&PreProposeHookMsg::ProposalCompletedHook { - proposal_id, - new_status: prop.status, - })?; - let mut hooks = hooks; - hooks.push(SubMsg::reply_on_error( - WasmMsg::Execute { - contract_addr: addr.into_string(), - msg, - funds: vec![], - }, - failed_pre_propose_module_hook_id(), - )); - hooks - } - }; + let proposal_completed_hooks = + proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?; Ok(response - .add_submessages(hooks) + .add_submessages(proposal_status_changed_hooks) + .add_submessages(proposal_completed_hooks) .add_attribute("action", "execute") .add_attribute("sender", info.sender) .add_attribute("proposal_id", proposal_id.to_string()) @@ -420,7 +519,7 @@ pub fn execute_vote( let old_status = prop.status; prop.votes.add_vote(vote, vote_power); - prop.update_status(&env.block); + prop.update_status(&env.block)?; PROPOSALS.save(deps.storage, proposal_id, &prop)?; @@ -492,7 +591,7 @@ pub fn execute_close( // Update status to ensure that proposals which were open and have // expired are moved to "rejected." - prop.update_status(&env.block); + prop.update_status(&env.block)?; if prop.status != Status::Rejected { return Err(ContractError::WrongCloseStatus {}); } @@ -502,7 +601,8 @@ pub fn execute_close( prop.status = Status::Closed; PROPOSALS.save(deps.storage, proposal_id, &prop)?; - let hooks = proposal_status_changed_hooks( + // Add proposal status change hooks + let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, proposal_id, @@ -512,28 +612,12 @@ pub fn execute_close( // Add prepropose / deposit module hook which will handle deposit refunds. let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?; - let hooks = match proposal_creation_policy { - ProposalCreationPolicy::Anyone {} => hooks, - ProposalCreationPolicy::Module { addr } => { - let msg = to_binary(&PreProposeHookMsg::ProposalCompletedHook { - proposal_id, - new_status: prop.status, - })?; - let mut hooks = hooks; - hooks.push(SubMsg::reply_on_error( - WasmMsg::Execute { - contract_addr: addr.into_string(), - msg, - funds: vec![], - }, - failed_pre_propose_module_hook_id(), - )); - hooks - } - }; + let proposal_completed_hooks = + proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?; Ok(Response::default() - .add_submessages(hooks) + .add_submessages(proposal_status_changed_hooks) + .add_submessages(proposal_completed_hooks) .add_attribute("action", "close") .add_attribute("sender", info.sender) .add_attribute("proposal_id", proposal_id.to_string())) @@ -550,6 +634,7 @@ pub fn execute_update_config( allow_revoting: bool, dao: String, close_proposal_on_execution_failure: bool, + veto: Option, ) -> Result { let config = CONFIG.load(deps.storage)?; @@ -563,6 +648,11 @@ pub fn execute_update_config( let (min_voting_period, max_voting_period) = validate_voting_period(min_voting_period, max_voting_period)?; + // if veto is configured, validate its fields + if let Some(veto_config) = &veto { + veto_config.validate(&deps.as_ref(), &max_voting_period)?; + }; + CONFIG.save( deps.storage, &Config { @@ -573,6 +663,7 @@ pub fn execute_update_config( allow_revoting, dao, close_proposal_on_execution_failure, + veto, }, )?; @@ -730,29 +821,29 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { limit, } => query_reverse_proposals(deps, env, start_before, limit), QueryMsg::ProposalCreationPolicy {} => query_creation_policy(deps), - QueryMsg::ProposalHooks {} => to_binary(&PROPOSAL_HOOKS.query_hooks(deps)?), - QueryMsg::VoteHooks {} => to_binary(&VOTE_HOOKS.query_hooks(deps)?), + QueryMsg::ProposalHooks {} => to_json_binary(&PROPOSAL_HOOKS.query_hooks(deps)?), + QueryMsg::VoteHooks {} => to_json_binary(&VOTE_HOOKS.query_hooks(deps)?), } } pub fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - to_binary(&config) + to_json_binary(&config) } pub fn query_dao(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - to_binary(&config.dao) + to_json_binary(&config.dao) } pub fn query_proposal(deps: Deps, env: Env, id: u64) -> StdResult { let proposal = PROPOSALS.load(deps.storage, id)?; - to_binary(&proposal.into_response(&env.block, id)) + to_json_binary(&proposal.into_response(&env.block, id)?) } pub fn query_creation_policy(deps: Deps) -> StdResult { let policy = CREATION_POLICY.load(deps.storage)?; - to_binary(&policy) + to_json_binary(&policy) } pub fn query_list_proposals( @@ -769,9 +860,9 @@ pub fn query_list_proposals( .collect::, _>>()? .into_iter() .map(|(id, proposal)| proposal.into_response(&env.block, id)) - .collect(); + .collect::>>()?; - to_binary(&ProposalListResponse { proposals: props }) + to_json_binary(&ProposalListResponse { proposals: props }) } pub fn query_reverse_proposals( @@ -788,18 +879,18 @@ pub fn query_reverse_proposals( .collect::, _>>()? .into_iter() .map(|(id, proposal)| proposal.into_response(&env.block, id)) - .collect(); + .collect::>>()?; - to_binary(&ProposalListResponse { proposals: props }) + to_json_binary(&ProposalListResponse { proposals: props }) } pub fn query_proposal_count(deps: Deps) -> StdResult { let proposal_count = PROPOSAL_COUNT.load(deps.storage)?; - to_binary(&proposal_count) + to_json_binary(&proposal_count) } pub fn query_next_proposal_id(deps: Deps) -> StdResult { - to_binary(&next_proposal_id(deps.storage)?) + to_json_binary(&next_proposal_id(deps.storage)?) } pub fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult { @@ -811,7 +902,7 @@ pub fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult>>()?; - to_binary(&VoteListResponse { votes }) + to_json_binary(&VoteListResponse { votes }) } pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; - to_binary(&dao_interface::voting::InfoResponse { info }) + to_json_binary(&dao_interface::voting::InfoResponse { info }) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -858,6 +949,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { // `CONTRACT_VERSION` here is from the data section of the // blob we are migrating to. `version` is from storage. If @@ -867,19 +959,27 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result Result Ok(Response::default() .add_attribute("action", "migrate") .add_attribute("from", "compatible")), @@ -957,7 +1057,9 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Err(ContractError::NoSuchProposal { id: proposal_id }), })?; - Ok(Response::new().add_attribute("proposal_execution_failed", proposal_id.to_string())) + Ok(Response::new() + .add_attribute("proposal_execution_failed", proposal_id.to_string()) + .add_attribute("error", msg.result.into_result().err().unwrap_or_default())) } TaggedReplyId::FailedProposalHook(idx) => { let addr = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; diff --git a/contracts/proposal/dao-proposal-single/src/error.rs b/contracts/proposal/dao-proposal-single/src/error.rs index 8af18058f..9fc049d21 100644 --- a/contracts/proposal/dao-proposal-single/src/error.rs +++ b/contracts/proposal/dao-proposal-single/src/error.rs @@ -3,10 +3,10 @@ use std::u64; use cosmwasm_std::StdError; use cw_hooks::HookError; use cw_utils::ParseReplyError; -use dao_voting::reply::error::TagError; +use dao_voting::{reply::error::TagError, veto::VetoError}; use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error(transparent)] Std(#[from] StdError), @@ -17,6 +17,9 @@ pub enum ContractError { #[error(transparent)] HookError(#[from] HookError), + #[error(transparent)] + VetoError(#[from] VetoError), + #[error("unauthorized")] Unauthorized {}, @@ -89,4 +92,7 @@ pub enum ContractError { #[error("can not migrate. current version is up to date")] AlreadyMigrated {}, + + #[error("incompatible migration version")] + MigrationVersionError {}, } diff --git a/contracts/proposal/dao-proposal-single/src/lib.rs b/contracts/proposal/dao-proposal-single/src/lib.rs index c9076cf76..b556f8b06 100644 --- a/contracts/proposal/dao-proposal-single/src/lib.rs +++ b/contracts/proposal/dao-proposal-single/src/lib.rs @@ -10,6 +10,6 @@ pub mod query; mod testing; pub mod state; -mod v1_state; +pub mod v1_state; pub use crate::error::ContractError; diff --git a/contracts/proposal/dao-proposal-single/src/msg.rs b/contracts/proposal/dao-proposal-single/src/msg.rs index c0be9b317..302303b48 100644 --- a/contracts/proposal/dao-proposal-single/src/msg.rs +++ b/contracts/proposal/dao-proposal-single/src/msg.rs @@ -3,7 +3,7 @@ use cw_utils::Duration; use dao_dao_macros::proposal_module_query; use dao_voting::{ pre_propose::PreProposeInfo, proposal::SingleChoiceProposeMsg, threshold::Threshold, - voting::Vote, + veto::VetoConfig, voting::Vote, }; #[cw_serde] @@ -38,6 +38,12 @@ pub struct InstantiateMsg { /// remain open until the DAO's treasury was large enough for it to be /// executed. pub close_proposal_on_execution_failure: bool, + /// Optional veto configuration for proposal execution. + /// If set, proposals can only be executed after the timelock + /// delay expiration. + /// During this period an oversight account (`veto.vetoer`) can + /// veto the proposal. + pub veto: Option, } #[cw_serde] @@ -68,6 +74,11 @@ pub enum ExecuteMsg { /// The ID of the proposal to execute. proposal_id: u64, }, + /// Callable only if veto is configured + Veto { + /// The ID of the proposal to veto. + proposal_id: u64, + }, /// Closes a proposal that has failed (either not passed or timed /// out). If applicable this will cause the proposal deposit /// associated wth said proposal to be returned. @@ -110,6 +121,9 @@ pub enum ExecuteMsg { /// remain open until the DAO's treasury was large enough for it to be /// executed. close_proposal_on_execution_failure: bool, + /// Optional time delay on proposal execution, during which the + /// proposal may be vetoed. + veto: Option, }, /// Update's the proposal creation policy used for this /// module. Only the DAO may call this method. @@ -219,6 +233,11 @@ pub enum MigrateMsg { /// no deposit or membership checks when submitting a proposal. The "ModuleMayPropose" /// option allows for instantiating a prepropose module which will handle deposit verification and return logic. pre_propose_info: PreProposeInfo, + /// This field was not present in DAO DAO v1. To migrate, a + /// value must be specified. + /// + /// optional configuration for veto feature + veto: Option, }, FromCompatible {}, } diff --git a/contracts/proposal/dao-proposal-single/src/proposal.rs b/contracts/proposal/dao-proposal-single/src/proposal.rs index 9ba174ad5..a597f3754 100644 --- a/contracts/proposal/dao-proposal-single/src/proposal.rs +++ b/contracts/proposal/dao-proposal-single/src/proposal.rs @@ -1,3 +1,5 @@ +use std::ops::Add; + use crate::query::ProposalResponse; use crate::state::PROPOSAL_COUNT; use cosmwasm_schema::cw_serde; @@ -5,11 +7,14 @@ use cosmwasm_std::{Addr, BlockInfo, CosmosMsg, Decimal, Empty, StdResult, Storag use cw_utils::Expiration; use dao_voting::status::Status; use dao_voting::threshold::{PercentageThreshold, Threshold}; +use dao_voting::veto::VetoConfig; use dao_voting::voting::{does_vote_count_fail, does_vote_count_pass, Votes}; #[cw_serde] pub struct SingleChoiceProposal { + /// The title of the proposal pub title: String, + /// The main body of the proposal text pub description: String, /// The address that created this proposal. pub proposer: Addr, @@ -31,9 +36,16 @@ pub struct SingleChoiceProposal { pub total_power: Uint128, /// The messages that will be executed should this proposal pass. pub msgs: Vec>, + /// The proposal status pub status: Status, + /// Votes on a particular proposal pub votes: Votes, + /// Whether or not revoting is enabled. If revoting is enabled, a proposal + /// cannot pass until the voting period has elapsed. pub allow_revoting: bool, + /// Optional veto configuration. If set to `None`, veto option + /// is disabled. Otherwise contains the configuration for veto flow. + pub veto: Option, } pub fn next_proposal_id(store: &dyn Storage) -> StdResult { @@ -54,28 +66,50 @@ impl SingleChoiceProposal { /// a vote has occurred, the status we read from the proposal status /// may be out of date. This method recomputes the status so that /// queries get accurate information. - pub fn into_response(mut self, block: &BlockInfo, id: u64) -> ProposalResponse { - self.update_status(block); - ProposalResponse { id, proposal: self } + pub fn into_response(mut self, block: &BlockInfo, id: u64) -> StdResult { + self.update_status(block)?; + Ok(ProposalResponse { id, proposal: self }) } /// Gets the current status of the proposal. - pub fn current_status(&self, block: &BlockInfo) -> Status { - if self.status == Status::Open && self.is_passed(block) { - Status::Passed - } else if self.status == Status::Open - && (self.expiration.is_expired(block) || self.is_rejected(block)) - { - Status::Rejected - } else { - self.status + pub fn current_status(&self, block: &BlockInfo) -> StdResult { + match self.status { + Status::Open if self.is_passed(block) => match &self.veto { + // if prop is passed and veto is configured, calculate timelock + // expiration. if it's expired, this proposal has passed. + // otherwise, set status to `VetoTimelock`. + Some(veto_config) => { + let expiration = self.expiration.add(veto_config.timelock_duration)?; + + if expiration.is_expired(block) { + Ok(Status::Passed) + } else { + Ok(Status::VetoTimelock { expiration }) + } + } + // Otherwise the proposal is simply passed + None => Ok(Status::Passed), + }, + Status::Open if self.expiration.is_expired(block) || self.is_rejected(block) => { + Ok(Status::Rejected) + } + Status::VetoTimelock { expiration } => { + // if prop timelock expired, proposal is now passed. + if expiration.is_expired(block) { + Ok(Status::Passed) + } else { + Ok(self.status) + } + } + _ => Ok(self.status), } } /// Sets a proposals status to its current status. - pub fn update_status(&mut self, block: &BlockInfo) { - let new_status = self.current_status(block); - self.status = new_status + pub fn update_status(&mut self, block: &BlockInfo) -> StdResult<()> { + let new_status = self.current_status(block)?; + self.status = new_status; + Ok(()) } /// Returns true iff this proposal is sure to pass (even before @@ -275,6 +309,7 @@ mod test { msgs: vec![], status: Status::Open, threshold, + veto: None, total_power, votes, }; diff --git a/contracts/proposal/dao-proposal-single/src/state.rs b/contracts/proposal/dao-proposal-single/src/state.rs index 3a130bdda..88e748b51 100644 --- a/contracts/proposal/dao-proposal-single/src/state.rs +++ b/contracts/proposal/dao-proposal-single/src/state.rs @@ -3,7 +3,9 @@ use cosmwasm_std::{Addr, Uint128}; use cw_hooks::Hooks; use cw_storage_plus::{Item, Map}; use cw_utils::Duration; -use dao_voting::{pre_propose::ProposalCreationPolicy, threshold::Threshold, voting::Vote}; +use dao_voting::{ + pre_propose::ProposalCreationPolicy, threshold::Threshold, veto::VetoConfig, voting::Vote, +}; use crate::proposal::SingleChoiceProposal; @@ -21,6 +23,7 @@ pub struct Ballot { #[serde(default)] pub rationale: Option, } + /// The governance module's configuration. #[cw_serde] pub struct Config { @@ -55,6 +58,9 @@ pub struct Config { /// remain open until the DAO's treasury was large enough for it to be /// executed. pub close_proposal_on_execution_failure: bool, + /// Optional veto configuration. If set to `None`, veto option + /// is disabled. Otherwise contains the configuration for veto flow. + pub veto: Option, } /// The current top level config for the module. The "config" key was diff --git a/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs b/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs index 5d32804b9..c5e419d9b 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs @@ -11,7 +11,7 @@ use crate::testing::{ }, queries::{query_balance_cw20, query_dao_token, query_proposal, query_single_proposal_module}, }; -use cosmwasm_std::{to_binary, Addr, CosmosMsg, Decimal, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, Decimal, Uint128, WasmMsg}; use cw20::Cw20Coin; use cw_multi_test::{next_block, App}; use cw_utils::Duration; @@ -160,6 +160,7 @@ pub fn test_executed_prop_state_remains_after_vote_swing() { let mut app = App::default(); let instantiate = InstantiateMsg { + veto: None, threshold: AbsolutePercentage { percentage: PercentageThreshold::Percent(Decimal::percent(15)), }, @@ -256,6 +257,7 @@ pub fn test_passed_prop_state_remains_after_vote_swing() { let mut app = App::default(); let instantiate = InstantiateMsg { + veto: None, threshold: AbsolutePercentage { percentage: PercentageThreshold::Percent(Decimal::percent(15)), }, @@ -301,7 +303,7 @@ pub fn test_passed_prop_state_remains_after_vote_swing() { recipient: "threshold".to_string(), amount: Uint128::new(100_000_000), }; - let binary_msg = to_binary(&msg).unwrap(); + let binary_msg = to_json_binary(&msg).unwrap(); mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); let proposal_id = make_proposal( diff --git a/contracts/proposal/dao-proposal-single/src/testing/contracts.rs b/contracts/proposal/dao-proposal-single/src/testing/contracts.rs index f27bd7e35..d222acbc5 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/contracts.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/contracts.rs @@ -39,17 +39,6 @@ pub(crate) fn cw20_stake_contract() -> Box> { Box::new(contract) } -pub(crate) fn v1_proposal_single_contract() -> Box> { - let contract = ContractWrapper::new( - cw_proposal_single_v1::contract::execute, - cw_proposal_single_v1::contract::instantiate, - cw_proposal_single_v1::contract::query, - ) - .with_reply(cw_proposal_single_v1::contract::reply) - .with_migrate(cw_proposal_single_v1::contract::migrate); - Box::new(contract) -} - pub(crate) fn proposal_single_contract() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, @@ -82,9 +71,9 @@ pub(crate) fn cw20_staked_balances_voting_contract() -> Box> pub(crate) fn native_staked_balances_voting_contract() -> Box> { let contract = ContractWrapper::new( - dao_voting_native_staked::contract::execute, - dao_voting_native_staked::contract::instantiate, - dao_voting_native_staked::contract::query, + dao_voting_token_staked::contract::execute, + dao_voting_token_staked::contract::instantiate, + dao_voting_token_staked::contract::query, ); Box::new(contract) } diff --git a/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs b/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs index ecd72e325..aad0cddf8 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs @@ -1,7 +1,9 @@ -use cosmwasm_std::{coins, Addr, Uint128}; +use std::mem::discriminant; + +use cosmwasm_std::{coins, Addr, Coin, Uint128}; use cw20::Cw20Coin; -use cw_multi_test::{App, BankSudo, Executor}; +use cw_multi_test::{App, BankSudo, Executor, SudoMsg}; use dao_interface::state::ProposalModule; use dao_pre_propose_single as cppbps; @@ -96,6 +98,17 @@ where { let mut app = App::default(); + // Mint some ujuno so that it exists for native staking tests + // Otherwise denom validation will fail + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: "sodenomexists".to_string(), + amount: vec![Coin { + amount: Uint128::new(10), + denom: "ujuno".to_string(), + }], + })) + .unwrap(); + let mut initial_balances = votes .iter() .map(|TestSingleChoiceVote { voter, weight, .. }| Cw20Coin { @@ -121,6 +134,7 @@ where let max_voting_period = cw_utils::Duration::Height(6); let instantiate = InstantiateMsg { + veto: None, threshold, max_voting_period, min_voting_period: None, @@ -269,7 +283,11 @@ where .query_wasm_smart(proposal_single, &QueryMsg::Proposal { proposal_id: 1 }) .unwrap(); - assert_eq!(proposal.proposal.status, expected_status); + // We just care about getting the right variant + assert_eq!( + discriminant::(&proposal.proposal.status), + discriminant::(&expected_status) + ); (app, core_addr) } diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index cd9f03549..14b84d3e2 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -1,6 +1,5 @@ -use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Empty, Uint128}; +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Empty, Uint128}; use cw20::Cw20Coin; -use dao_voting_cw20_staked::msg::ActiveThreshold; use cw_multi_test::{next_block, App, BankSudo, Executor, SudoMsg}; use cw_utils::Duration; @@ -10,8 +9,9 @@ use dao_pre_propose_single as cppbps; use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo}, pre_propose::PreProposeInfo, - threshold::{PercentageThreshold, Threshold::ThresholdQuorum}, + threshold::{ActiveThreshold, PercentageThreshold, Threshold::ThresholdQuorum}, }; +use dao_voting_cw4::msg::GroupContract; use crate::msg::InstantiateMsg; @@ -34,13 +34,14 @@ pub(crate) fn get_pre_propose_info( PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, - msg: to_binary(&cppbps::InstantiateMsg { + msg: to_json_binary(&cppbps::InstantiateMsg { deposit_info, open_proposal_submission, extension: Empty::default(), }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "pre_propose_contract".to_string(), }, } @@ -48,6 +49,7 @@ pub(crate) fn get_pre_propose_info( pub(crate) fn get_default_token_dao_proposal_module_instantiate(app: &mut App) -> InstantiateMsg { InstantiateMsg { + veto: None, threshold: ThresholdQuorum { quorum: PercentageThreshold::Percent(Decimal::percent(15)), threshold: PercentageThreshold::Majority {}, @@ -74,6 +76,7 @@ pub(crate) fn get_default_non_token_dao_proposal_module_instantiate( app: &mut App, ) -> InstantiateMsg { InstantiateMsg { + veto: None, threshold: ThresholdQuorum { threshold: PercentageThreshold::Percent(Decimal::percent(15)), quorum: PercentageThreshold::Majority {}, @@ -145,20 +148,24 @@ pub(crate) fn instantiate_with_staked_cw721_governance( automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: cw721_stake_id, - msg: to_binary(&dao_voting_cw721_staked::msg::InstantiateMsg { - owner: Some(Admin::CoreModule {}), + msg: to_json_binary(&dao_voting_cw721_staked::msg::InstantiateMsg { unstaking_duration: None, - nft_address: nft_address.to_string(), + nft_contract: dao_voting_cw721_staked::msg::NftContract::Existing { + address: nft_address.to_string(), + }, + active_threshold: None, }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), - msg: to_binary(&proposal_module_instantiate).unwrap(), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, }; @@ -188,14 +195,12 @@ pub(crate) fn instantiate_with_staked_cw721_governance( app.execute_contract( Addr::unchecked("ekez"), nft_address.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint( - cw721_base::msg::MintMsg::> { - token_id: format!("{address}_{i}"), - owner: address.clone(), - token_uri: None, - extension: None, - }, - ), + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { + token_id: format!("{address}_{i}"), + owner: address.clone(), + token_uri: None, + extension: None, + }, &[], ) .unwrap(); @@ -205,7 +210,7 @@ pub(crate) fn instantiate_with_staked_cw721_governance( &cw721_base::msg::ExecuteMsg::SendNft::, Empty> { contract: staking_addr.to_string(), token_id: format!("{address}_{i}"), - msg: to_binary("").unwrap(), + msg: to_json_binary("").unwrap(), }, &[], ) @@ -262,21 +267,24 @@ pub(crate) fn instantiate_with_native_staked_balances_governance( automatically_add_cw721s: false, 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(), + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: dao_voting_token_staked::msg::TokenInfo::Existing { + denom: "ujuno".to_string(), + }, unstaking_duration: None, + active_threshold: None, }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), - msg: to_binary(&proposal_module_instantiate).unwrap(), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, }; @@ -313,7 +321,7 @@ pub(crate) fn instantiate_with_native_staked_balances_governance( app.execute_contract( Addr::unchecked(&address), native_staking_addr.clone(), - &dao_voting_native_staked::msg::ExecuteMsg::Stake {}, + &dao_voting_token_staked::msg::ExecuteMsg::Stake {}, &[Coin { amount, denom: "ujuno".to_string(), @@ -372,7 +380,7 @@ pub(crate) fn instantiate_with_staked_balances_governance( automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: staked_balances_voting_id, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { active_threshold: None, token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: cw20_id, @@ -389,13 +397,15 @@ pub(crate) fn instantiate_with_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), - msg: to_binary(&proposal_module_instantiate).unwrap(), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, }; @@ -443,7 +453,7 @@ pub(crate) fn instantiate_with_staked_balances_governance( &cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount, - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -485,7 +495,7 @@ pub(crate) fn instantiate_with_staking_active_threshold( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: cw20_id, label: "DAO DAO governance token".to_string(), @@ -502,12 +512,14 @@ pub(crate) fn instantiate_with_staking_active_threshold( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - msg: to_binary(&proposal_module_instantiate).unwrap(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -571,18 +583,22 @@ pub(crate) fn instantiate_with_cw4_groups_governance( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw4::msg::InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members: initial_weights, + msg: to_json_binary(&dao_voting_cw4::msg::InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: initial_weights, + }, }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - msg: to_binary(&proposal_module_instantiate).unwrap(), + msg: to_json_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, diff --git a/contracts/proposal/dao-proposal-single/src/testing/migration_tests.rs b/contracts/proposal/dao-proposal-single/src/testing/migration_tests.rs index 1e3aa7c0c..3dc5c33e8 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/migration_tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/migration_tests.rs @@ -1,13 +1,16 @@ -use cosmwasm_std::{to_binary, Addr, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, Uint128, WasmMsg}; use cw20::Cw20Coin; use cw_multi_test::{next_block, App, Executor}; +use cw_utils::Duration; use dao_interface::query::{GetItemResponse, ProposalModuleCountResponse}; use dao_testing::contracts::{ cw20_base_contract, cw20_stake_contract, cw20_staked_balances_voting_contract, dao_dao_contract, proposal_single_contract, v1_dao_dao_contract, v1_proposal_single_contract, }; +use dao_voting::veto::VetoConfig; use dao_voting::{deposit::UncheckedDepositInfo, status::Status}; +use crate::testing::queries::query_list_proposals; use crate::testing::{ execute::{execute_proposal, make_proposal, vote_on_proposal}, instantiate::get_pre_propose_info, @@ -68,7 +71,7 @@ fn test_v1_v2_full_migration() { automatically_add_cw721s: true, voting_module_instantiate_info: cw_core_v1::msg::ModuleInstantiateInfo { code_id: voting_code, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { active_threshold: None, token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: cw20_code, @@ -89,7 +92,7 @@ fn test_v1_v2_full_migration() { }, proposal_modules_instantiate_info: vec![cw_core_v1::msg::ModuleInstantiateInfo { code_id: proposal_code, - msg: to_binary(&cw_proposal_single_v1::msg::InstantiateMsg { + msg: to_json_binary(&cw_proposal_single_v1::msg::InstantiateMsg { threshold: voting_v1::Threshold::AbsolutePercentage { percentage: voting_v1::PercentageThreshold::Majority {}, }, @@ -152,7 +155,7 @@ fn test_v1_v2_full_migration() { &cw20::Cw20ExecuteMsg::Send { contract: staking.into_string(), amount: Uint128::new(1), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -188,7 +191,7 @@ fn test_v1_v2_full_migration() { description: "d".to_string(), msgs: vec![WasmMsg::Execute { contract_addr: core.to_string(), - msg: to_binary(&cw_core_v1::msg::ExecuteMsg::UpdateCw20List { + msg: to_json_binary(&cw_core_v1::msg::ExecuteMsg::UpdateCw20List { to_add: vec![token.to_string()], to_remove: vec![], }) @@ -247,7 +250,7 @@ fn test_v1_v2_full_migration() { description: "d".to_string(), msgs: vec![WasmMsg::Execute { contract_addr: token.to_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient: sender.to_string(), // more tokens than the DAO posseses. amount: Uint128::new(101), @@ -306,6 +309,8 @@ fn test_v1_v2_full_migration() { }), false, ); + + // now migrate with valid config app.execute_contract( sender.clone(), proposal.clone(), @@ -316,7 +321,7 @@ fn test_v1_v2_full_migration() { WasmMsg::Migrate { contract_addr: core.to_string(), new_code_id: v2_core_code, - msg: to_binary(&dao_interface::msg::MigrateMsg::FromV1 { + msg: to_json_binary(&dao_interface::msg::MigrateMsg::FromV1 { dao_uri: Some("dao-uri".to_string()), params: None, }) @@ -326,9 +331,15 @@ fn test_v1_v2_full_migration() { WasmMsg::Migrate { contract_addr: proposal.to_string(), new_code_id: v2_proposal_code, - msg: to_binary(&crate::msg::MigrateMsg::FromV1 { + msg: to_json_binary(&crate::msg::MigrateMsg::FromV1 { close_proposal_on_execution_failure: true, pre_propose_info, + veto: Some(VetoConfig { + timelock_duration: Duration::Height(10), + vetoer: sender.to_string(), + early_execute: true, + veto_before_passed: false, + }), }) .unwrap(), } @@ -370,6 +381,12 @@ fn test_v1_v2_full_migration() { let count = query_proposal_count(&app, &proposal); assert_eq!(count, 3); + let migrated_existing_props = query_list_proposals(&app, &proposal, None, None); + // assert that even though we migrate with a veto config, + // existing proposals are not affected + for prop in migrated_existing_props.proposals { + assert_eq!(prop.proposal.veto, None); + } // ---- // check that proposal module counts have been updated. // ---- @@ -413,7 +430,7 @@ fn test_v1_v2_full_migration() { sender.as_str(), vec![WasmMsg::Execute { contract_addr: core.to_string(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::UpdateCw20List { + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::UpdateCw20List { to_add: vec![], to_remove: vec![token.into_string()], }) @@ -429,6 +446,18 @@ fn test_v1_v2_full_migration() { 4, dao_voting::voting::Vote::Yes, ); + + let new_prop = query_proposal(&app, &proposal, 4); + assert_eq!( + new_prop.proposal.veto, + Some(VetoConfig { + timelock_duration: Duration::Height(10), + vetoer: sender.to_string(), + early_execute: true, + veto_before_passed: false, + }) + ); + execute_proposal(&mut app, &proposal, sender.as_str(), 4); let tokens: Vec = app .wrap() diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index fff0baa98..63ddd5b9f 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -1,8 +1,10 @@ +use std::ops::Add; + use cosmwasm_std::{ coins, testing::{mock_dependencies, mock_env}, - to_binary, Addr, Attribute, BankMsg, Binary, ContractInfoResponse, CosmosMsg, Decimal, Empty, - Reply, StdError, SubMsgResult, Uint128, WasmMsg, WasmQuery, + to_json_binary, Addr, Attribute, BankMsg, Binary, ContractInfoResponse, CosmosMsg, Decimal, + Empty, Reply, StdError, SubMsgResult, Uint128, WasmMsg, WasmQuery, }; use cw2::ContractVersion; use cw20::Cw20Coin; @@ -24,10 +26,10 @@ use dao_voting::{ mask_proposal_hook_index, mask_vote_hook_index, }, status::Status, - threshold::{PercentageThreshold, Threshold}, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, + veto::{VetoConfig, VetoError}, voting::{Vote, Votes}, }; -use dao_voting_cw20_staked::msg::ActiveThreshold; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, @@ -36,11 +38,7 @@ use crate::{ query::{ProposalResponse, VoteInfo}, state::Config, testing::{ - contracts::{ - cw20_base_contract, cw20_stake_contract, cw20_staked_balances_voting_contract, - cw_core_contract, pre_propose_single_contract, proposal_single_contract, - v1_proposal_single_contract, - }, + contracts::{pre_propose_single_contract, proposal_single_contract}, execute::{ add_proposal_hook, add_proposal_hook_should_fail, add_vote_hook, add_vote_hook_should_fail, close_proposal, close_proposal_should_fail, @@ -131,6 +129,7 @@ fn test_simple_propose_staked_balances() { total_power: Uint128::new(100_000_000), msgs: vec![], status: Status::Open, + veto: None, votes: Votes::zero(), }; @@ -180,6 +179,7 @@ fn test_simple_proposal_cw4_voting() { total_power: Uint128::new(1), msgs: vec![], status: Status::Open, + veto: None, votes: Votes::zero(), }; @@ -285,6 +285,7 @@ fn test_instantiate_with_non_voting_module_cw20_deposit() { msgs: vec![], status: Status::Open, votes: Votes::zero(), + veto: None, }; assert_eq!(created.proposal, expected); @@ -303,14 +304,1207 @@ fn test_instantiate_with_non_voting_module_cw20_deposit() { refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed }) ); -} - -#[test] -fn test_proposal_message_execution() { - let mut app = App::default(); - let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); - instantiate.close_proposal_on_execution_failure = false; - let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); +} + +#[test] +fn test_proposal_message_execution() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(cw20_balance, Uint128::zero()); + assert_eq!(native_balance, Uint128::zero()); + + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Passed); + + // Can't use library function because we expect this to fail due + // to insufficent balance in the bank module. + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap_err(); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Passed); + + mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); + execute_proposal(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Executed); + + let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(cw20_balance, Uint128::new(20_000_000)); + assert_eq!(native_balance, Uint128::new(10)); + + // Sneak in a check here that proposals can't be executed more + // than once in the on close on execute config suituation. + let err = execute_proposal_should_fail(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); + assert!(matches!(err, ContractError::NotPassed {})) +} + +#[test] +fn test_proposal_message_timelock_execution() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.close_proposal_on_execution_failure = false; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + Cw20Coin { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(cw20_balance, Uint128::zero()); + assert_eq!(native_balance, Uint128::zero()); + + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); + + // vetoer can't execute when timelock is active and + // early execute not enabled. + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::VetoError(VetoError::NoEarlyExecute {})); + + // Proposal cannot be excuted before timelock expires + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::Timelocked {})); + + // Time passes + app.update_block(|block| { + block.time = block.time.plus_seconds(604800 + 200); + }); + + // Proposal executes successfully + execute_proposal(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Executed); + + Ok(()) +} + +// only the authorized vetoer can veto an open proposal +#[test] +fn test_open_proposal_veto_unauthorized() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + // only the vetoer can veto + let err: ContractError = app + .execute_contract( + Addr::unchecked("not-oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::VetoError(VetoError::Unauthorized {})); +} + +// open proposal can only be vetoed if `veto_before_passed` flag is enabled +#[test] +fn test_open_proposal_veto_with_early_veto_flag_disabled() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::VetoError(VetoError::NoVetoBeforePassed {}) + ); +} + +#[test] +fn test_open_proposal_veto_with_no_timelock() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + instantiate.veto = None; + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::VetoError(VetoError::NoVetoConfiguration {}) + ); +} + +// if proposal is not open or timelocked, attempts to veto should +// throw an error +#[test] +fn test_vetoed_proposal_veto() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + app.execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Vetoed {}); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!( + ContractError::VetoError(VetoError::InvalidProposalStatus { + status: "vetoed".to_string() + }), + err, + ); +} + +#[test] +fn test_open_proposal_veto_early() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + app.execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Vetoed {}); +} + +// only the vetoer can veto during timelock period +#[test] +fn test_timelocked_proposal_veto_unauthorized() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + Cw20Coin { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("not-oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::Unauthorized {}),); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + Ok(()) +} + +// vetoer can only veto the proposal before the timelock expires +#[test] +fn test_timelocked_proposal_veto_expired_timelock() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + Cw20Coin { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + app.update_block(|b| b.time = b.time.plus_seconds(604800 + 200)); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::TimelockExpired {}),); + + Ok(()) +} + +// vetoer can only exec timelocked prop if the early exec flag is enabled +#[test] +fn test_timelocked_proposal_execute_no_early_exec() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::NoEarlyExecute {}),); + + Ok(()) +} + +#[test] +fn test_timelocked_proposal_execute_early() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + // assert timelock is active + assert!(!veto_config + .timelock_duration + .after(&app.block_info()) + .is_expired(&app.block_info())); + mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); + + app.execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Executed {}); + + Ok(()) +} + +// only vetoer can exec timelocked prop early +#[test] +fn test_timelocked_proposal_execute_active_timelock_unauthorized() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + // assert timelock is active + assert!(!veto_config + .timelock_duration + .after(&app.block_info()) + .is_expired(&app.block_info())); + + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + + assert_eq!(err, ContractError::VetoError(VetoError::Timelocked {}),); + + Ok(()) +} + +// anyone can exec the prop after the timelock expires +#[test] +fn test_timelocked_proposal_execute_expired_timelock_not_vetoer() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + let expiration = proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?; + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { expiration } + ); + + app.update_block(|b| b.time = b.time.plus_seconds(604800 + 201)); + // assert timelock is expired + assert!(expiration.is_expired(&app.block_info())); + mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); + + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + proposal_module.clone(), + &ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Executed {},); + + Ok(()) +} + +#[test] +fn test_proposal_message_timelock_veto() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); + let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); + assert_eq!(cw20_balance, Uint128::zero()); + assert_eq!(native_balance, Uint128::zero()); + + // Vetoer can't veto early + let err: ContractError = app + .execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + ContractError::VetoError(VetoError::NoVetoBeforePassed {}) + ); + + // Vote on proposal to pass it + vote_on_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + proposal_id, + Vote::Yes, + ); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); + + // Non-vetoer cannot veto + let err: ContractError = app + .execute_contract( + Addr::unchecked(CREATOR_ADDR), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::VetoError(VetoError::Unauthorized {})); + + // Oversite vetos prop + app.execute_contract( + Addr::unchecked("oversight"), + proposal_module.clone(), + &ExecuteMsg::Veto { proposal_id }, + &[], + ) + .unwrap(); + + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Vetoed); + + Ok(()) +} + +#[test] +fn test_proposal_message_timelock_early_execution() -> anyhow::Result<()> { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + let veto_config = VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: true, + veto_before_passed: false, + }; + instantiate.veto = Some(veto_config.clone()); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + Cw20Coin { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); let proposal_module = query_single_proposal_module(&app, &core_addr); let gov_token = query_dao_token(&app, &core_addr); @@ -322,7 +1516,7 @@ fn test_proposal_message_execution() { vec![ WasmMsg::Execute { contract_addr: gov_token.to_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::Mint { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { recipient: CREATOR_ADDR.to_string(), amount: Uint128::new(10_000_000), }) @@ -350,34 +1544,103 @@ fn test_proposal_message_execution() { Vote::Yes, ); let proposal = query_proposal(&app, &proposal_module, proposal_id); - assert_eq!(proposal.proposal.status, Status::Passed); - // Can't use library function because we expect this to fail due - // to insufficent balance in the bank module. + // Proposal is timelocked to the moment of prop expiring + timelock delay + assert_eq!( + proposal.proposal.status, + Status::VetoTimelock { + expiration: proposal + .proposal + .expiration + .add(veto_config.timelock_duration)?, + } + ); + + mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); + + // Proposal can be executed early by vetoer + execute_proposal(&mut app, &proposal_module, "oversight", proposal_id); + let proposal = query_proposal(&app, &proposal_module, proposal_id); + assert_eq!(proposal.proposal.status, Status::Executed); + + Ok(()) +} + +#[test] +fn test_proposal_message_timelock_veto_before_passed() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.close_proposal_on_execution_failure = false; + instantiate.veto = Some(VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: "oversight".to_string(), + early_execute: false, + veto_before_passed: true, + }); + let core_addr = instantiate_with_staked_balances_governance( + &mut app, + instantiate, + Some(vec![ + Cw20Coin { + address: "oversight".to_string(), + amount: Uint128::new(15), + }, + Cw20Coin { + address: CREATOR_ADDR.to_string(), + amount: Uint128::new(85), + }, + ]), + ); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let gov_token = query_dao_token(&app, &core_addr); + + mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); + let proposal_id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![ + WasmMsg::Execute { + contract_addr: gov_token.to_string(), + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Mint { + recipient: CREATOR_ADDR.to_string(), + amount: Uint128::new(10_000_000), + }) + .unwrap(), + funds: vec![], + } + .into(), + BankMsg::Send { + to_address: CREATOR_ADDR.to_string(), + amount: coins(10, "ujuno"), + } + .into(), + ], + ); + + let proposal = query_proposal(&app, &proposal_module, proposal_id); + + // Proposal is open for voting + assert_eq!(proposal.proposal.status, Status::Open); + + // Oversite vetos prop app.execute_contract( - Addr::unchecked(CREATOR_ADDR), + Addr::unchecked("oversight"), proposal_module.clone(), - &ExecuteMsg::Execute { proposal_id }, + &ExecuteMsg::Veto { proposal_id }, &[], ) - .unwrap_err(); - let proposal = query_proposal(&app, &proposal_module, proposal_id); - assert_eq!(proposal.proposal.status, Status::Passed); + .unwrap(); - mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); - execute_proposal(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); let proposal = query_proposal(&app, &proposal_module, proposal_id); - assert_eq!(proposal.proposal.status, Status::Executed); + assert_eq!(proposal.proposal.status, Status::Vetoed); - let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); - let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); - assert_eq!(cw20_balance, Uint128::new(20_000_000)); - assert_eq!(native_balance, Uint128::new(10)); + // mint_natives(&mut app, core_addr.as_str(), coins(10, "ujuno")); - // Sneak in a check here that proposals can't be executed more - // than once in the on close on execute config suituation. - let err = execute_proposal_should_fail(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); - assert!(matches!(err, ContractError::NotPassed {})) + // // Proposal can be executed early by vetoer + // execute_proposal(&mut app, &proposal_module, "oversight", proposal_id); + // let proposal = query_proposal(&app, &proposal_module, proposal_id); + // assert_eq!(proposal.proposal.status, Status::Executed); } #[test] @@ -401,7 +1664,7 @@ fn test_proposal_close_after_expiry() { assert!(matches!(err, ContractError::WrongCloseStatus {})); // Expire the proposal. Now it should be closable. - app.update_block(|mut b| b.time = b.time.plus_seconds(604800)); + app.update_block(|b| b.time = b.time.plus_seconds(604800)); close_proposal(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); let proposal = query_proposal(&app, &proposal_module, proposal_id); assert_eq!(proposal.proposal.status, Status::Closed); @@ -445,9 +1708,9 @@ fn test_proposal_cant_close_after_expiry_is_passed() { assert_eq!(proposal.proposal.status, Status::Open); // Expire the proposal. This should pass it. - app.update_block(|mut b| b.time = b.time.plus_seconds(604800)); + app.update_block(|b| b.time = b.time.plus_seconds(604800)); let proposal = query_proposal(&app, &proposal_module, proposal_id); - assert_eq!(proposal.proposal.status, Status::Passed); + assert_eq!(proposal.proposal.status, Status::Passed,); // Make sure it can't be closed. let err = close_proposal_should_fail(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); @@ -482,7 +1745,7 @@ fn test_execute_no_non_passed_execution() { assert!(matches!(err, ContractError::NotPassed {})); // Expire the proposal. - app.update_block(|mut b| b.time = b.time.plus_seconds(604800)); + app.update_block(|b| b.time = b.time.plus_seconds(604800)); let err = execute_proposal_should_fail(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); assert!(matches!(err, ContractError::NotPassed {})); @@ -541,7 +1804,7 @@ fn test_cant_execute_not_member_when_proposal_created() { &cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(10_000_000), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -579,7 +1842,13 @@ fn test_update_config() { CREATOR_ADDR, vec![WasmMsg::Execute { contract_addr: proposal_module.to_string(), - msg: to_binary(&ExecuteMsg::UpdateConfig { + msg: to_json_binary(&ExecuteMsg::UpdateConfig { + veto: Some(VetoConfig { + timelock_duration: Duration::Height(2), + vetoer: CREATOR_ADDR.to_string(), + early_execute: false, + veto_before_passed: false, + }), threshold: Threshold::AbsoluteCount { threshold: Uint128::new(10_000), }, @@ -608,6 +1877,12 @@ fn test_update_config() { assert_eq!( config, Config { + veto: Some(VetoConfig { + timelock_duration: Duration::Height(2), + vetoer: CREATOR_ADDR.to_string(), + early_execute: false, + veto_before_passed: false, + }), threshold: Threshold::AbsoluteCount { threshold: Uint128::new(10_000) }, @@ -624,8 +1899,38 @@ fn test_update_config() { let err: ContractError = app .execute_contract( Addr::unchecked(CREATOR_ADDR), + proposal_module.clone(), + &&ExecuteMsg::UpdateConfig { + veto: None, + threshold: Threshold::AbsoluteCount { + threshold: Uint128::new(10_000), + }, + max_voting_period: Duration::Height(6), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + dao: core_addr.to_string(), + close_proposal_on_execution_failure: false, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert!(matches!(err, ContractError::Unauthorized {})); + + // Check that veto config is validated (mismatching duration units). + let err: ContractError = app + .execute_contract( + Addr::unchecked(core_addr.clone()), proposal_module, &&ExecuteMsg::UpdateConfig { + veto: Some(VetoConfig { + timelock_duration: Duration::Time(100), + vetoer: CREATOR_ADDR.to_string(), + early_execute: false, + veto_before_passed: false, + }), threshold: Threshold::AbsoluteCount { threshold: Uint128::new(10_000), }, @@ -641,7 +1946,10 @@ fn test_update_config() { .unwrap_err() .downcast() .unwrap(); - assert!(matches!(err, ContractError::Unauthorized {})) + assert!(matches!( + err, + ContractError::VetoError(VetoError::TimelockDurationUnitMismatch {}) + )) } #[test] @@ -720,6 +2028,7 @@ fn test_anyone_may_propose_and_proposal_listing() { no: Uint128::zero(), abstain: Uint128::zero() }, + veto: None } } ) @@ -891,7 +2200,7 @@ fn test_active_threshold_absolute() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(100), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract(Addr::unchecked(CREATOR_ADDR), gov_token, &msg, &[]) .unwrap(); @@ -972,7 +2281,7 @@ fn test_active_threshold_percent() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount: Uint128::new(20_000_000), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract(Addr::unchecked(CREATOR_ADDR), gov_token, &msg, &[]) .unwrap(); @@ -1051,7 +2360,7 @@ fn test_min_voting_period_no_early_pass() { let proposal_response = query_proposal(&app, &proposal_module, proposal_id); assert_eq!(proposal_response.proposal.status, Status::Open); - app.update_block(|mut block| block.height += 10); + app.update_block(|block| block.height += 10); let proposal_response = query_proposal(&app, &proposal_module, proposal_id); assert_eq!(proposal_response.proposal.status, Status::Passed); } @@ -1089,7 +2398,7 @@ fn test_min_duration_same_as_proposal_duration() { vote_on_proposal(&mut app, &proposal_module, "whale", proposal_id, Vote::Yes); vote_on_proposal(&mut app, &proposal_module, "ekez", proposal_id, Vote::No); - app.update_block(|mut b| b.height += 100); + app.update_block(|b| b.height += 100); let proposal_response = query_proposal(&app, &proposal_module, proposal_id); assert_eq!(proposal_response.proposal.status, Status::Passed); } @@ -1148,7 +2457,7 @@ fn test_revoting_playthrough() { assert!(matches!(err, ContractError::AlreadyCast {})); // Expire the proposal allowing the votes to be tallied. - app.update_block(|mut b| b.time = b.time.plus_seconds(604800)); + app.update_block(|b| b.time = b.time.plus_seconds(604800)); let proposal_response = query_proposal(&app, &proposal_module, proposal_id); assert_eq!(proposal_response.proposal.status, Status::Passed); execute_proposal(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); @@ -1186,6 +2495,7 @@ fn test_allow_revoting_config_changes() { core_addr.clone(), proposal_module.clone(), &ExecuteMsg::UpdateConfig { + veto: None, threshold: Threshold::ThresholdQuorum { quorum: PercentageThreshold::Percent(Decimal::percent(15)), threshold: PercentageThreshold::Majority {}, @@ -1237,7 +2547,7 @@ fn test_allow_revoting_config_changes() { Vote::No, ); // Expire the revoting proposal and close it. - app.update_block(|mut b| b.time = b.time.plus_seconds(604800)); + app.update_block(|b| b.time = b.time.plus_seconds(604800)); close_proposal(&mut app, &proposal_module, CREATOR_ADDR, revoting_proposal); } @@ -1394,7 +2704,7 @@ fn test_three_of_five_multisig_revoting() { assert!(matches!(err, ContractError::AlreadyCast {})); // Expire the revoting proposal and close it. - app.update_block(|mut b| b.time = b.time.plus_seconds(604800)); + app.update_block(|b| b.time = b.time.plus_seconds(604800)); let proposal: ProposalResponse = query_proposal(&app, &proposal_module, proposal_id); assert_eq!(proposal.proposal.status, Status::Rejected); } @@ -1489,6 +2799,7 @@ fn test_proposal_count_initialized_to_zero() { let core_addr = instantiate_with_staked_balances_governance( &mut app, InstantiateMsg { + veto: None, threshold: Threshold::ThresholdQuorum { threshold: PercentageThreshold::Majority {}, quorum: PercentageThreshold::Percent(Decimal::percent(10)), @@ -1546,7 +2857,7 @@ fn test_migrate_from_compatible() { CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: proposal_module.to_string(), new_code_id, - msg: to_binary(&MigrateMsg::FromCompatible {}).unwrap(), + msg: to_json_binary(&MigrateMsg::FromCompatible {}).unwrap(), }), ) .unwrap(); @@ -1565,292 +2876,277 @@ pub fn test_migrate_updates_version() { assert_eq!(version.contract, CONTRACT_NAME); } -/// Instantiates a DAO with a v1 proposal module and then migrates it -/// to v2. -#[test] -fn test_migrate_from_v1() { - use cw_proposal_single_v1 as v1; - use dao_pre_propose_single as cppbps; - - let mut app = App::default(); - let v1_proposal_single_code = app.store_code(v1_proposal_single_contract()); - - let instantiate = v1::msg::InstantiateMsg { - threshold: voting_v1::Threshold::AbsolutePercentage { - percentage: voting_v1::PercentageThreshold::Majority {}, - }, - max_voting_period: cw_utils_v1::Duration::Height(6), - min_voting_period: None, - only_members_execute: false, - allow_revoting: false, - deposit_info: Some(v1::msg::DepositInfo { - token: v1::msg::DepositToken::VotingModuleToken {}, - deposit: Uint128::new(1), - refund_failed_proposals: true, - }), - }; - - let initial_balances = vec![Cw20Coin { - amount: Uint128::new(100), - address: CREATOR_ADDR.to_string(), - }]; - - let cw20_id = app.store_code(cw20_base_contract()); - let cw20_stake_id = app.store_code(cw20_stake_contract()); - let staked_balances_voting_id = app.store_code(cw20_staked_balances_voting_contract()); - let core_contract_id = app.store_code(cw_core_contract()); - - let instantiate_core = dao_interface::msg::InstantiateMsg { - admin: None, - name: "DAO DAO".to_string(), - description: "A DAO that builds DAOs".to_string(), - image_url: None, - dao_uri: None, - automatically_add_cw20s: true, - automatically_add_cw721s: false, - voting_module_instantiate_info: ModuleInstantiateInfo { - code_id: staked_balances_voting_id, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { - active_threshold: None, - token_info: dao_voting_cw20_staked::msg::TokenInfo::New { - code_id: cw20_id, - label: "DAO DAO governance token.".to_string(), - name: "DAO DAO".to_string(), - symbol: "DAO".to_string(), - decimals: 6, - initial_balances: initial_balances.clone(), - marketing: None, - staking_code_id: cw20_stake_id, - unstaking_duration: Some(Duration::Height(6)), - initial_dao_balance: None, - }, - }) - .unwrap(), - admin: None, - label: "DAO DAO voting module".to_string(), - }, - proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { - code_id: v1_proposal_single_code, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), - msg: to_binary(&instantiate).unwrap(), - }], - initial_items: None, - }; - - let core_addr = app - .instantiate_contract( - core_contract_id, - Addr::unchecked(CREATOR_ADDR), - &instantiate_core, - &[], - "DAO DAO", - None, - ) - .unwrap(); - - let core_state: dao_interface::query::DumpStateResponse = app - .wrap() - .query_wasm_smart( - core_addr.clone(), - &dao_interface::msg::QueryMsg::DumpState {}, - ) - .unwrap(); - let voting_module = core_state.voting_module; - - let staking_contract: Addr = app - .wrap() - .query_wasm_smart( - voting_module.clone(), - &dao_voting_cw20_staked::msg::QueryMsg::StakingContract {}, - ) - .unwrap(); - let token_contract: Addr = app - .wrap() - .query_wasm_smart( - voting_module, - &dao_interface::voting::Query::TokenContract {}, - ) - .unwrap(); - - // Stake all the initial balances. - for Cw20Coin { address, amount } in initial_balances { - app.execute_contract( - Addr::unchecked(address), - token_contract.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: staking_contract.to_string(), - amount, - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), - }, - &[], - ) - .unwrap(); - } - - // Update the block so that those staked balances appear. - app.update_block(|block| block.height += 1); - - let proposal_module = query_single_proposal_module(&app, &core_addr); - - // Make a proposal so we can test that migration doesn't work with - // open proposals that have deposits. - mint_cw20s(&mut app, &token_contract, &core_addr, CREATOR_ADDR, 1); - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - token_contract.clone(), - &cw20::Cw20ExecuteMsg::IncreaseAllowance { - spender: proposal_module.to_string(), - amount: Uint128::new(1), - expires: None, - }, - &[], - ) - .unwrap(); - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - proposal_module.clone(), - &v1::msg::ExecuteMsg::Propose { - title: "title".to_string(), - description: "description".to_string(), - msgs: vec![], - }, - &[], - ) - .unwrap(); - - let v2_proposal_single = app.store_code(proposal_single_contract()); - let pre_propose_single = app.store_code(pre_propose_single_contract()); - - // Attempt to migrate. This will fail as there is a pending - // proposal. - let migrate_msg = MigrateMsg::FromV1 { - close_proposal_on_execution_failure: true, - pre_propose_info: PreProposeInfo::ModuleMayPropose { - info: ModuleInstantiateInfo { - code_id: pre_propose_single, - msg: to_binary(&dao_pre_propose_single::InstantiateMsg { - deposit_info: Some(UncheckedDepositInfo { - denom: dao_voting::deposit::DepositToken::VotingModuleToken {}, - amount: Uint128::new(1), - refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed, - }), - open_proposal_submission: false, - extension: Empty::default(), - }) - .unwrap(), - admin: Some(Admin::CoreModule {}), - label: "DAO DAO pre-propose".to_string(), - }, - }, - }; - let err: ContractError = app - .execute( - core_addr.clone(), - CosmosMsg::Wasm(WasmMsg::Migrate { - contract_addr: proposal_module.to_string(), - new_code_id: v2_proposal_single, - msg: to_binary(&migrate_msg).unwrap(), - }), - ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!(err, ContractError::PendingProposals {})); - - // Vote on and close the pending proposal. - vote_on_proposal(&mut app, &proposal_module, CREATOR_ADDR, 1, Vote::No); - close_proposal(&mut app, &proposal_module, CREATOR_ADDR, 1); - - // Now we can migrate! - app.execute( - core_addr.clone(), - CosmosMsg::Wasm(WasmMsg::Migrate { - contract_addr: proposal_module.to_string(), - new_code_id: v2_proposal_single, - msg: to_binary(&migrate_msg).unwrap(), - }), - ) - .unwrap(); - - let new_config = query_proposal_config(&app, &proposal_module); - assert_eq!( - new_config, - Config { - threshold: Threshold::AbsolutePercentage { - percentage: PercentageThreshold::Majority {} - }, - max_voting_period: Duration::Height(6), - min_voting_period: None, - only_members_execute: false, - allow_revoting: false, - dao: core_addr.clone(), - close_proposal_on_execution_failure: true, - } - ); - - // We can not migrate more than once. - let err: ContractError = app - .execute( - core_addr.clone(), - CosmosMsg::Wasm(WasmMsg::Migrate { - contract_addr: proposal_module.to_string(), - new_code_id: v2_proposal_single, - msg: to_binary(&migrate_msg).unwrap(), - }), - ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!(err, ContractError::AlreadyMigrated {})); - - // Make sure we can still query for ballots (rationale works post - // migration). - let vote = query_vote(&app, &proposal_module, CREATOR_ADDR, 1); - assert_eq!( - vote.vote.unwrap(), - VoteInfo { - voter: Addr::unchecked(CREATOR_ADDR), - vote: Vote::No, - power: Uint128::new(100), - rationale: None - } - ); - - let proposal_creation_policy = query_creation_policy(&app, &proposal_module); - - // Check that a new creation policy has been birthed. - let pre_propose = match proposal_creation_policy { - ProposalCreationPolicy::Anyone {} => panic!("expected a pre-propose module"), - ProposalCreationPolicy::Module { addr } => addr, - }; - let pre_propose_config = query_pre_proposal_single_config(&app, &pre_propose); - assert_eq!( - pre_propose_config, - cppbps::Config { - open_proposal_submission: false, - deposit_info: Some(CheckedDepositInfo { - denom: CheckedDenom::Cw20(token_contract.clone()), - amount: Uint128::new(1), - refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed, - }) - } - ); - - // Make sure we can still make a proposal and vote on it. - mint_cw20s(&mut app, &token_contract, &core_addr, CREATOR_ADDR, 1); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); - vote_on_proposal( - &mut app, - &proposal_module, - CREATOR_ADDR, - proposal_id, - Vote::Yes, - ); - execute_proposal(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); - let proposal = query_proposal(&app, &proposal_module, proposal_id); - assert_eq!(proposal.proposal.status, Status::Executed); -} +// //// TODO test migrate +// /// Instantiates a DAO with a v1 proposal module and then migrates it +// /// to v2. +// #[test] +// fn test_migrate_from_v1() { +// use cw_proposal_single_v1 as v1; +// use dao_pre_propose_single as cppbps; + +// let mut app = App::default(); +// let v1_proposal_single_code = app.store_code(v1_proposal_single_contract()); + +// let instantiate = v1::msg::InstantiateMsg { +// threshold: voting_v1::Threshold::AbsolutePercentage { +// percentage: voting_v1::PercentageThreshold::Majority {}, +// }, +// max_voting_period: cw_utils_v1::Duration::Height(6), +// min_voting_period: None, +// only_members_execute: false, +// allow_revoting: false, +// deposit_info: Some(v1::msg::DepositInfo { +// token: v1::msg::DepositToken::VotingModuleToken {}, +// deposit: Uint128::new(1), +// refund_failed_proposals: true, +// }), +// }; + +// let initial_balances = vec![Cw20Coin { +// amount: Uint128::new(100), +// address: CREATOR_ADDR.to_string(), +// }]; + +// let cw20_id = app.store_code(cw20_base_contract()); +// let cw20_stake_id = app.store_code(cw20_stake_contract()); +// let staked_balances_voting_id = app.store_code(cw20_staked_balances_voting_contract()); +// let core_contract_id = app.store_code(cw_core_contract()); + +// let instantiate_core = dao_interface::msg::InstantiateMsg { +// admin: None, +// name: "DAO DAO".to_string(), +// description: "A DAO that builds DAOs".to_string(), +// image_url: None, +// dao_uri: None, +// automatically_add_cw20s: true, +// automatically_add_cw721s: false, +// voting_module_instantiate_info: ModuleInstantiateInfo { +// code_id: staked_balances_voting_id, +// msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { +// active_threshold: None, +// token_info: dao_voting_cw20_staked::msg::TokenInfo::New { +// code_id: cw20_id, +// label: "DAO DAO governance token.".to_string(), +// name: "DAO DAO".to_string(), +// symbol: "DAO".to_string(), +// decimals: 6, +// initial_balances: initial_balances.clone(), +// marketing: None, +// staking_code_id: cw20_stake_id, +// unstaking_duration: Some(Duration::Height(6)), +// initial_dao_balance: None, +// }, +// }) +// .unwrap(), +// admin: None, +// funds: vec![], +// label: "DAO DAO voting module".to_string(), +// }, +// proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { +// code_id: v1_proposal_single_code, +// msg: to_json_binary(&instantiate).unwrap(), +// admin: Some(Admin::CoreModule {}), +// funds: vec![], +// label: "DAO DAO governance module.".to_string(), +// }], +// initial_items: None, +// }; + +// let core_addr = app +// .instantiate_contract( +// core_contract_id, +// Addr::unchecked(CREATOR_ADDR), +// &instantiate_core, +// &[], +// "DAO DAO", +// None, +// ) +// .unwrap(); + +// let core_state: dao_interface::query::DumpStateResponse = app +// .wrap() +// .query_wasm_smart( +// core_addr.clone(), +// &dao_interface::msg::QueryMsg::DumpState {}, +// ) +// .unwrap(); +// let voting_module = core_state.voting_module; + +// let staking_contract: Addr = app +// .wrap() +// .query_wasm_smart( +// voting_module.clone(), +// &dao_voting_cw20_staked::msg::QueryMsg::StakingContract {}, +// ) +// .unwrap(); +// let token_contract: Addr = app +// .wrap() +// .query_wasm_smart( +// voting_module, +// &dao_interface::voting::Query::TokenContract {}, +// ) +// .unwrap(); + +// // Stake all the initial balances. +// for Cw20Coin { address, amount } in initial_balances { +// app.execute_contract( +// Addr::unchecked(address), +// token_contract.clone(), +// &cw20::Cw20ExecuteMsg::Send { +// contract: staking_contract.to_string(), +// amount, +// msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), +// }, +// &[], +// ) +// .unwrap(); +// } + +// // Update the block so that those staked balances appear. +// app.update_block(|block| block.height += 1); + +// let proposal_module = query_single_proposal_module(&app, &core_addr); + +// // Make a proposal so we can test that migration doesn't work with +// // open proposals that have deposits. +// mint_cw20s(&mut app, &token_contract, &core_addr, CREATOR_ADDR, 1); +// app.execute_contract( +// Addr::unchecked(CREATOR_ADDR), +// token_contract.clone(), +// &cw20::Cw20ExecuteMsg::IncreaseAllowance { +// spender: proposal_module.to_string(), +// amount: Uint128::new(1), +// expires: None, +// }, +// &[], +// ) +// .unwrap(); +// app.execute_contract( +// Addr::unchecked(CREATOR_ADDR), +// proposal_module.clone(), +// &v1::msg::ExecuteMsg::Propose { +// title: "title".to_string(), +// description: "description".to_string(), +// msgs: vec![], +// }, +// &[], +// ) +// .unwrap(); + +// let v2_proposal_single = app.store_code(proposal_single_contract()); +// let pre_propose_single = app.store_code(pre_propose_single_contract()); + +// // Attempt to migrate. This will fail as there is a pending +// // proposal. +// let migrate_msg = MigrateMsg::FromV2 { timelock: None }; +// let err: ContractError = app +// .execute( +// core_addr.clone(), +// CosmosMsg::Wasm(WasmMsg::Migrate { +// contract_addr: proposal_module.to_string(), +// new_code_id: v2_proposal_single, +// msg: to_json_binary(&migrate_msg).unwrap(), +// }), +// ) +// .unwrap_err() +// .downcast() +// .unwrap(); +// assert!(matches!(err, ContractError::PendingProposals {})); + +// // Vote on and close the pending proposal. +// vote_on_proposal(&mut app, &proposal_module, CREATOR_ADDR, 1, Vote::No); +// close_proposal(&mut app, &proposal_module, CREATOR_ADDR, 1); + +// // Now we can migrate! +// app.execute( +// core_addr.clone(), +// CosmosMsg::Wasm(WasmMsg::Migrate { +// contract_addr: proposal_module.to_string(), +// new_code_id: v2_proposal_single, +// msg: to_json_binary(&migrate_msg).unwrap(), +// }), +// ) +// .unwrap(); + +// let new_config = query_proposal_config(&app, &proposal_module); +// assert_eq!( +// new_config, +// Config { +// timelock: None, +// threshold: Threshold::AbsolutePercentage { +// percentage: PercentageThreshold::Majority {} +// }, +// max_voting_period: Duration::Height(6), +// min_voting_period: None, +// only_members_execute: false, +// allow_revoting: false, +// dao: core_addr.clone(), +// close_proposal_on_execution_failure: true, +// } +// ); + +// // We can not migrate more than once. +// let err: ContractError = app +// .execute( +// core_addr.clone(), +// CosmosMsg::Wasm(WasmMsg::Migrate { +// contract_addr: proposal_module.to_string(), +// new_code_id: v2_proposal_single, +// msg: to_json_binary(&migrate_msg).unwrap(), +// }), +// ) +// .unwrap_err() +// .downcast() +// .unwrap(); +// assert!(matches!(err, ContractError::AlreadyMigrated {})); + +// // Make sure we can still query for ballots (rationale works post +// // migration). +// let vote = query_vote(&app, &proposal_module, CREATOR_ADDR, 1); +// assert_eq!( +// vote.vote.unwrap(), +// VoteInfo { +// voter: Addr::unchecked(CREATOR_ADDR), +// vote: Vote::No, +// power: Uint128::new(100), +// rationale: None +// } +// ); + +// let proposal_creation_policy = query_creation_policy(&app, &proposal_module); + +// // Check that a new creation policy has been birthed. +// let pre_propose = match proposal_creation_policy { +// ProposalCreationPolicy::Anyone {} => panic!("expected a pre-propose module"), +// ProposalCreationPolicy::Module { addr } => addr, +// }; +// let pre_propose_config = query_pre_proposal_single_config(&app, &pre_propose); +// assert_eq!( +// pre_propose_config, +// cppbps::Config { +// open_proposal_submission: false, +// deposit_info: Some(CheckedDepositInfo { +// denom: CheckedDenom::Cw20(token_contract.clone()), +// amount: Uint128::new(1), +// refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed, +// }) +// } +// ); + +// // Make sure we can still make a proposal and vote on it. +// mint_cw20s(&mut app, &token_contract, &core_addr, CREATOR_ADDR, 1); +// let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); +// vote_on_proposal( +// &mut app, +// &proposal_module, +// CREATOR_ADDR, +// proposal_id, +// Vote::Yes, +// ); +// execute_proposal(&mut app, &proposal_module, CREATOR_ADDR, proposal_id); +// let proposal = query_proposal(&app, &proposal_module, proposal_id); +// assert_eq!(proposal.proposal.status, Status::Executed); +// } // - Make a proposal that will fail to execute. // - Verify that it goes to execution failed and that proposal @@ -1911,6 +3207,7 @@ fn test_execution_failed() { core_addr, proposal_module.clone(), &ExecuteMsg::UpdateConfig { + veto: None, threshold: config.threshold, max_voting_period: config.max_voting_period, min_voting_period: config.min_voting_period, @@ -1982,6 +3279,7 @@ fn test_reply_proposal_mock() { total_power: Uint128::new(100_000_000), msgs: vec![], status: Status::Open, + veto: None, votes: Votes::zero(), }, ) @@ -2395,11 +3693,11 @@ fn test_update_pre_propose_module() { CREATOR_ADDR, vec![WasmMsg::Execute { contract_addr: proposal_module.to_string(), - msg: to_binary(&ExecuteMsg::UpdatePreProposeInfo { + msg: to_json_binary(&ExecuteMsg::UpdatePreProposeInfo { info: PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_id, - msg: to_binary(&dao_pre_propose_single::InstantiateMsg { + msg: to_json_binary(&dao_pre_propose_single::InstantiateMsg { deposit_info: Some(UncheckedDepositInfo { denom: dao_voting::deposit::DepositToken::VotingModuleToken {}, amount: Uint128::new(1), @@ -2410,6 +3708,7 @@ fn test_update_pre_propose_module() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "new pre-propose module".to_string(), }, }, @@ -2492,7 +3791,8 @@ fn test_update_pre_propose_module() { CREATOR_ADDR, vec![WasmMsg::Execute { contract_addr: pre_propose_start.into_string(), - msg: to_binary(&dao_pre_propose_single::ExecuteMsg::Withdraw { denom: None }).unwrap(), + msg: to_json_binary(&dao_pre_propose_single::ExecuteMsg::Withdraw { denom: None }) + .unwrap(), funds: vec![], } .into()], @@ -2636,7 +3936,7 @@ pub fn test_not_allow_voting_on_expired_proposal() { } = setup_test(vec![]); // expire the proposal - app.update_block(|mut b| b.time = b.time.plus_seconds(604800)); + app.update_block(|b| b.time = b.time.plus_seconds(604800)); let proposal = query_proposal(&app, &proposal_module, proposal_id); assert_eq!(proposal.proposal.status, Status::Rejected); assert_eq!(proposal.proposal.votes.yes, Uint128::zero()); diff --git a/contracts/staking/cw20-stake-external-rewards/Cargo.toml b/contracts/staking/cw20-stake-external-rewards/Cargo.toml index 1a7dd7cf1..b648436c1 100644 --- a/contracts/staking/cw20-stake-external-rewards/Cargo.toml +++ b/contracts/staking/cw20-stake-external-rewards/Cargo.toml @@ -28,6 +28,7 @@ cw2 = { workspace = true } thiserror = { workspace = true } cw20-stake = { workspace = true, features = ["library"]} cw-ownable = { workspace = true } +dao-hooks = { workspace = true } cw20-stake-external-rewards-v1 = { workspace = true } cw20-013 = { package = "cw20", version = "0.13" } diff --git a/contracts/staking/cw20-stake-external-rewards/README.md b/contracts/staking/cw20-stake-external-rewards/README.md index 34128376d..8bdc8245c 100644 --- a/contracts/staking/cw20-stake-external-rewards/README.md +++ b/contracts/staking/cw20-stake-external-rewards/README.md @@ -1,4 +1,7 @@ # CW20 Stake External Rewards +[![cw20-stake-external-rewards on crates.io](https://img.shields.io/crates/v/cw20-stake-external-rewards.svg?logo=rust)](https://crates.io/crates/cw20-stake-external-rewards) +[![docs.rs](https://img.shields.io/docsrs/cw20-stake-external-rewards?logo=docsdotrs)](https://docs.rs/cw20-stake-external-rewards/latest/cw20_stake_external_rewards/) + This contract enables staking rewards in terms of non-governance tokens. diff --git a/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json b/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json index af3bc2f44..613aa7585 100644 --- a/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json +++ b/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json @@ -1,6 +1,6 @@ { "contract_name": "cw20-stake-external-rewards", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -286,6 +286,7 @@ ] }, "StakeChangedHookMsg": { + "description": "An enum representing staking hooks.", "oneOf": [ { "type": "object", diff --git a/contracts/staking/cw20-stake-external-rewards/src/contract.rs b/contracts/staking/cw20-stake-external-rewards/src/contract.rs index a2a50412d..e92b0b6d9 100644 --- a/contracts/staking/cw20-stake-external-rewards/src/contract.rs +++ b/contracts/staking/cw20-stake-external-rewards/src/contract.rs @@ -14,12 +14,12 @@ use crate::ContractError::{ use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Empty, Env, + from_json, to_json_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, StdResult, Uint128, Uint256, WasmMsg, }; use cw2::{get_contract_version, set_contract_version, ContractVersion}; use cw20::{Cw20ReceiveMsg, Denom}; -use cw20_stake::hooks::StakeChangedHookMsg; +use dao_hooks::stake::StakeChangedHookMsg; use cw20::Denom::Cw20; use std::cmp::min; @@ -143,7 +143,7 @@ pub fn execute_receive( info: MessageInfo, wrapper: Cw20ReceiveMsg, ) -> Result, ContractError> { - let msg: ReceiveMsg = from_binary(&wrapper.msg)?; + let msg: ReceiveMsg = from_json(&wrapper.msg)?; let config = CONFIG.load(deps.storage)?; let sender = deps.api.addr_validate(&wrapper.sender)?; if config.reward_token != Denom::Cw20(info.sender) { @@ -280,7 +280,7 @@ pub fn get_transfer_msg(recipient: Addr, amount: Uint128, denom: Denom) -> StdRe } .into()), Denom::Cw20(addr) => { - let cw20_msg = to_binary(&cw20::Cw20ExecuteMsg::Transfer { + let cw20_msg = to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient: recipient.into_string(), amount, })?; @@ -414,11 +414,11 @@ fn scale_factor() -> Uint256 { #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Info {} => Ok(to_binary(&query_info(deps, env)?)?), + QueryMsg::Info {} => Ok(to_json_binary(&query_info(deps, env)?)?), QueryMsg::GetPendingRewards { address } => { - Ok(to_binary(&query_pending_rewards(deps, env, address)?)?) + Ok(to_json_binary(&query_pending_rewards(deps, env, address)?)?) } - QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), + QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), } } @@ -462,7 +462,7 @@ mod tests { use crate::{msg::MigrateMsg, ContractError}; - use cosmwasm_std::{coin, to_binary, Addr, Empty, Uint128, WasmMsg}; + use cosmwasm_std::{coin, to_json_binary, Addr, Empty, Uint128, WasmMsg}; use cw20::{Cw20Coin, Cw20ExecuteMsg, Denom}; use cw_ownable::{Action, Ownership, OwnershipError}; use cw_utils::Duration; @@ -566,7 +566,7 @@ mod tests { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_addr.to_string(), amount: Uint128::new(amount), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract(Addr::unchecked(sender), cw20_addr.clone(), &msg, &[]) .unwrap(); @@ -679,7 +679,7 @@ mod tests { reward_addr: &Addr, amount: u128, ) { - let fund_sub_msg = to_binary(&ReceiveMsg::Fund {}).unwrap(); + let fund_sub_msg = to_json_binary(&ReceiveMsg::Fund {}).unwrap(); let fund_msg = Cw20ExecuteMsg::Send { contract: reward_addr.clone().into_string(), amount: Uint128::new(amount), @@ -1680,7 +1680,7 @@ mod tests { amount: Uint128::new(500000000), }], ); - let fund_sub_msg = to_binary(&ReceiveMsg::Fund {}).unwrap(); + let fund_sub_msg = to_json_binary(&ReceiveMsg::Fund {}).unwrap(); let fund_msg = Cw20ExecuteMsg::Send { contract: reward_addr.into_string(), amount: Uint128::new(100), @@ -1733,7 +1733,7 @@ mod tests { app.borrow_mut().update_block(|b| b.height = 1000); // Test with invalid token - let fund_sub_msg = to_binary(&ReceiveMsg::Fund {}).unwrap(); + let fund_sub_msg = to_json_binary(&ReceiveMsg::Fund {}).unwrap(); let fund_msg = Cw20ExecuteMsg::Send { contract: reward_addr.clone().into_string(), amount: Uint128::new(100), @@ -2003,7 +2003,7 @@ mod tests { WasmMsg::Migrate { contract_addr: rewards_addr.to_string(), new_code_id: v2_code, - msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(), + msg: to_json_binary(&MigrateMsg::FromV1 {}).unwrap(), } .into(), ) @@ -2025,7 +2025,7 @@ mod tests { WasmMsg::Migrate { contract_addr: rewards_addr.to_string(), new_code_id: v2_code, - msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(), + msg: to_json_binary(&MigrateMsg::FromV1 {}).unwrap(), } .into(), ) diff --git a/contracts/staking/cw20-stake-external-rewards/src/msg.rs b/contracts/staking/cw20-stake-external-rewards/src/msg.rs index 565508e39..eb873c798 100644 --- a/contracts/staking/cw20-stake-external-rewards/src/msg.rs +++ b/contracts/staking/cw20-stake-external-rewards/src/msg.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::Uint128; use cw20::{Cw20ReceiveMsg, Denom}; -use cw20_stake::hooks::StakeChangedHookMsg; +use dao_hooks::stake::StakeChangedHookMsg; use crate::state::{Config, RewardConfig}; diff --git a/contracts/staking/cw20-stake-reward-distributor/README.md b/contracts/staking/cw20-stake-reward-distributor/README.md index 46119b78e..df3bb736b 100644 --- a/contracts/staking/cw20-stake-reward-distributor/README.md +++ b/contracts/staking/cw20-stake-reward-distributor/README.md @@ -1,5 +1,8 @@ # CW20 Stake Reward Distributor +[![cw20-stake-reward-distributor on crates.io](https://img.shields.io/crates/v/cw20-stake-reward-distributor.svg?logo=rust)](https://crates.io/crates/cw20-stake-reward-distributor) +[![docs.rs](https://img.shields.io/docsrs/cw20-stake-reward-distributor?logo=docsdotrs)](https://docs.rs/cw20-stake-reward-distributor/latest/cw20_stake_reward_distributor/) + A contract to fund cw20-stake contracts with rewards in terms of the same tokens being staked. diff --git a/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json b/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json index a816d65f7..c50272c06 100644 --- a/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json +++ b/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json @@ -1,6 +1,6 @@ { "contract_name": "cw20-stake-reward-distributor", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/staking/cw20-stake-reward-distributor/src/contract.rs b/contracts/staking/cw20-stake-reward-distributor/src/contract.rs index d4108d88c..66636add1 100644 --- a/contracts/staking/cw20-stake-reward-distributor/src/contract.rs +++ b/contracts/staking/cw20-stake-reward-distributor/src/contract.rs @@ -2,7 +2,7 @@ use std::cmp::min; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdError, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, CosmosMsg, StdError, Uint128, WasmMsg}; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InfoResponse, InstantiateMsg, MigrateMsg, QueryMsg}; @@ -162,10 +162,10 @@ fn get_distribution_msg(deps: Deps, env: &Env) -> Result StdResult { match msg { - QueryMsg::Info {} => to_binary(&query_info(deps, env)?), - QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), + QueryMsg::Info {} => to_json_binary(&query_info(deps, env)?), + QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), } } diff --git a/contracts/staking/cw20-stake-reward-distributor/src/tests.rs b/contracts/staking/cw20-stake-reward-distributor/src/tests.rs index 61056d39e..7b6bde529 100644 --- a/contracts/staking/cw20-stake-reward-distributor/src/tests.rs +++ b/contracts/staking/cw20-stake-reward-distributor/src/tests.rs @@ -6,7 +6,7 @@ use crate::{ use cw20_stake_reward_distributor_v1 as v1; -use cosmwasm_std::{to_binary, Addr, Empty, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, Empty, Uint128, WasmMsg}; use cw20::Cw20Coin; use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor}; use cw_ownable::{Action, Expiration, Ownership, OwnershipError}; @@ -246,7 +246,7 @@ fn test_distribute() { app.execute_contract(Addr::unchecked(OWNER), cw20_addr.clone(), &msg, &[]) .unwrap(); - app.update_block(|mut block| block.height += 10); + app.update_block(|block| block.height += 10); app.execute_contract( Addr::unchecked(OWNER), distributor_addr.clone(), @@ -262,7 +262,7 @@ fn test_distribute() { assert_eq!(distributor_info.balance, Uint128::new(990)); assert_eq!(distributor_info.last_payment_block, app.block_info().height); - app.update_block(|mut block| block.height += 500); + app.update_block(|block| block.height += 500); app.execute_contract( Addr::unchecked(OWNER), distributor_addr.clone(), @@ -278,7 +278,7 @@ fn test_distribute() { assert_eq!(distributor_info.balance, Uint128::new(490)); assert_eq!(distributor_info.last_payment_block, app.block_info().height); - app.update_block(|mut block| block.height += 1000); + app.update_block(|block| block.height += 1000); app.execute_contract( Addr::unchecked(OWNER), distributor_addr.clone(), @@ -296,7 +296,7 @@ fn test_distribute() { let last_payment_block = distributor_info.last_payment_block; // Pays out nothing - app.update_block(|mut block| block.height += 1100); + app.update_block(|block| block.height += 1100); let err: ContractError = app .execute_contract( Addr::unchecked(OWNER), @@ -318,7 +318,7 @@ fn test_distribute() { assert_eq!(distributor_info.last_payment_block, last_payment_block); // go to a block before the last payment - app.update_block(|mut block| block.height -= 2000); + app.update_block(|block| block.height -= 2000); let err: ContractError = app .execute_contract( Addr::unchecked(OWNER), @@ -458,7 +458,7 @@ fn test_withdraw() { app.execute_contract(Addr::unchecked(OWNER), cw20_addr.clone(), &msg, &[]) .unwrap(); - app.update_block(|mut block| block.height += 10); + app.update_block(|block| block.height += 10); app.execute_contract( Addr::unchecked(OWNER), distributor_addr.clone(), @@ -541,7 +541,7 @@ fn test_dao_deploy() { app.execute_contract(Addr::unchecked(OWNER), cw20_addr.clone(), &msg, &[]) .unwrap(); - app.update_block(|mut block| block.height += 10); + app.update_block(|block| block.height += 10); app.execute_contract( Addr::unchecked(OWNER), distributor_addr.clone(), @@ -711,7 +711,7 @@ fn test_migrate_from_v1() { WasmMsg::Migrate { contract_addr: distributor.to_string(), new_code_id: v2_code, - msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(), + msg: to_json_binary(&MigrateMsg::FromV1 {}).unwrap(), } .into(), ) @@ -747,7 +747,7 @@ fn test_migrate_from_v1() { WasmMsg::Migrate { contract_addr: distributor.to_string(), new_code_id: v2_code, - msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(), + msg: to_json_binary(&MigrateMsg::FromV1 {}).unwrap(), } .into(), ) diff --git a/contracts/staking/cw20-stake/Cargo.toml b/contracts/staking/cw20-stake/Cargo.toml index a3706eed7..20acf9375 100644 --- a/contracts/staking/cw20-stake/Cargo.toml +++ b/contracts/staking/cw20-stake/Cargo.toml @@ -21,6 +21,7 @@ cosmwasm-storage = { workspace = true } cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } cw-controllers = { workspace = true } +cw-hooks = { workspace = true } cw20 = { workspace = true } cw-utils = { workspace = true } cw20-base = { workspace = true, features = ["library"] } @@ -28,6 +29,8 @@ cw2 = { workspace = true } thiserror = { workspace = true } cw-paginate-storage = { workspace = true } cw-ownable = { workspace = true } +dao-hooks = { workspace = true } +dao-voting = { workspace = true } cw20-stake-v1 = { workspace = true, features = ["library"] } cw-utils-v1 = { workspace = true } diff --git a/contracts/staking/cw20-stake/README.md b/contracts/staking/cw20-stake/README.md index a359ac18a..5142d1ec0 100644 --- a/contracts/staking/cw20-stake/README.md +++ b/contracts/staking/cw20-stake/README.md @@ -1,5 +1,8 @@ # CW20 Stake +[![cw20-stake on crates.io](https://img.shields.io/crates/v/cw20-stake.svg?logo=rust)](https://crates.io/crates/cw20-stake) +[![docs.rs](https://img.shields.io/docsrs/cw20-stake?logo=docsdotrs)](https://docs.rs/cw20-stake/latest/cw20_stake/) + This is a basic implementation of a cw20 staking contract. Staked tokens can be unbonded with a configurable unbonding period. Staked balances can be queried at any arbitrary height by external contracts. diff --git a/contracts/staking/cw20-stake/schema/cw20-stake.json b/contracts/staking/cw20-stake/schema/cw20-stake.json index 285f8387a..634491fe4 100644 --- a/contracts/staking/cw20-stake/schema/cw20-stake.json +++ b/contracts/staking/cw20-stake/schema/cw20-stake.json @@ -1,6 +1,6 @@ { "contract_name": "cw20-stake", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/staking/cw20-stake/src/contract.rs b/contracts/staking/cw20-stake/src/contract.rs index d25417f87..64af7482a 100644 --- a/contracts/staking/cw20-stake/src/contract.rs +++ b/contracts/staking/cw20-stake/src/contract.rs @@ -2,24 +2,11 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, + from_json, to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, StdResult, Uint128, }; - -use cw20::{Cw20ReceiveMsg, TokenInfoResponse}; - -use crate::hooks::{stake_hook_msgs, unstake_hook_msgs}; -use crate::math; -use crate::msg::{ - ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, - ReceiveMsg, StakedBalanceAtHeightResponse, StakedValueResponse, StakerBalanceResponse, - TotalStakedAtHeightResponse, TotalValueResponse, -}; -use crate::state::{ - Config, BALANCE, CLAIMS, CONFIG, HOOKS, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL, -}; -use crate::ContractError; use cw2::{get_contract_version, set_contract_version, ContractVersion}; +use cw20::{Cw20ReceiveMsg, TokenInfoResponse}; pub use cw20_base::allowances::{ execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from, execute_transfer_from, query_allowance, @@ -32,28 +19,23 @@ pub use cw20_base::contract::{ pub use cw20_base::enumerable::{query_all_accounts, query_owner_allowances}; use cw_controllers::ClaimsResponse; use cw_utils::Duration; +use dao_hooks::stake::{stake_hook_msgs, unstake_hook_msgs}; +use dao_voting::duration::validate_duration; + +use crate::math; +use crate::msg::{ + ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, + ReceiveMsg, StakedBalanceAtHeightResponse, StakedValueResponse, StakerBalanceResponse, + TotalStakedAtHeightResponse, TotalValueResponse, +}; +use crate::state::{ + Config, BALANCE, CLAIMS, CONFIG, HOOKS, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL, +}; +use crate::ContractError; pub(crate) const CONTRACT_NAME: &str = "crates.io:cw20-stake"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -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, @@ -147,7 +129,7 @@ pub fn execute_receive( expected: config.token_address, }); } - let msg: ReceiveMsg = from_binary(&wrapper.msg)?; + let msg: ReceiveMsg = from_json(&wrapper.msg)?; let sender = deps.api.addr_validate(&wrapper.sender)?; match msg { ReceiveMsg::Stake {} => execute_stake(deps, env, sender, wrapper.amount), @@ -182,7 +164,7 @@ pub fn execute_stake( deps.storage, &balance.checked_add(amount).map_err(StdError::overflow)?, )?; - let hook_msgs = stake_hook_msgs(deps.storage, sender.clone(), amount_to_stake)?; + let hook_msgs = stake_hook_msgs(HOOKS, deps.storage, sender.clone(), amount_to_stake)?; Ok(Response::new() .add_submessages(hook_msgs) .add_attribute("action", "stake") @@ -230,7 +212,7 @@ pub fn execute_unstake( .checked_sub(amount_to_claim) .map_err(StdError::overflow)?, )?; - let hook_msgs = unstake_hook_msgs(deps.storage, info.sender.clone(), amount)?; + let hook_msgs = unstake_hook_msgs(HOOKS, deps.storage, info.sender.clone(), amount)?; match config.unstaking_duration { None => { let cw_send_msg = cw20::Cw20ExecuteMsg::Transfer { @@ -239,7 +221,7 @@ pub fn execute_unstake( }; let wasm_msg = cosmwasm_std::WasmMsg::Execute { contract_addr: config.token_address.to_string(), - msg: to_binary(&cw_send_msg)?, + msg: to_json_binary(&cw_send_msg)?, funds: vec![], }; Ok(Response::new() @@ -288,7 +270,7 @@ pub fn execute_claim( }; let wasm_msg = cosmwasm_std::WasmMsg::Execute { contract_addr: config.token_address.to_string(), - msg: to_binary(&cw_send_msg)?, + msg: to_json_binary(&cw_send_msg)?, funds: vec![], }; Ok(Response::new() @@ -354,21 +336,23 @@ pub fn execute_update_owner( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::GetConfig {} => to_binary(&query_config(deps)?), + QueryMsg::GetConfig {} => to_json_binary(&query_config(deps)?), QueryMsg::StakedBalanceAtHeight { address, height } => { - to_binary(&query_staked_balance_at_height(deps, env, address, height)?) + to_json_binary(&query_staked_balance_at_height(deps, env, address, height)?) } QueryMsg::TotalStakedAtHeight { height } => { - to_binary(&query_total_staked_at_height(deps, env, height)?) + to_json_binary(&query_total_staked_at_height(deps, env, height)?) + } + QueryMsg::StakedValue { address } => { + to_json_binary(&query_staked_value(deps, env, address)?) } - QueryMsg::StakedValue { address } => to_binary(&query_staked_value(deps, env, address)?), - QueryMsg::TotalValue {} => to_binary(&query_total_value(deps, env)?), - QueryMsg::Claims { address } => to_binary(&query_claims(deps, address)?), - QueryMsg::GetHooks {} => to_binary(&query_hooks(deps)?), + QueryMsg::TotalValue {} => to_json_binary(&query_total_value(deps, env)?), + QueryMsg::Claims { address } => to_json_binary(&query_claims(deps, address)?), + QueryMsg::GetHooks {} => to_json_binary(&query_hooks(deps)?), QueryMsg::ListStakers { start_after, limit } => { query_list_stakers(deps, start_after, limit) } - QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?), + QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), } } @@ -468,7 +452,7 @@ pub fn query_list_stakers( }) .collect(); - to_binary(&ListStakersResponse { stakers }) + to_json_binary(&ListStakersResponse { stakers }) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/staking/cw20-stake/src/error.rs b/contracts/staking/cw20-stake/src/error.rs index cd7140a21..394cd0e04 100644 --- a/contracts/staking/cw20-stake/src/error.rs +++ b/contracts/staking/cw20-stake/src/error.rs @@ -5,29 +5,40 @@ use thiserror::Error; pub enum ContractError { #[error(transparent)] Std(#[from] StdError), + #[error(transparent)] Cw20Error(#[from] cw20_base::ContractError), + #[error(transparent)] Ownership(#[from] cw_ownable::OwnershipError), + #[error(transparent)] - HookError(#[from] cw_controllers::HookError), + HookError(#[from] cw_hooks::HookError), + + #[error(transparent)] + UnstakingDurationError(#[from] dao_voting::duration::UnstakingDurationError), + + #[error("can not migrate. current version is up to date")] + AlreadyMigrated {}, - #[error("Provided cw20 errored in response to TokenInfo query")] - InvalidCw20 {}, - #[error("Nothing to claim")] - NothingToClaim {}, - #[error("Nothing to unstake")] - NothingStaked {}, #[error("Unstaking this amount violates the invariant: (cw20 total_supply <= 2^128)")] Cw20InvaraintViolation {}, + #[error("Can not unstake more than has been staked")] ImpossibleUnstake {}, + + #[error("Provided cw20 errored in response to TokenInfo query")] + InvalidCw20 {}, + #[error("Invalid token")] InvalidToken { received: Addr, expected: Addr }, + + #[error("Nothing to claim")] + NothingToClaim {}, + + #[error("Nothing to unstake")] + NothingStaked {}, + #[error("Too many outstanding claims. Claim some tokens before unstaking more.")] TooManyClaims {}, - #[error("Invalid unstaking duration, unstaking duration cannot be 0")] - InvalidUnstakingDuration {}, - #[error("can not migrate. current version is up to date")] - AlreadyMigrated {}, } diff --git a/contracts/staking/cw20-stake/src/lib.rs b/contracts/staking/cw20-stake/src/lib.rs index 6688af2cd..e8bfc90e0 100644 --- a/contracts/staking/cw20-stake/src/lib.rs +++ b/contracts/staking/cw20-stake/src/lib.rs @@ -2,7 +2,6 @@ pub mod contract; mod error; -pub mod hooks; mod math; pub mod msg; pub mod state; diff --git a/contracts/staking/cw20-stake/src/state.rs b/contracts/staking/cw20-stake/src/state.rs index 2ebb1c887..5323d9008 100644 --- a/contracts/staking/cw20-stake/src/state.rs +++ b/contracts/staking/cw20-stake/src/state.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; use cw_controllers::Claims; -use cw_controllers::Hooks; +use cw_hooks::Hooks; use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; use cw_utils::Duration; diff --git a/contracts/staking/cw20-stake/src/tests.rs b/contracts/staking/cw20-stake/src/tests.rs index 81ae03b1a..147bd806f 100644 --- a/contracts/staking/cw20-stake/src/tests.rs +++ b/contracts/staking/cw20-stake/src/tests.rs @@ -1,3 +1,13 @@ +use anyhow::Result as AnyResult; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{to_json_binary, Addr, Empty, MessageInfo, Uint128, WasmMsg}; +use cw20::Cw20Coin; +use cw_controllers::{Claim, ClaimsResponse}; +use cw_multi_test::{next_block, App, AppResponse, Contract, ContractWrapper, Executor}; +use cw_ownable::{Action, Ownership, OwnershipError}; +use cw_utils::Duration; +use cw_utils::Expiration::AtHeight; +use dao_voting::duration::UnstakingDurationError; use std::borrow::BorrowMut; use crate::msg::{ @@ -7,20 +17,9 @@ use crate::msg::{ }; use crate::state::{Config, MAX_CLAIMS}; use crate::ContractError; -use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{to_binary, Addr, Empty, MessageInfo, Uint128, WasmMsg}; -use cw20::Cw20Coin; -use cw_ownable::{Action, Ownership, OwnershipError}; -use cw_utils::Duration; - -use cw_multi_test::{next_block, App, AppResponse, Contract, ContractWrapper, Executor}; -use anyhow::Result as AnyResult; use cw20_stake_v1 as v1; -use cw_controllers::{Claim, ClaimsResponse}; -use cw_utils::Expiration::AtHeight; - const ADDR1: &str = "addr0001"; const ADDR2: &str = "addr0002"; const ADDR3: &str = "addr0003"; @@ -191,7 +190,7 @@ fn stake_tokens( let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_addr.to_string(), amount, - msg: to_binary(&ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract(info.sender, cw20_addr.clone(), &msg, &[]) } @@ -273,14 +272,20 @@ fn test_update_config() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, ContractError::InvalidUnstakingDuration {}); + assert_eq!( + err, + ContractError::UnstakingDurationError(UnstakingDurationError::InvalidUnstakingDuration {}) + ); let info = mock_info(OWNER, &[]); let err: ContractError = update_config(&mut app, &staking_addr, info, Some(Duration::Time(0))) .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, ContractError::InvalidUnstakingDuration {}); + assert_eq!( + err, + ContractError::UnstakingDurationError(UnstakingDurationError::InvalidUnstakingDuration {}) + ); } #[test] @@ -648,7 +653,7 @@ fn test_auto_compounding_staking() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_addr.to_string(), amount: Uint128::from(100u128), - msg: to_binary(&ReceiveMsg::Fund {}).unwrap(), + msg: to_json_binary(&ReceiveMsg::Fund {}).unwrap(), }; let _res = app .borrow_mut() @@ -720,7 +725,7 @@ fn test_auto_compounding_staking() { let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_addr.to_string(), amount: Uint128::from(90u128), - msg: to_binary(&ReceiveMsg::Fund {}).unwrap(), + msg: to_json_binary(&ReceiveMsg::Fund {}).unwrap(), }; let _res = app .borrow_mut() @@ -1146,7 +1151,7 @@ fn test_migrate_from_v1() { WasmMsg::Migrate { contract_addr: staking.to_string(), new_code_id: v2_code, - msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(), + msg: to_json_binary(&MigrateMsg::FromV1 {}).unwrap(), } .into(), ) @@ -1159,7 +1164,7 @@ fn test_migrate_from_v1() { WasmMsg::Migrate { contract_addr: staking.to_string(), new_code_id: v2_code, - msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(), + msg: to_json_binary(&MigrateMsg::FromV1 {}).unwrap(), } .into(), ) diff --git a/test-contracts/dao-proposal-hook-counter/.cargo/config b/contracts/test/dao-proposal-hook-counter/.cargo/config similarity index 100% rename from test-contracts/dao-proposal-hook-counter/.cargo/config rename to contracts/test/dao-proposal-hook-counter/.cargo/config diff --git a/contracts/dao-dao-core/.gitignore b/contracts/test/dao-proposal-hook-counter/.gitignore similarity index 100% rename from contracts/dao-dao-core/.gitignore rename to contracts/test/dao-proposal-hook-counter/.gitignore diff --git a/test-contracts/dao-proposal-hook-counter/Cargo.toml b/contracts/test/dao-proposal-hook-counter/Cargo.toml similarity index 93% rename from test-contracts/dao-proposal-hook-counter/Cargo.toml rename to contracts/test/dao-proposal-hook-counter/Cargo.toml index 5effd61ef..395fb338a 100644 --- a/test-contracts/dao-proposal-hook-counter/Cargo.toml +++ b/contracts/test/dao-proposal-hook-counter/Cargo.toml @@ -22,8 +22,7 @@ cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } cw2 = { workspace = true } thiserror = { workspace = true } -dao-proposal-hooks = { workspace = true } -dao-vote-hooks = { workspace = true } +dao-hooks = { workspace = true } [dev-dependencies] cw-hooks = { workspace = true } diff --git a/test-contracts/dao-proposal-hook-counter/README.md b/contracts/test/dao-proposal-hook-counter/README.md similarity index 100% rename from test-contracts/dao-proposal-hook-counter/README.md rename to contracts/test/dao-proposal-hook-counter/README.md diff --git a/test-contracts/dao-proposal-hook-counter/examples/schema.rs b/contracts/test/dao-proposal-hook-counter/examples/schema.rs similarity index 100% rename from test-contracts/dao-proposal-hook-counter/examples/schema.rs rename to contracts/test/dao-proposal-hook-counter/examples/schema.rs diff --git a/test-contracts/dao-proposal-hook-counter/schema/execute_msg.json b/contracts/test/dao-proposal-hook-counter/schema/execute_msg.json similarity index 100% rename from test-contracts/dao-proposal-hook-counter/schema/execute_msg.json rename to contracts/test/dao-proposal-hook-counter/schema/execute_msg.json diff --git a/test-contracts/dao-proposal-hook-counter/schema/instantiate_msg.json b/contracts/test/dao-proposal-hook-counter/schema/instantiate_msg.json similarity index 100% rename from test-contracts/dao-proposal-hook-counter/schema/instantiate_msg.json rename to contracts/test/dao-proposal-hook-counter/schema/instantiate_msg.json diff --git a/test-contracts/dao-proposal-hook-counter/schema/query_msg.json b/contracts/test/dao-proposal-hook-counter/schema/query_msg.json similarity index 100% rename from test-contracts/dao-proposal-hook-counter/schema/query_msg.json rename to contracts/test/dao-proposal-hook-counter/schema/query_msg.json diff --git a/test-contracts/dao-proposal-hook-counter/src/contract.rs b/contracts/test/dao-proposal-hook-counter/src/contract.rs similarity index 63% rename from test-contracts/dao-proposal-hook-counter/src/contract.rs rename to contracts/test/dao-proposal-hook-counter/src/contract.rs index 2a9163d02..3aaefca3d 100644 --- a/test-contracts/dao-proposal-hook-counter/src/contract.rs +++ b/contracts/test/dao-proposal-hook-counter/src/contract.rs @@ -1,13 +1,17 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cosmwasm_std::{ + to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, +}; use cw2::set_contract_version; -use dao_proposal_hooks::ProposalHookMsg; -use dao_vote_hooks::VoteHookMsg; +use dao_hooks::stake::StakeChangedHookMsg; +use dao_hooks::{proposal::ProposalHookMsg, vote::VoteHookMsg}; use crate::error::ContractError; use crate::msg::{CountResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{Config, CONFIG, PROPOSAL_COUNTER, STATUS_CHANGED_COUNTER, VOTE_COUNTER}; +use crate::state::{ + Config, CONFIG, PROPOSAL_COUNTER, STAKE_COUNTER, STATUS_CHANGED_COUNTER, VOTE_COUNTER, +}; const CONTRACT_NAME: &str = "crates.io:proposal-hooks-counter"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -26,6 +30,7 @@ pub fn instantiate( }; CONFIG.save(deps.storage, &config)?; PROPOSAL_COUNTER.save(deps.storage, &0)?; + STAKE_COUNTER.save(deps.storage, &Uint128::zero())?; VOTE_COUNTER.save(deps.storage, &0)?; STATUS_CHANGED_COUNTER.save(deps.storage, &0)?; Ok(Response::new().add_attribute("action", "instantiate")) @@ -47,6 +52,7 @@ pub fn execute( ExecuteMsg::ProposalHook(proposal_hook) => { execute_proposal_hook(deps, env, info, proposal_hook) } + ExecuteMsg::StakeChangeHook(stake_hook) => execute_stake_hook(deps, env, info, stake_hook), ExecuteMsg::VoteHook(vote_hook) => execute_vote_hook(deps, env, info, vote_hook), } } @@ -60,12 +66,12 @@ pub fn execute_proposal_hook( match proposal_hook { ProposalHookMsg::NewProposal { .. } => { let mut count = PROPOSAL_COUNTER.load(deps.storage)?; - count += 1; + count = count.checked_add(1).unwrap_or_default(); PROPOSAL_COUNTER.save(deps.storage, &count)?; } ProposalHookMsg::ProposalStatusChanged { .. } => { let mut count = STATUS_CHANGED_COUNTER.load(deps.storage)?; - count += 1; + count = count.checked_add(1).unwrap_or_default(); STATUS_CHANGED_COUNTER.save(deps.storage, &count)?; } } @@ -73,6 +79,28 @@ pub fn execute_proposal_hook( Ok(Response::new().add_attribute("action", "proposal_hook")) } +pub fn execute_stake_hook( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + stake_hook: StakeChangedHookMsg, +) -> Result { + match stake_hook { + StakeChangedHookMsg::Stake { .. } => { + let mut count = STAKE_COUNTER.load(deps.storage)?; + count = count.checked_add(Uint128::new(1))?; + STAKE_COUNTER.save(deps.storage, &count)?; + } + StakeChangedHookMsg::Unstake { .. } => { + let mut count = STAKE_COUNTER.load(deps.storage)?; + count = count.checked_add(Uint128::new(1))?; + STAKE_COUNTER.save(deps.storage, &count)?; + } + } + + Ok(Response::new().add_attribute("action", "stake_hook")) +} + pub fn execute_vote_hook( deps: DepsMut, _env: Env, @@ -82,7 +110,7 @@ pub fn execute_vote_hook( match vote_hook { VoteHookMsg::NewVote { .. } => { let mut count = VOTE_COUNTER.load(deps.storage)?; - count += 1; + count = count.checked_add(1).unwrap_or_default(); VOTE_COUNTER.save(deps.storage, &count)?; } } @@ -93,14 +121,15 @@ pub fn execute_vote_hook( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::VoteCounter {} => to_binary(&CountResponse { - count: VOTE_COUNTER.load(deps.storage)?, - }), - QueryMsg::ProposalCounter {} => to_binary(&CountResponse { + QueryMsg::ProposalCounter {} => to_json_binary(&CountResponse { count: PROPOSAL_COUNTER.load(deps.storage)?, }), - QueryMsg::StatusChangedCounter {} => to_binary(&CountResponse { + QueryMsg::StakeCounter {} => to_json_binary(&STAKE_COUNTER.load(deps.storage)?), + QueryMsg::StatusChangedCounter {} => to_json_binary(&CountResponse { count: STATUS_CHANGED_COUNTER.load(deps.storage)?, }), + QueryMsg::VoteCounter {} => to_json_binary(&CountResponse { + count: VOTE_COUNTER.load(deps.storage)?, + }), } } diff --git a/test-contracts/dao-proposal-sudo/src/error.rs b/contracts/test/dao-proposal-hook-counter/src/error.rs similarity index 60% rename from test-contracts/dao-proposal-sudo/src/error.rs rename to contracts/test/dao-proposal-hook-counter/src/error.rs index dc19f1033..0e7ceb05e 100644 --- a/test-contracts/dao-proposal-sudo/src/error.rs +++ b/contracts/test/dao-proposal-hook-counter/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{OverflowError, StdError}; use thiserror::Error; #[derive(Error, Debug)] @@ -6,6 +6,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error(transparent)] + OverflowError(#[from] OverflowError), + #[error("Unauthorized")] Unauthorized {}, } diff --git a/test-contracts/dao-proposal-hook-counter/src/lib.rs b/contracts/test/dao-proposal-hook-counter/src/lib.rs similarity index 100% rename from test-contracts/dao-proposal-hook-counter/src/lib.rs rename to contracts/test/dao-proposal-hook-counter/src/lib.rs diff --git a/test-contracts/dao-proposal-hook-counter/src/msg.rs b/contracts/test/dao-proposal-hook-counter/src/msg.rs similarity index 71% rename from test-contracts/dao-proposal-hook-counter/src/msg.rs rename to contracts/test/dao-proposal-hook-counter/src/msg.rs index 825c57bf4..ad6048228 100644 --- a/test-contracts/dao-proposal-hook-counter/src/msg.rs +++ b/contracts/test/dao-proposal-hook-counter/src/msg.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use dao_proposal_hooks::ProposalHookMsg; -use dao_vote_hooks::VoteHookMsg; +use cosmwasm_std::Uint128; +use dao_hooks::{proposal::ProposalHookMsg, stake::StakeChangedHookMsg, vote::VoteHookMsg}; #[cw_serde] pub struct InstantiateMsg { @@ -10,12 +10,15 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { ProposalHook(ProposalHookMsg), + StakeChangeHook(StakeChangedHookMsg), VoteHook(VoteHookMsg), } #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { + #[returns(Uint128)] + StakeCounter {}, #[returns(u64)] VoteCounter {}, #[returns(u64)] diff --git a/test-contracts/dao-proposal-hook-counter/src/state.rs b/contracts/test/dao-proposal-hook-counter/src/state.rs similarity index 80% rename from test-contracts/dao-proposal-hook-counter/src/state.rs rename to contracts/test/dao-proposal-hook-counter/src/state.rs index 4f22cb0e7..6bdbdfa23 100644 --- a/test-contracts/dao-proposal-hook-counter/src/state.rs +++ b/contracts/test/dao-proposal-hook-counter/src/state.rs @@ -1,4 +1,5 @@ use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint128; use cw_storage_plus::Item; #[cw_serde] @@ -6,6 +7,7 @@ pub struct Config { pub should_error: bool, } pub const CONFIG: Item = Item::new("config"); -pub const VOTE_COUNTER: Item = Item::new("vote_counter"); pub const PROPOSAL_COUNTER: Item = Item::new("proposal_counter"); +pub const STAKE_COUNTER: Item = Item::new("stake_counter"); pub const STATUS_CHANGED_COUNTER: Item = Item::new("stauts_changed_counter"); +pub const VOTE_COUNTER: Item = Item::new("vote_counter"); diff --git a/test-contracts/dao-proposal-hook-counter/src/tests.rs b/contracts/test/dao-proposal-hook-counter/src/tests.rs similarity index 98% rename from test-contracts/dao-proposal-hook-counter/src/tests.rs rename to contracts/test/dao-proposal-hook-counter/src/tests.rs index cb3483a81..9ca8effed 100644 --- a/test-contracts/dao-proposal-hook-counter/src/tests.rs +++ b/contracts/test/dao-proposal-hook-counter/src/tests.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{to_binary, Addr, Empty, Uint128}; +use cosmwasm_std::{to_json_binary, Addr, Empty, Uint128}; use cw20::Cw20Coin; use cw_hooks::HooksResponse; use cw_multi_test::{App, Contract, ContractWrapper, Executor}; @@ -108,7 +108,7 @@ fn instantiate_with_default_governance( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw20_balance::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_balance::msg::InstantiateMsg { token_info: dao_voting_cw20_balance::msg::TokenInfo::New { code_id: cw20_id, label: "DAO DAO governance token".to_string(), @@ -121,12 +121,14 @@ fn instantiate_with_default_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id, - msg: to_binary(&msg).unwrap(), + msg: to_json_binary(&msg).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -153,6 +155,7 @@ fn test_counters() { allow_revoting: false, pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, close_proposal_on_execution_failure: true, + veto: None, }; let governance_addr = diff --git a/test-contracts/dao-proposal-sudo/.cargo/config b/contracts/test/dao-proposal-sudo/.cargo/config similarity index 100% rename from test-contracts/dao-proposal-sudo/.cargo/config rename to contracts/test/dao-proposal-sudo/.cargo/config diff --git a/contracts/voting/dao-voting-cw20-staked/.gitignore b/contracts/test/dao-proposal-sudo/.gitignore similarity index 100% rename from contracts/voting/dao-voting-cw20-staked/.gitignore rename to contracts/test/dao-proposal-sudo/.gitignore diff --git a/test-contracts/dao-proposal-sudo/Cargo.toml b/contracts/test/dao-proposal-sudo/Cargo.toml similarity index 100% rename from test-contracts/dao-proposal-sudo/Cargo.toml rename to contracts/test/dao-proposal-sudo/Cargo.toml diff --git a/test-contracts/dao-proposal-sudo/README b/contracts/test/dao-proposal-sudo/README similarity index 100% rename from test-contracts/dao-proposal-sudo/README rename to contracts/test/dao-proposal-sudo/README diff --git a/test-contracts/dao-proposal-sudo/examples/schema.rs b/contracts/test/dao-proposal-sudo/examples/schema.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/examples/schema.rs rename to contracts/test/dao-proposal-sudo/examples/schema.rs diff --git a/test-contracts/dao-proposal-sudo/schema/admin_response.json b/contracts/test/dao-proposal-sudo/schema/admin_response.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/admin_response.json rename to contracts/test/dao-proposal-sudo/schema/admin_response.json diff --git a/test-contracts/dao-proposal-sudo/schema/dao_response.json b/contracts/test/dao-proposal-sudo/schema/dao_response.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/dao_response.json rename to contracts/test/dao-proposal-sudo/schema/dao_response.json diff --git a/test-contracts/dao-proposal-sudo/schema/execute_msg.json b/contracts/test/dao-proposal-sudo/schema/execute_msg.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/execute_msg.json rename to contracts/test/dao-proposal-sudo/schema/execute_msg.json diff --git a/test-contracts/dao-proposal-sudo/schema/info_response.json b/contracts/test/dao-proposal-sudo/schema/info_response.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/info_response.json rename to contracts/test/dao-proposal-sudo/schema/info_response.json diff --git a/test-contracts/dao-proposal-sudo/schema/instantiate_msg.json b/contracts/test/dao-proposal-sudo/schema/instantiate_msg.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/instantiate_msg.json rename to contracts/test/dao-proposal-sudo/schema/instantiate_msg.json diff --git a/test-contracts/dao-proposal-sudo/schema/query_msg.json b/contracts/test/dao-proposal-sudo/schema/query_msg.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/query_msg.json rename to contracts/test/dao-proposal-sudo/schema/query_msg.json diff --git a/test-contracts/dao-proposal-sudo/src/contract.rs b/contracts/test/dao-proposal-sudo/src/contract.rs similarity index 86% rename from test-contracts/dao-proposal-sudo/src/contract.rs rename to contracts/test/dao-proposal-sudo/src/contract.rs index 82df228e3..902a0ee34 100644 --- a/test-contracts/dao-proposal-sudo/src/contract.rs +++ b/contracts/test/dao-proposal-sudo/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult, WasmMsg, }; use cw2::set_contract_version; @@ -59,7 +59,7 @@ pub fn execute_execute( let msg = WasmMsg::Execute { contract_addr: dao.to_string(), - msg: to_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs })?, + msg: to_json_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs })?, funds: vec![], }; @@ -78,14 +78,14 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } pub fn query_admin(deps: Deps) -> StdResult { - to_binary(&ROOT.load(deps.storage)?) + to_json_binary(&ROOT.load(deps.storage)?) } pub fn query_dao(deps: Deps) -> StdResult { - to_binary(&DAO.load(deps.storage)?) + to_json_binary(&DAO.load(deps.storage)?) } pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; - to_binary(&dao_interface::voting::InfoResponse { info }) + to_json_binary(&dao_interface::voting::InfoResponse { info }) } diff --git a/test-contracts/dao-proposal-hook-counter/src/error.rs b/contracts/test/dao-proposal-sudo/src/error.rs similarity index 100% rename from test-contracts/dao-proposal-hook-counter/src/error.rs rename to contracts/test/dao-proposal-sudo/src/error.rs diff --git a/test-contracts/dao-proposal-sudo/src/lib.rs b/contracts/test/dao-proposal-sudo/src/lib.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/src/lib.rs rename to contracts/test/dao-proposal-sudo/src/lib.rs diff --git a/test-contracts/dao-proposal-sudo/src/msg.rs b/contracts/test/dao-proposal-sudo/src/msg.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/src/msg.rs rename to contracts/test/dao-proposal-sudo/src/msg.rs diff --git a/test-contracts/dao-proposal-sudo/src/state.rs b/contracts/test/dao-proposal-sudo/src/state.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/src/state.rs rename to contracts/test/dao-proposal-sudo/src/state.rs diff --git a/test-contracts/dao-voting-cw20-balance/.cargo/config b/contracts/test/dao-test-custom-factory/.cargo/config similarity index 100% rename from test-contracts/dao-voting-cw20-balance/.cargo/config rename to contracts/test/dao-test-custom-factory/.cargo/config diff --git a/contracts/voting/dao-voting-native-staked/Cargo.toml b/contracts/test/dao-test-custom-factory/Cargo.toml similarity index 65% rename from contracts/voting/dao-voting-native-staked/Cargo.toml rename to contracts/test/dao-test-custom-factory/Cargo.toml index 495e4b215..47ffd045e 100644 --- a/contracts/voting/dao-voting-native-staked/Cargo.toml +++ b/contracts/test/dao-test-custom-factory/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." +name = "dao-test-custom-factory" +authors = ["Jake Hartnell"] +description = "A test contract for testing factory patterns in dao-voting-token-staked and dao-voting-cw721-staked." edition = { workspace = true } license = { workspace = true } repository = { workspace = true } @@ -20,16 +20,17 @@ library = [] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-storage = { workspace = true } -cw-storage-plus = { workspace = true } cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } cw-utils = { workspace = true } -cw-controllers = { workspace = true } - thiserror = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } -cw-paginate-storage = { workspace = true } +dao-voting = { workspace = true } +cw-tokenfactory-issuer = { workspace = true, features = ["library"] } [dev-dependencies] cw-multi-test = { workspace = true } -anyhow = { workspace = true } diff --git a/contracts/test/dao-test-custom-factory/README b/contracts/test/dao-test-custom-factory/README new file mode 100644 index 000000000..61c1265e5 --- /dev/null +++ b/contracts/test/dao-test-custom-factory/README @@ -0,0 +1,2 @@ +# Test Custom Factory contract +Used for testing custom factories with `dao-voting-token-staked` and `dao-voting-cw721-staked`. This also serves an example for how to build custom factory contracts for token or NFT based DAOs. diff --git a/contracts/test/dao-test-custom-factory/examples/schema.rs b/contracts/test/dao-test-custom-factory/examples/schema.rs new file mode 100644 index 000000000..f0a2ba4f4 --- /dev/null +++ b/contracts/test/dao-test-custom-factory/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use dao_test_custom_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/test/dao-test-custom-factory/src/contract.rs b/contracts/test/dao-test-custom-factory/src/contract.rs new file mode 100644 index 000000000..67ad19686 --- /dev/null +++ b/contracts/test/dao-test-custom-factory/src/contract.rs @@ -0,0 +1,503 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Reply, + Response, StdResult, SubMsg, Uint128, WasmMsg, +}; +use cw2::set_contract_version; +use cw721::{Cw721QueryMsg, NumTokensResponse}; +use cw721_base::InstantiateMsg as Cw721InstantiateMsg; +use cw_ownable::Ownership; +use cw_storage_plus::Item; +use cw_tokenfactory_issuer::msg::{ + ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, +}; +use cw_utils::{one_coin, parse_reply_instantiate_data}; +use dao_interface::{ + nft::NftFactoryCallback, + state::ModuleInstantiateCallback, + token::{InitialBalance, NewTokenInfo, TokenFactoryCallback}, + voting::{ActiveThresholdQuery, Query as VotingModuleQueryMsg}, +}; +use dao_voting::threshold::{ + assert_valid_absolute_count_threshold, assert_valid_percentage_threshold, ActiveThreshold, + ActiveThresholdResponse, +}; + +use crate::{ + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, +}; + +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const INSTANTIATE_ISSUER_REPLY_ID: u64 = 1; +const INSTANTIATE_NFT_REPLY_ID: u64 = 2; + +const DAO: Item = Item::new("dao"); +const INITIAL_NFTS: Item> = Item::new("initial_nfts"); +const NFT_CONTRACT: Item = Item::new("nft_contract"); +const VOTING_MODULE: Item = Item::new("voting_module"); +const TOKEN_INFO: Item = Item::new("token_info"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new().add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::NftFactory { + code_id, + cw721_instantiate_msg, + initial_nfts, + } => execute_nft_factory( + deps, + env, + info, + cw721_instantiate_msg, + code_id, + initial_nfts, + ), + ExecuteMsg::NftFactoryWithFunds { + code_id, + cw721_instantiate_msg, + initial_nfts, + } => execute_nft_factory_with_funds( + deps, + env, + info, + cw721_instantiate_msg, + code_id, + initial_nfts, + ), + ExecuteMsg::NftFactoryNoCallback {} => execute_nft_factory_no_callback(deps, env, info), + ExecuteMsg::NftFactoryWrongCallback {} => { + execute_nft_factory_wrong_callback(deps, env, info) + } + ExecuteMsg::TokenFactoryFactory(token) => { + execute_token_factory_factory(deps, env, info, token) + } + ExecuteMsg::TokenFactoryFactoryWithFunds(token) => { + execute_token_factory_factory_with_funds(deps, env, info, token) + } + ExecuteMsg::TokenFactoryFactoryNoCallback {} => { + execute_token_factory_factory_no_callback(deps, env, info) + } + ExecuteMsg::TokenFactoryFactoryWrongCallback {} => { + execute_token_factory_factory_wrong_callback(deps, env, info) + } + ExecuteMsg::ValidateNftDao {} => execute_validate_nft_dao(deps, info), + } +} + +/// An example factory that instantiates a new NFT contract +/// A more realistic example would be something like a minter contract that creates +/// an NFT along with a minter contract for sales like on Stargaze. +pub fn execute_nft_factory( + deps: DepsMut, + _env: Env, + info: MessageInfo, + cw721_instantiate_msg: Cw721InstantiateMsg, + code_id: u64, + initial_nfts: Vec, +) -> Result { + // Save voting module address + VOTING_MODULE.save(deps.storage, &info.sender)?; + + // Query for DAO + let dao: Addr = deps + .querier + .query_wasm_smart(info.sender, &VotingModuleQueryMsg::Dao {})?; + + // Save DAO and TOKEN_INFO for use in replies + DAO.save(deps.storage, &dao)?; + + // Save initial NFTs for use in replies + INITIAL_NFTS.save(deps.storage, &initial_nfts)?; + + // Override minter to be the DAO address + let msg = to_json_binary(&Cw721InstantiateMsg { + name: cw721_instantiate_msg.name, + symbol: cw721_instantiate_msg.symbol, + minter: dao.to_string(), + })?; + + // Instantiate new contract, further setup is handled in the + // SubMsg reply. + let msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + admin: Some(dao.to_string()), + code_id, + msg, + funds: vec![], + label: "cw_tokenfactory_issuer".to_string(), + }, + INSTANTIATE_NFT_REPLY_ID, + ); + + Ok(Response::new().add_submessage(msg)) +} + +/// Requires one coin sent to test funds pass through for factory contracts +pub fn execute_nft_factory_with_funds( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw721_instantiate_msg: Cw721InstantiateMsg, + code_id: u64, + initial_nfts: Vec, +) -> Result { + // Validate one coin was sent + one_coin(&info)?; + + execute_nft_factory( + deps, + env, + info, + cw721_instantiate_msg, + code_id, + initial_nfts, + ) +} + +/// No callback for testing +pub fn execute_nft_factory_no_callback( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok(Response::new()) +} + +/// Wrong callback for testing +pub fn execute_nft_factory_wrong_callback( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok( + Response::new().set_data(to_json_binary(&TokenFactoryCallback { + denom: "wrong".to_string(), + token_contract: None, + module_instantiate_callback: None, + })?), + ) +} + +/// An example factory that instantiates a cw_tokenfactory_issuer contract +/// A more realistic example would be something like a DeFi Pool or Augmented +/// bonding curve. +pub fn execute_token_factory_factory( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token: NewTokenInfo, +) -> Result { + // Save voting module address + VOTING_MODULE.save(deps.storage, &info.sender)?; + + // Query for DAO + let dao: Addr = deps + .querier + .query_wasm_smart(info.sender, &VotingModuleQueryMsg::Dao {})?; + + // Save DAO and TOKEN_INFO for use in replies + DAO.save(deps.storage, &dao)?; + TOKEN_INFO.save(deps.storage, &token)?; + + // Instantiate new contract, further setup is handled in the + // SubMsg reply. + let msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + admin: Some(dao.to_string()), + code_id: token.token_issuer_code_id, + msg: to_json_binary(&IssuerInstantiateMsg::NewToken { + subdenom: token.subdenom, + })?, + funds: vec![], + label: "cw_tokenfactory_issuer".to_string(), + }, + INSTANTIATE_ISSUER_REPLY_ID, + ); + + Ok(Response::new().add_submessage(msg)) +} + +/// Requires one coin sent to test funds pass through for factory contracts +pub fn execute_token_factory_factory_with_funds( + deps: DepsMut, + env: Env, + info: MessageInfo, + token: NewTokenInfo, +) -> Result { + // Validate one coin was sent + one_coin(&info)?; + + execute_token_factory_factory(deps, env, info, token) +} + +/// No callback for testing +pub fn execute_token_factory_factory_no_callback( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok(Response::new()) +} + +/// Wrong callback for testing +pub fn execute_token_factory_factory_wrong_callback( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok( + Response::new().set_data(to_json_binary(&NftFactoryCallback { + nft_contract: "nope".to_string(), + module_instantiate_callback: None, + })?), + ) +} + +/// Example method called in the ModuleInstantiateCallback providing +/// an example for checking the DAO has been setup correctly. +pub fn execute_validate_nft_dao( + deps: DepsMut, + info: MessageInfo, +) -> Result { + // Load the collection and voting module address + let collection_addr = NFT_CONTRACT.load(deps.storage)?; + let voting_module = VOTING_MODULE.load(deps.storage)?; + + // Query the collection owner and check that it's the DAO. + let owner: Ownership = deps.querier.query_wasm_smart( + collection_addr.clone(), + &cw721_base::msg::QueryMsg::::Ownership {}, + )?; + match owner.owner { + Some(owner) => { + if owner != info.sender { + return Err(ContractError::Unauthorized {}); + } + } + None => return Err(ContractError::Unauthorized {}), + } + + // Query the total supply of the NFT contract + let nft_supply: NumTokensResponse = deps + .querier + .query_wasm_smart(collection_addr.clone(), &Cw721QueryMsg::NumTokens {})?; + + // Check greater than zero + if nft_supply.count == 0 { + return Err(ContractError::NoInitialNfts {}); + } + + // Query active threshold + let active_threshold: ActiveThresholdResponse = deps + .querier + .query_wasm_smart(voting_module, &ActiveThresholdQuery::ActiveThreshold {})?; + + // If Active Threshold absolute count is configured, + // check the count is not greater than supply. + // Percentage is validated in the voting module contract. + if let Some(ActiveThreshold::AbsoluteCount { count }) = active_threshold.active_threshold { + assert_valid_absolute_count_threshold(count, Uint128::new(nft_supply.count.into()))?; + } + + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Info {} => query_info(deps), + } +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = cw2::get_contract_version(deps.storage)?; + to_json_binary(&dao_interface::voting::InfoResponse { info }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + match msg.id { + INSTANTIATE_ISSUER_REPLY_ID => { + // Load DAO address and TOKEN_INFO + let dao = DAO.load(deps.storage)?; + let token = TOKEN_INFO.load(deps.storage)?; + let voting_module = VOTING_MODULE.load(deps.storage)?; + + // Parse issuer address from instantiate reply + let issuer_addr = parse_reply_instantiate_data(msg)?.contract_address; + + // Format the denom + let denom = format!("factory/{}/{}", &issuer_addr, token.subdenom); + + 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(); + + // Here we validate the active threshold to show how validation should be done + // in a factory contract. + let active_threshold: ActiveThresholdResponse = deps + .querier + .query_wasm_smart(voting_module, &ActiveThresholdQuery::ActiveThreshold {})?; + + if let Some(threshold) = active_threshold.active_threshold { + match threshold { + ActiveThreshold::Percentage { percent } => { + assert_valid_percentage_threshold(percent)?; + } + ActiveThreshold::AbsoluteCount { count } => { + assert_valid_absolute_count_threshold(count, initial_supply)?; + } + } + } + + // 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_json_binary(&IssuerExecuteMsg::SetMinterAllowance { + address: env.contract.address.to_string(), + allowance: total_supply, + })?, + 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_json_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_json_binary(&IssuerExecuteMsg::Mint { + to_address: dao.to_string(), + amount: initial_dao_balance, + })?, + funds: vec![], + }); + } + } + + // Begin update issuer contract owner to be the DAO, this is a + // two-step ownership transfer. + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_json_binary(&IssuerExecuteMsg::UpdateOwnership( + cw_ownable::Action::TransferOwnership { + new_owner: dao.to_string(), + expiry: None, + }, + ))?, + funds: vec![], + }); + + // DAO must accept ownership transfer. Here we include a + // ModuleInstantiateCallback message that will be called by the + // dao-dao-core contract when voting module instantiation is + // complete. + let callback = ModuleInstantiateCallback { + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_json_binary(&IssuerExecuteMsg::UpdateOwnership( + cw_ownable::Action::AcceptOwnership {}, + ))?, + funds: vec![], + })], + }; + + // Responses for `dao-voting-token-staked` MUST include a + // TokenFactoryCallback. + Ok(Response::new().add_messages(msgs).set_data(to_json_binary( + &TokenFactoryCallback { + denom, + token_contract: Some(issuer_addr.to_string()), + module_instantiate_callback: Some(callback), + }, + )?)) + } + INSTANTIATE_NFT_REPLY_ID => { + // Parse nft address from instantiate reply + let nft_address = parse_reply_instantiate_data(msg)?.contract_address; + + // Save NFT contract for use in validation reply + NFT_CONTRACT.save(deps.storage, &deps.api.addr_validate(&nft_address)?)?; + + let initial_nfts = INITIAL_NFTS.load(deps.storage)?; + + // Add mint messages that will be called by the DAO in the + // ModuleInstantiateCallback + let mut msgs: Vec = initial_nfts + .iter() + .flat_map(|nft| -> Result { + Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: nft_address.clone(), + funds: vec![], + msg: nft.clone(), + })) + }) + .collect::>(); + + // Clear space + INITIAL_NFTS.remove(deps.storage); + + // After DAO mints NFT, it calls back into the factory contract + // To validate the setup. NOTE: other patterns could be used for this + // but factory contracts SHOULD validate setups. + msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::ValidateNftDao {})?, + funds: vec![], + })); + + // Responses for `dao-voting-cw721-staked` MUST include a + // NftFactoryCallback. + Ok( + Response::new().set_data(to_json_binary(&NftFactoryCallback { + nft_contract: nft_address.to_string(), + module_instantiate_callback: Some(ModuleInstantiateCallback { msgs }), + })?), + ) + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/test/dao-test-custom-factory/src/error.rs b/contracts/test/dao-test-custom-factory/src/error.rs new file mode 100644 index 000000000..f7d753d54 --- /dev/null +++ b/contracts/test/dao-test-custom-factory/src/error.rs @@ -0,0 +1,31 @@ +use cosmwasm_std::StdError; +use cw_utils::{ParseReplyError, PaymentError}; +use dao_voting::threshold::ActiveThresholdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error(transparent)] + ActiveThresholdError(#[from] ActiveThresholdError), + + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + + #[error(transparent)] + PaymentError(#[from] PaymentError), + + #[error("New NFT contract must be instantiated with at least one NFT")] + NoInitialNfts {}, + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Factory message must serialize to WasmMsg::Execute")] + UnsupportedFactoryMsg {}, +} diff --git a/contracts/test/dao-test-custom-factory/src/lib.rs b/contracts/test/dao-test-custom-factory/src/lib.rs new file mode 100644 index 000000000..3915b791e --- /dev/null +++ b/contracts/test/dao-test-custom-factory/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +mod error; +pub mod msg; + +pub use crate::error::ContractError; diff --git a/contracts/test/dao-test-custom-factory/src/msg.rs b/contracts/test/dao-test-custom-factory/src/msg.rs new file mode 100644 index 000000000..42128cd2e --- /dev/null +++ b/contracts/test/dao-test-custom-factory/src/msg.rs @@ -0,0 +1,44 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Binary; +use cw721_base::InstantiateMsg as Cw721InstantiateMsg; +use dao_interface::token::NewTokenInfo; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + /// Example NFT factory implementation + NftFactory { + code_id: u64, + cw721_instantiate_msg: Cw721InstantiateMsg, + initial_nfts: Vec, + }, + /// Example NFT factory implentation that execpts funds + NftFactoryWithFunds { + code_id: u64, + cw721_instantiate_msg: Cw721InstantiateMsg, + initial_nfts: Vec, + }, + /// Used for testing no callback + NftFactoryNoCallback {}, + /// Used for testing wrong callback + NftFactoryWrongCallback {}, + /// Example Factory Implementation + TokenFactoryFactory(NewTokenInfo), + /// Example Factory Implementation that accepts funds + TokenFactoryFactoryWithFunds(NewTokenInfo), + /// Used for testing no callback + TokenFactoryFactoryNoCallback {}, + /// Used for testing wrong callback + TokenFactoryFactoryWrongCallback {}, + /// Validate NFT DAO + ValidateNftDao {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(dao_interface::voting::InfoResponse)] + Info {}, +} diff --git a/contracts/test/dao-voting-cw20-balance/.cargo/config b/contracts/test/dao-voting-cw20-balance/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/test/dao-voting-cw20-balance/.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-cw4/.gitignore b/contracts/test/dao-voting-cw20-balance/.gitignore similarity index 100% rename from contracts/voting/dao-voting-cw4/.gitignore rename to contracts/test/dao-voting-cw20-balance/.gitignore diff --git a/test-contracts/dao-voting-cw20-balance/Cargo.toml b/contracts/test/dao-voting-cw20-balance/Cargo.toml similarity index 100% rename from test-contracts/dao-voting-cw20-balance/Cargo.toml rename to contracts/test/dao-voting-cw20-balance/Cargo.toml diff --git a/test-contracts/dao-voting-cw20-balance/README.md b/contracts/test/dao-voting-cw20-balance/README.md similarity index 100% rename from test-contracts/dao-voting-cw20-balance/README.md rename to contracts/test/dao-voting-cw20-balance/README.md diff --git a/test-contracts/dao-voting-cw20-balance/examples/schema.rs b/contracts/test/dao-voting-cw20-balance/examples/schema.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/examples/schema.rs rename to contracts/test/dao-voting-cw20-balance/examples/schema.rs diff --git a/test-contracts/dao-voting-cw20-balance/schema/execute_msg.json b/contracts/test/dao-voting-cw20-balance/schema/execute_msg.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/execute_msg.json rename to contracts/test/dao-voting-cw20-balance/schema/execute_msg.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/info_response.json b/contracts/test/dao-voting-cw20-balance/schema/info_response.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/info_response.json rename to contracts/test/dao-voting-cw20-balance/schema/info_response.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/instantiate_msg.json b/contracts/test/dao-voting-cw20-balance/schema/instantiate_msg.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/instantiate_msg.json rename to contracts/test/dao-voting-cw20-balance/schema/instantiate_msg.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/query_msg.json b/contracts/test/dao-voting-cw20-balance/schema/query_msg.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/query_msg.json rename to contracts/test/dao-voting-cw20-balance/schema/query_msg.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/total_power_at_height_response.json b/contracts/test/dao-voting-cw20-balance/schema/total_power_at_height_response.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/total_power_at_height_response.json rename to contracts/test/dao-voting-cw20-balance/schema/total_power_at_height_response.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/voting_power_at_height_response.json b/contracts/test/dao-voting-cw20-balance/schema/voting_power_at_height_response.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/voting_power_at_height_response.json rename to contracts/test/dao-voting-cw20-balance/schema/voting_power_at_height_response.json diff --git a/test-contracts/dao-voting-cw20-balance/src/contract.rs b/contracts/test/dao-voting-cw20-balance/src/contract.rs similarity index 92% rename from test-contracts/dao-voting-cw20-balance/src/contract.rs rename to contracts/test/dao-voting-cw20-balance/src/contract.rs index 9c533568c..6d75355fd 100644 --- a/test-contracts/dao-voting-cw20-balance/src/contract.rs +++ b/contracts/test/dao-voting-cw20-balance/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, + to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, Uint128, WasmMsg, }; use cw2::set_contract_version; @@ -55,7 +55,7 @@ pub fn instantiate( let msg = WasmMsg::Instantiate { admin: Some(info.sender.to_string()), code_id, - msg: to_binary(&cw20_base::msg::InstantiateMsg { + msg: to_json_binary(&cw20_base::msg::InstantiateMsg { name, symbol, decimals, @@ -104,12 +104,12 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { pub fn query_dao(deps: Deps) -> StdResult { let dao = DAO.load(deps.storage)?; - to_binary(&dao) + to_json_binary(&dao) } pub fn query_token_contract(deps: Deps) -> StdResult { let token = TOKEN.load(deps.storage)?; - to_binary(&token) + to_json_binary(&token) } pub fn query_voting_power_at_height(deps: Deps, env: Env, address: String) -> StdResult { @@ -121,7 +121,7 @@ pub fn query_voting_power_at_height(deps: Deps, env: Env, address: String) -> St address: address.to_string(), }, )?; - to_binary(&dao_interface::voting::VotingPowerAtHeightResponse { + to_json_binary(&dao_interface::voting::VotingPowerAtHeightResponse { power: balance.balance, height: env.block.height, }) @@ -132,7 +132,7 @@ pub fn query_total_power_at_height(deps: Deps, env: Env) -> StdResult { let info: cw20::TokenInfoResponse = deps .querier .query_wasm_smart(token, &cw20::Cw20QueryMsg::TokenInfo {})?; - to_binary(&dao_interface::voting::TotalPowerAtHeightResponse { + to_json_binary(&dao_interface::voting::TotalPowerAtHeightResponse { power: info.total_supply, height: env.block.height, }) @@ -140,7 +140,7 @@ pub fn query_total_power_at_height(deps: Deps, env: Env) -> StdResult { pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; - to_binary(&dao_interface::voting::InfoResponse { info }) + to_json_binary(&dao_interface::voting::InfoResponse { info }) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/test-contracts/dao-voting-cw20-balance/src/error.rs b/contracts/test/dao-voting-cw20-balance/src/error.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/error.rs rename to contracts/test/dao-voting-cw20-balance/src/error.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/lib.rs b/contracts/test/dao-voting-cw20-balance/src/lib.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/lib.rs rename to contracts/test/dao-voting-cw20-balance/src/lib.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/msg.rs b/contracts/test/dao-voting-cw20-balance/src/msg.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/msg.rs rename to contracts/test/dao-voting-cw20-balance/src/msg.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/state.rs b/contracts/test/dao-voting-cw20-balance/src/state.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/state.rs rename to contracts/test/dao-voting-cw20-balance/src/state.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/tests.rs b/contracts/test/dao-voting-cw20-balance/src/tests.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/tests.rs rename to contracts/test/dao-voting-cw20-balance/src/tests.rs diff --git a/contracts/voting/dao-voting-cw20-staked/Cargo.toml b/contracts/voting/dao-voting-cw20-staked/Cargo.toml index 43ccc1449..7b2b0d6a3 100644 --- a/contracts/voting/dao-voting-cw20-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw20-staked/Cargo.toml @@ -21,14 +21,15 @@ cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-storage = { workspace = true } cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } cw2 = { workspace = true } cw20 = { workspace = true } -cw-utils = { workspace = true } +cw20-base = { workspace = true, features = ["library"] } +cw20-stake = { workspace = true, features = ["library"] } thiserror = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } -cw20-stake = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } +dao-voting = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/voting/dao-voting-cw20-staked/README.md b/contracts/voting/dao-voting-cw20-staked/README.md index 78fb52602..2856227f7 100644 --- a/contracts/voting/dao-voting-cw20-staked/README.md +++ b/contracts/voting/dao-voting-cw20-staked/README.md @@ -1,5 +1,8 @@ # CW20 Staked Balance Voting +[![dao-voting-cw20-staked on crates.io](https://img.shields.io/crates/v/dao-voting-cw20-staked.svg?logo=rust)](https://crates.io/crates/dao-voting-cw20-staked) +[![docs.rs](https://img.shields.io/docsrs/dao-voting-cw20-staked?logo=docsdotrs)](https://docs.rs/dao-voting-cw20-staked/latest/dao_voting_cw20_staked/) + A voting power module which determines voting power based on the staked token balance of specific addresses at given heights. diff --git a/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json b/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json index 64a919c22..1a80745e0 100644 --- a/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json +++ b/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-cw20-staked", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -11,6 +11,7 @@ ], "properties": { "active_threshold": { + "description": "The number or percentage of tokens that must be staked for the DAO to be active", "anyOf": [ { "$ref": "#/definitions/ActiveThreshold" diff --git a/contracts/voting/dao-voting-cw20-staked/src/contract.rs b/contracts/voting/dao-voting-cw20-staked/src/contract.rs index ef3cb602b..47913a57c 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/contract.rs @@ -1,20 +1,18 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Addr, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, - SubMsg, Uint128, Uint256, WasmMsg, + to_json_binary, Addr, Binary, Decimal, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdResult, SubMsg, Uint128, Uint256, WasmMsg, }; -use cw2::set_contract_version; +use cw2::{get_contract_version, set_contract_version, ContractVersion}; use cw20::{Cw20Coin, TokenInfoResponse}; use cw_utils::parse_reply_instantiate_data; use dao_interface::voting::IsActiveResponse; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; use std::convert::TryInto; use crate::error::ContractError; -use crate::msg::{ - ActiveThreshold, 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, @@ -91,7 +89,7 @@ pub fn instantiate( funds: vec![], admin: Some(info.sender.to_string()), label: env.contract.address.to_string(), - msg: to_binary(&cw20_stake::msg::InstantiateMsg { + msg: to_json_binary(&cw20_stake::msg::InstantiateMsg { owner: Some(info.sender.to_string()), unstaking_duration, token_address: address.to_string(), @@ -143,7 +141,7 @@ pub fn instantiate( let msg = WasmMsg::Instantiate { admin: Some(info.sender.to_string()), code_id, - msg: to_binary(&cw20_base::msg::InstantiateMsg { + msg: to_json_binary(&cw20_base::msg::InstantiateMsg { name, symbol, decimals, @@ -247,12 +245,12 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { pub fn query_token_contract(deps: Deps) -> StdResult { let token = TOKEN.load(deps.storage)?; - to_binary(&token) + to_json_binary(&token) } pub fn query_staking_contract(deps: Deps) -> StdResult { let staking_contract = STAKING_CONTRACT.load(deps.storage)?; - to_binary(&staking_contract) + to_json_binary(&staking_contract) } pub fn query_voting_power_at_height( @@ -270,7 +268,7 @@ pub fn query_voting_power_at_height( height, }, )?; - to_binary(&dao_interface::voting::VotingPowerAtHeightResponse { + to_json_binary(&dao_interface::voting::VotingPowerAtHeightResponse { power: res.balance, height: res.height, }) @@ -286,7 +284,7 @@ pub fn query_total_power_at_height( staking_contract, &cw20_stake::msg::QueryMsg::TotalStakedAtHeight { height }, )?; - to_binary(&dao_interface::voting::TotalPowerAtHeightResponse { + to_json_binary(&dao_interface::voting::TotalPowerAtHeightResponse { power: res.total, height: res.height, }) @@ -294,12 +292,12 @@ pub fn query_total_power_at_height( pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; - to_binary(&dao_interface::voting::InfoResponse { info }) + to_json_binary(&dao_interface::voting::InfoResponse { info }) } pub fn query_dao(deps: Deps) -> StdResult { let dao = DAO.load(deps.storage)?; - to_binary(&dao) + to_json_binary(&dao) } pub fn query_is_active(deps: Deps) -> StdResult { @@ -313,7 +311,7 @@ pub fn query_is_active(deps: Deps) -> StdResult { &cw20_stake::msg::QueryMsg::TotalStakedAtHeight { height: None }, )?; match threshold { - ActiveThreshold::AbsoluteCount { count } => to_binary(&IsActiveResponse { + ActiveThreshold::AbsoluteCount { count } => to_json_binary(&IsActiveResponse { active: actual_power.total >= count, }), ActiveThreshold::Percentage { percent } => { @@ -356,27 +354,33 @@ pub fn query_is_active(deps: Deps) -> StdResult { let rounded = (applied + Uint256::from(PRECISION_FACTOR) - Uint256::from(1u128)) / Uint256::from(PRECISION_FACTOR); let count: Uint128 = rounded.try_into().unwrap(); - to_binary(&IsActiveResponse { + to_json_binary(&IsActiveResponse { active: actual_power.total >= count, }) } } } else { - to_binary(&IsActiveResponse { active: true }) + to_json_binary(&IsActiveResponse { active: true }) } } pub fn query_active_threshold(deps: Deps) -> StdResult { - to_binary(&ActiveThresholdResponse { + to_json_binary(&ActiveThresholdResponse { active_threshold: ACTIVE_THRESHOLD.may_load(deps.storage)?, }) } #[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::default()) + let storage_version: ContractVersion = get_contract_version(deps.storage)?; + + // Only migrate if newer + if storage_version.version.as_str() < CONTRACT_VERSION { + // 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)] @@ -409,7 +413,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result, } @@ -96,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/state.rs b/contracts/voting/dao-voting-cw20-staked/src/state.rs index 05ea27233..d4e61d397 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/state.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/state.rs @@ -1,7 +1,7 @@ -use crate::msg::ActiveThreshold; use cosmwasm_std::Addr; use cw_storage_plus::Item; use cw_utils::Duration; +use dao_voting::threshold::ActiveThreshold; pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); pub const TOKEN: Item = Item::new("token"); diff --git a/contracts/voting/dao-voting-cw20-staked/src/tests.rs b/contracts/voting/dao-voting-cw20-staked/src/tests.rs index 06c89f2dc..0022f98ed 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/tests.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/tests.rs @@ -1,18 +1,16 @@ use cosmwasm_std::{ testing::{mock_dependencies, mock_env}, - to_binary, Addr, CosmosMsg, Decimal, Empty, Uint128, WasmMsg, + to_json_binary, Addr, CosmosMsg, Decimal, Empty, Uint128, WasmMsg, }; 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, ActiveThresholdResponse}; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, - msg::{ - ActiveThreshold, ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, - StakingInfo, - }, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StakingInfo}, }; const DAO_ADDR: &str = "dao"; @@ -63,7 +61,7 @@ fn stake_tokens(app: &mut App, staking_addr: Addr, cw20_addr: Addr, sender: &str let msg = cw20::Cw20ExecuteMsg::Send { contract: staking_addr.to_string(), amount: Uint128::new(amount), - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; app.execute_contract(Addr::unchecked(sender), cw20_addr, &msg, &[]) .unwrap(); @@ -1110,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 @@ -1377,7 +1375,7 @@ fn test_migrate() { CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: voting_addr.to_string(), new_code_id: voting_id, - msg: to_binary(&MigrateMsg {}).unwrap(), + msg: to_json_binary(&MigrateMsg {}).unwrap(), }), ) .unwrap(); @@ -1393,7 +1391,7 @@ fn test_migrate() { #[test] pub fn test_migrate_update_version() { let mut deps = mock_dependencies(); - cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); + cw2::set_contract_version(&mut deps.storage, "my-contract", "1.0.0").unwrap(); migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); let version = cw2::get_contract_version(&deps.storage).unwrap(); assert_eq!(version.version, CONTRACT_VERSION); diff --git a/contracts/voting/dao-voting-cw4/README.md b/contracts/voting/dao-voting-cw4/README.md index 14793999c..014e3f229 100644 --- a/contracts/voting/dao-voting-cw4/README.md +++ b/contracts/voting/dao-voting-cw4/README.md @@ -1,5 +1,8 @@ # CW4 Group Voting +[![dao-voting-cw4 on crates.io](https://img.shields.io/crates/v/dao-voting-cw4.svg?logo=rust)](https://crates.io/crates/dao-voting-cw4) +[![docs.rs](https://img.shields.io/docsrs/dao-voting-cw4?logo=docsdotrs)](https://docs.rs/dao-voting-cw4/latest/dao_voting_cw4/) + A simple voting power module which determines voting power based on the weight of a user in a cw4-group contract. This allocates voting power in the same way that one would expect a multisig to. diff --git a/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json b/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json index 829fd9e25..23dba52d5 100644 --- a/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json +++ b/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json @@ -1,30 +1,76 @@ { "contract_name": "dao-voting-cw4", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", "type": "object", "required": [ - "cw4_group_code_id", - "initial_members" + "group_contract" ], "properties": { - "cw4_group_code_id": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "initial_members": { - "type": "array", - "items": { - "$ref": "#/definitions/Member" - } + "group_contract": { + "$ref": "#/definitions/GroupContract" } }, "additionalProperties": false, "definitions": { + "GroupContract": { + "oneOf": [ + { + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "new" + ], + "properties": { + "new": { + "type": "object", + "required": [ + "cw4_group_code_id", + "initial_members" + ], + "properties": { + "cw4_group_code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "initial_members": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Member": { "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", "type": "object", @@ -49,63 +95,8 @@ "execute": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", - "oneOf": [ - { - "type": "object", - "required": [ - "member_changed_hook" - ], - "properties": { - "member_changed_hook": { - "type": "object", - "required": [ - "diffs" - ], - "properties": { - "diffs": { - "type": "array", - "items": { - "$ref": "#/definitions/MemberDiff" - } - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - } - ], - "definitions": { - "MemberDiff": { - "description": "MemberDiff shows the old and new states for a given cw4 member They cannot both be None. old = None, new = Some -> Insert old = Some, new = Some -> Update old = Some, new = None -> Delete", - "type": "object", - "required": [ - "key" - ], - "properties": { - "key": { - "type": "string" - }, - "new": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - }, - "old": { - "type": [ - "integer", - "null" - ], - "format": "uint64", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - } + "type": "string", + "enum": [] }, "query": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw4/src/contract.rs b/contracts/voting/dao-voting-cw4/src/contract.rs index 9b2bca990..48307676f 100644 --- a/contracts/voting/dao-voting-cw4/src/contract.rs +++ b/contracts/voting/dao-voting-cw4/src/contract.rs @@ -1,15 +1,16 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult, - SubMsg, Uint128, WasmMsg, + to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, + Uint128, WasmMsg, }; -use cw2::set_contract_version; +use cw2::{get_contract_version, set_contract_version, ContractVersion}; +use cw4::{MemberListResponse, MemberResponse, TotalWeightResponse}; use cw_utils::parse_reply_instantiate_data; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::state::{DAO, GROUP_CONTRACT, TOTAL_WEIGHT, USER_WEIGHTS}; +use crate::msg::{ExecuteMsg, GroupContract, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{DAO, GROUP_CONTRACT}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cw4"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -24,124 +25,95 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - if msg.initial_members.is_empty() { - return Err(ContractError::NoMembers {}); - } - let original_len = msg.initial_members.len(); - let mut initial_members = msg.initial_members; - initial_members.sort_by(|a, b| a.addr.cmp(&b.addr)); - initial_members.dedup(); - let new_len = initial_members.len(); - - if original_len != new_len { - return Err(ContractError::DuplicateMembers {}); - } - let mut total_weight = Uint128::zero(); - for member in initial_members.iter() { - let member_addr = deps.api.addr_validate(&member.addr)?; - if member.weight > 0 { - // This works because query_voting_power_at_height will return 0 on address missing - // from storage, so no need to store anything. - let weight = Uint128::from(member.weight); - USER_WEIGHTS.save(deps.storage, &member_addr, &weight, env.block.height)?; - total_weight += weight; - } - } + DAO.save(deps.storage, &info.sender)?; - if total_weight.is_zero() { - return Err(ContractError::ZeroTotalWeight {}); - } - TOTAL_WEIGHT.save(deps.storage, &total_weight, env.block.height)?; - - // We need to set ourself as the CW4 admin it is then transferred to the DAO in the reply - let msg = WasmMsg::Instantiate { - admin: Some(info.sender.to_string()), - code_id: msg.cw4_group_code_id, - msg: to_binary(&cw4_group::msg::InstantiateMsg { - admin: Some(env.contract.address.to_string()), - members: initial_members, - })?, - funds: vec![], - label: env.contract.address.to_string(), - }; - - let msg = SubMsg::reply_on_success(msg, INSTANTIATE_GROUP_REPLY_ID); + match msg.group_contract { + GroupContract::New { + cw4_group_code_id, + initial_members, + } => { + if initial_members.is_empty() { + return Err(ContractError::NoMembers {}); + } + let original_len = initial_members.len(); + let mut initial_members = initial_members; + initial_members.sort_by(|a, b| a.addr.cmp(&b.addr)); + initial_members.dedup(); + let new_len = initial_members.len(); + + if original_len != new_len { + return Err(ContractError::DuplicateMembers {}); + } - DAO.save(deps.storage, &info.sender)?; + let mut total_weight = Uint128::zero(); + for member in initial_members.iter() { + deps.api.addr_validate(&member.addr)?; + if member.weight > 0 { + // This works because query_voting_power_at_height will return 0 on address missing + // from storage, so no need to store anything. + let weight = Uint128::from(member.weight); + total_weight += weight; + } + } - Ok(Response::new() - .add_attribute("action", "instantiate") - .add_submessage(msg)) -} + if total_weight.is_zero() { + return Err(ContractError::ZeroTotalWeight {}); + } -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - match msg { - ExecuteMsg::MemberChangedHook { diffs } => { - execute_member_changed_hook(deps, env, info, diffs) + // Instantiate group contract, set DAO as admin. + // Voting module contracts are instantiated by the main dao-dao-core + // contract, so the Admin is set to info.sender. + let msg = WasmMsg::Instantiate { + admin: Some(info.sender.to_string()), + code_id: cw4_group_code_id, + msg: to_json_binary(&cw4_group::msg::InstantiateMsg { + admin: Some(info.sender.to_string()), + members: initial_members, + })?, + funds: vec![], + label: env.contract.address.to_string(), + }; + + let msg = SubMsg::reply_on_success(msg, INSTANTIATE_GROUP_REPLY_ID); + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_submessage(msg)) } - } -} + GroupContract::Existing { address } => { + let group_contract = deps.api.addr_validate(&address)?; + + // Validate valid group contract that has at least one member. + let res: MemberListResponse = deps.querier.query_wasm_smart( + group_contract.clone(), + &cw4_group::msg::QueryMsg::ListMembers { + start_after: None, + limit: Some(1), + }, + )?; -pub fn execute_member_changed_hook( - deps: DepsMut, - env: Env, - info: MessageInfo, - diffs: Vec, -) -> Result { - let group_contract = GROUP_CONTRACT.load(deps.storage)?; - if info.sender != group_contract { - return Err(ContractError::Unauthorized {}); - } + if res.members.is_empty() { + return Err(ContractError::NoMembers {}); + } - let total_weight = TOTAL_WEIGHT.load(deps.storage)?; - // As difference can be negative we need to keep track of both - // In seperate counters to apply at once and prevent underflow - let mut positive_difference: Uint128 = Uint128::zero(); - let mut negative_difference: Uint128 = Uint128::zero(); - for diff in diffs { - let user_address = deps.api.addr_validate(&diff.key)?; - let weight = diff.new.unwrap_or_default(); - let old = diff.old.unwrap_or_default(); - // Do we need to add to positive difference or negative difference - if weight > old { - positive_difference += Uint128::from(weight - old); - } else { - negative_difference += Uint128::from(old - weight); - } + GROUP_CONTRACT.save(deps.storage, &group_contract)?; - if weight != 0 { - USER_WEIGHTS.save( - deps.storage, - &user_address, - &Uint128::from(weight), - env.block.height, - )?; - } else if weight == 0 && weight != old { - // This works because query_voting_power_at_height will return 0 on address missing - // from storage, so no need to store anything. - // - // Note that we also check for weight != old: If for some reason this hook is triggered - // with weight 0 for old and new values, we don't need to do anything. - USER_WEIGHTS.remove(deps.storage, &user_address, env.block.height)?; + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute("group_contract", group_contract.to_string())) } } - let new_total_weight = total_weight - .checked_add(positive_difference) - .map_err(StdError::overflow)? - .checked_sub(negative_difference) - .map_err(StdError::overflow)?; - TOTAL_WEIGHT.save(deps.storage, &new_total_weight, env.block.height)?; - - Ok(Response::new() - .add_attribute("action", "member_changed_hook") - .add_attribute("total_weight", new_total_weight.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsg, +) -> Result { + Err(ContractError::NoExecute {}) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -152,8 +124,8 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height), QueryMsg::Info {} => query_info(deps), - QueryMsg::GroupContract {} => to_binary(&GROUP_CONTRACT.load(deps.storage)?), - QueryMsg::Dao {} => to_binary(&DAO.load(deps.storage)?), + QueryMsg::GroupContract {} => to_json_binary(&GROUP_CONTRACT.load(deps.storage)?), + QueryMsg::Dao {} => to_json_binary(&DAO.load(deps.storage)?), } } @@ -163,37 +135,54 @@ pub fn query_voting_power_at_height( address: String, height: Option, ) -> StdResult { - let address = deps.api.addr_validate(&address)?; - let height = height.unwrap_or(env.block.height); - let power = USER_WEIGHTS - .may_load_at_height(deps.storage, &address, height)? - .unwrap_or_default(); - - to_binary(&dao_interface::voting::VotingPowerAtHeightResponse { power, height }) + let addr = deps.api.addr_validate(&address)?.to_string(); + let group_contract = GROUP_CONTRACT.load(deps.storage)?; + let res: MemberResponse = deps.querier.query_wasm_smart( + group_contract, + &cw4_group::msg::QueryMsg::Member { + addr, + at_height: height, + }, + )?; + + to_json_binary(&dao_interface::voting::VotingPowerAtHeightResponse { + power: res.weight.unwrap_or(0).into(), + height: height.unwrap_or(env.block.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 = TOTAL_WEIGHT - .may_load_at_height(deps.storage, height)? - .unwrap_or_default(); - to_binary(&dao_interface::voting::TotalPowerAtHeightResponse { power, height }) + let group_contract = GROUP_CONTRACT.load(deps.storage)?; + let res: TotalWeightResponse = deps.querier.query_wasm_smart( + group_contract, + &cw4_group::msg::QueryMsg::TotalWeight { at_height: height }, + )?; + to_json_binary(&dao_interface::voting::TotalPowerAtHeightResponse { + power: res.weight.into(), + height: height.unwrap_or(env.block.height), + }) } pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; - to_binary(&dao_interface::voting::InfoResponse { info }) + to_json_binary(&dao_interface::voting::InfoResponse { info }) } #[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::default()) + let storage_version: ContractVersion = get_contract_version(deps.storage)?; + + // Only migrate if newer + if storage_version.version.as_str() < CONTRACT_VERSION { + // 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 { +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { match msg.id { INSTANTIATE_GROUP_REPLY_ID => { let res = parse_reply_instantiate_data(msg); @@ -204,27 +193,8 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Err(ContractError::GroupContractInstantiateError {}), } diff --git a/contracts/voting/dao-voting-cw4/src/error.rs b/contracts/voting/dao-voting-cw4/src/error.rs index a8bd1a909..a0ce03e9c 100644 --- a/contracts/voting/dao-voting-cw4/src/error.rs +++ b/contracts/voting/dao-voting-cw4/src/error.rs @@ -1,7 +1,7 @@ use cosmwasm_std::StdError; use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), @@ -12,18 +12,21 @@ pub enum ContractError { #[error("Can not change the contract's token after it has been set")] DuplicateGroupContract {}, + #[error("Cannot instantiate a group contract with duplicate initial members")] + DuplicateMembers {}, + #[error("Error occured whilst instantiating group contract")] GroupContractInstantiateError {}, - #[error("Cannot instantiate a group contract with no initial members")] + #[error("Contract only supports queries")] + NoExecute {}, + + #[error("Cannot instantiate or use a group contract with no initial members")] NoMembers {}, - #[error("Cannot instantiate a group contract with duplicate initial members")] - DuplicateMembers {}, + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, #[error("Total weight of the CW4 contract cannot be zero")] ZeroTotalWeight {}, - - #[error("Got a submessage reply with unknown id: {id}")] - UnknownReplyId { id: u64 }, } diff --git a/contracts/voting/dao-voting-cw4/src/msg.rs b/contracts/voting/dao-voting-cw4/src/msg.rs index 9ef904698..24bd0eebc 100644 --- a/contracts/voting/dao-voting-cw4/src/msg.rs +++ b/contracts/voting/dao-voting-cw4/src/msg.rs @@ -2,16 +2,24 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use dao_dao_macros::voting_module_query; #[cw_serde] -pub struct InstantiateMsg { - pub cw4_group_code_id: u64, - pub initial_members: Vec, +pub enum GroupContract { + Existing { + address: String, + }, + New { + cw4_group_code_id: u64, + initial_members: Vec, + }, } #[cw_serde] -pub enum ExecuteMsg { - MemberChangedHook { diffs: Vec }, +pub struct InstantiateMsg { + pub group_contract: GroupContract, } +#[cw_serde] +pub enum ExecuteMsg {} + #[voting_module_query] #[cw_serde] #[derive(QueryResponses)] diff --git a/contracts/voting/dao-voting-cw4/src/state.rs b/contracts/voting/dao-voting-cw4/src/state.rs index 11b449728..bdc0d8004 100644 --- a/contracts/voting/dao-voting-cw4/src/state.rs +++ b/contracts/voting/dao-voting-cw4/src/state.rs @@ -1,19 +1,5 @@ -use cosmwasm_std::{Addr, Uint128}; -use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; - -pub const USER_WEIGHTS: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( - "user_weights", - "user_weights__checkpoints", - "user_weights__changelog", - Strategy::EveryBlock, -); - -pub const TOTAL_WEIGHT: SnapshotItem = SnapshotItem::new( - "total_weight", - "total_weight__checkpoints", - "total_weight__changelog", - Strategy::EveryBlock, -); +use cosmwasm_std::Addr; +use cw_storage_plus::Item; pub const GROUP_CONTRACT: Item = Item::new("group_contract"); pub const DAO: Item = Item::new("dao_address"); diff --git a/contracts/voting/dao-voting-cw4/src/tests.rs b/contracts/voting/dao-voting-cw4/src/tests.rs index 94ca06f08..769c53c4c 100644 --- a/contracts/voting/dao-voting-cw4/src/tests.rs +++ b/contracts/voting/dao-voting-cw4/src/tests.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ testing::{mock_dependencies, mock_env}, - to_binary, Addr, CosmosMsg, Empty, Uint128, WasmMsg, + to_json_binary, Addr, CosmosMsg, Empty, Uint128, WasmMsg, }; use cw2::ContractVersion; use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor}; @@ -10,7 +10,7 @@ use dao_interface::voting::{ use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, - msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + msg::{GroupContract, InstantiateMsg, MigrateMsg, QueryMsg}, ContractError, }; @@ -78,8 +78,10 @@ fn setup_test_case(app: &mut App) -> Addr { app, voting_id, InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members: members, + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: members, + }, }, ) } @@ -94,8 +96,10 @@ fn test_instantiate() { let voting_id = app.store_code(voting_contract()); let cw4_id = app.store_code(cw4_contract()); let msg = InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members: vec![], + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: [].into(), + }, }; let _err = app .instantiate_contract( @@ -110,21 +114,98 @@ fn test_instantiate() { // Instantiate with members but no weight let msg = InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members: vec![ - cw4::Member { - addr: ADDR1.to_string(), - weight: 0, + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: ADDR1.to_string(), + weight: 0, + }, + cw4::Member { + addr: ADDR2.to_string(), + weight: 0, + }, + cw4::Member { + addr: ADDR3.to_string(), + weight: 0, + }, + ], + }, + }; + let _err = app + .instantiate_contract( + voting_id, + Addr::unchecked(DAO_ADDR), + &msg, + &[], + "voting module", + None, + ) + .unwrap_err(); +} + +#[test] +pub fn test_instantiate_existing_contract() { + let mut app = App::default(); + + let voting_id = app.store_code(voting_contract()); + let cw4_id = app.store_code(cw4_contract()); + + // Fail with no members. + let cw4_addr = app + .instantiate_contract( + cw4_id, + Addr::unchecked(DAO_ADDR), + &cw4_group::msg::InstantiateMsg { + admin: Some(DAO_ADDR.to_string()), + members: vec![], }, - cw4::Member { - addr: ADDR2.to_string(), - weight: 0, + &[], + "cw4 group", + None, + ) + .unwrap(); + + let err: ContractError = app + .instantiate_contract( + voting_id, + Addr::unchecked(DAO_ADDR), + &InstantiateMsg { + group_contract: GroupContract::Existing { + address: cw4_addr.to_string(), + }, }, - cw4::Member { - addr: ADDR3.to_string(), - weight: 0, + &[], + "voting module", + None, + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::NoMembers {}); + + let cw4_addr = app + .instantiate_contract( + cw4_id, + Addr::unchecked(DAO_ADDR), + &cw4_group::msg::InstantiateMsg { + admin: Some(DAO_ADDR.to_string()), + members: vec![cw4::Member { + addr: ADDR1.to_string(), + weight: 1, + }], }, - ], + &[], + "cw4 group", + None, + ) + .unwrap(); + + // Instantiate with existing contract + let msg = InstantiateMsg { + group_contract: GroupContract::Existing { + address: cw4_addr.to_string(), + }, }; let _err = app .instantiate_contract( @@ -135,7 +216,32 @@ fn test_instantiate() { "voting module", None, ) - .unwrap_err(); + .unwrap(); + + // Update ADDR1's weight to 2 + let msg = cw4_group::msg::ExecuteMsg::UpdateMembers { + remove: vec![], + add: vec![cw4::Member { + addr: ADDR1.to_string(), + weight: 2, + }], + }; + + app.execute_contract(Addr::unchecked(DAO_ADDR), cw4_addr.clone(), &msg, &[]) + .unwrap(); + + // Same should be true about the groups contract. + let cw4_power: cw4::MemberResponse = app + .wrap() + .query_wasm_smart( + cw4_addr, + &cw4::Cw4QueryMsg::Member { + addr: ADDR1.to_string(), + at_height: None, + }, + ) + .unwrap(); + assert_eq!(cw4_power.weight.unwrap(), 2); } #[test] @@ -170,38 +276,6 @@ fn test_contract_info() { assert_eq!(dao_contract, Addr::unchecked(DAO_ADDR)); } -#[test] -fn test_permissions() { - let mut app = App::default(); - let voting_addr = setup_test_case(&mut app); - - // DAO can not execute hook message. - let err: ContractError = app - .execute_contract( - Addr::unchecked(DAO_ADDR), - voting_addr.clone(), - &ExecuteMsg::MemberChangedHook { diffs: vec![] }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!(err, ContractError::Unauthorized {})); - - // Contract itself can not execute hook message. - let err: ContractError = app - .execute_contract( - voting_addr.clone(), - voting_addr, - &ExecuteMsg::MemberChangedHook { diffs: vec![] }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!(err, ContractError::Unauthorized {})); -} - #[test] fn test_power_at_height() { let mut app = App::default(); @@ -502,8 +576,10 @@ fn test_migrate() { let voting_id = app.store_code(voting_contract()); let cw4_id = app.store_code(cw4_contract()); let msg = InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members, + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members, + }, }; let voting_addr = app .instantiate_contract( @@ -532,7 +608,7 @@ fn test_migrate() { CosmosMsg::Wasm(WasmMsg::Migrate { contract_addr: voting_addr.to_string(), new_code_id: voting_id, - msg: to_binary(&MigrateMsg {}).unwrap(), + msg: to_json_binary(&MigrateMsg {}).unwrap(), }), ) .unwrap(); @@ -560,25 +636,27 @@ fn test_duplicate_member() { // Instantiate with members but have a duplicate // Total weight is actually 69 but ADDR3 appears twice. let msg = InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members: vec![ - cw4::Member { - addr: ADDR3.to_string(), // same address above - weight: 19, - }, - cw4::Member { - addr: ADDR1.to_string(), - weight: 25, - }, - cw4::Member { - addr: ADDR2.to_string(), - weight: 25, - }, - cw4::Member { - addr: ADDR3.to_string(), - weight: 19, - }, - ], + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: ADDR3.to_string(), // same address above + weight: 19, + }, + cw4::Member { + addr: ADDR1.to_string(), + weight: 25, + }, + cw4::Member { + addr: ADDR2.to_string(), + weight: 25, + }, + cw4::Member { + addr: ADDR3.to_string(), + weight: 19, + }, + ], + }, }; // Previous versions voting power was 100, due to no dedup. // Now we error @@ -631,22 +709,7 @@ fn test_zero_voting_power() { app.execute_contract(Addr::unchecked(DAO_ADDR), cw4_addr, &msg, &[]) .unwrap(); - // Should still be one as voting power should not update until - // the following block. - let addr1_voting_power: VotingPowerAtHeightResponse = app - .wrap() - .query_wasm_smart( - voting_addr.clone(), - &QueryMsg::VotingPowerAtHeight { - address: ADDR1.to_string(), - height: None, - }, - ) - .unwrap(); - assert_eq!(addr1_voting_power.power, Uint128::new(1u128)); - - // update block to see the changes - app.update_block(next_block); + // Check ADDR1's power is now 0 let addr1_voting_power: VotingPowerAtHeightResponse = app .wrap() .query_wasm_smart( @@ -672,7 +735,7 @@ fn test_zero_voting_power() { #[test] pub fn test_migrate_update_version() { let mut deps = mock_dependencies(); - cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); + cw2::set_contract_version(&mut deps.storage, "my-contract", "1.0.0").unwrap(); migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); let version = cw2::get_contract_version(&deps.storage).unwrap(); assert_eq!(version.version, CONTRACT_VERSION); diff --git a/contracts/voting/dao-voting-cw721-roles/.cargo/config b/contracts/voting/dao-voting-cw721-roles/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/.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-cw721-roles/Cargo.toml b/contracts/voting/dao-voting-cw721-roles/Cargo.toml new file mode 100644 index 000000000..dad568389 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "dao-voting-cw721-roles" +authors = ["Jake Hartnell"] +description = "A DAO DAO voting module based on non-transferrable cw721 tokens." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +dao-cw721-extensions = { workspace = true } +dao-dao-macros = { workspace = true } +dao-interface = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +cw721-controllers = { workspace = true } +cw-ownable = { workspace = true } +cw-paginate-storage = { workspace = true } +cw721 = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +cw4 = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-roles = { workspace = true } +anyhow = { workspace = true } +dao-testing = { workspace = true } diff --git a/contracts/voting/dao-voting-cw721-roles/README.md b/contracts/voting/dao-voting-cw721-roles/README.md new file mode 100644 index 000000000..f15f4e20a --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/README.md @@ -0,0 +1,3 @@ +# dao-voting-cw721-roles + +This contract works in conjunction with the [cw721-roles contract](../../external/cw721-roles), and allows for a DAO with non-transferrable roles that can have different weights for voting power. 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). diff --git a/contracts/voting/dao-voting-cw721-roles/examples/schema.rs b/contracts/voting/dao-voting-cw721-roles/examples/schema.rs new file mode 100644 index 000000000..0e391c586 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use dao_voting_cw721_roles::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json b/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json new file mode 100644 index 000000000..5ccec487c --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json @@ -0,0 +1,378 @@ +{ + "contract_name": "dao-voting-cw721-roles", + "contract_version": "2.3.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "nft_contract" + ], + "properties": { + "nft_contract": { + "description": "Info about the associated NFT contract", + "allOf": [ + { + "$ref": "#/definitions/NftContract" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "MetadataExt": { + "type": "object", + "required": [ + "weight" + ], + "properties": { + "role": { + "description": "Optional on-chain role for this member, can be used by other contracts to enforce permissions", + "type": [ + "string", + "null" + ] + }, + "weight": { + "description": "The voting weight of this role", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NftContract": { + "oneOf": [ + { + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of an already instantiated cw721-weighted-roles token contract.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "new" + ], + "properties": { + "new": { + "type": "object", + "required": [ + "code_id", + "initial_nfts", + "label", + "name", + "symbol" + ], + "properties": { + "code_id": { + "description": "Code ID for cw721 token contract.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "initial_nfts": { + "description": "Initial NFTs to mint when instantiating the new cw721 contract. If empty, an error is thrown.", + "type": "array", + "items": { + "$ref": "#/definitions/NftMintMsg" + } + }, + "label": { + "description": "Label to use for instantiated cw721 contract.", + "type": "string" + }, + "name": { + "description": "NFT collection name", + "type": "string" + }, + "symbol": { + "description": "NFT collection symbol", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "NftMintMsg": { + "type": "object", + "required": [ + "extension", + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "allOf": [ + { + "$ref": "#/definitions/MetadataExt" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "type": "string", + "enum": [] + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "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 + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "nft_address" + ], + "properties": { + "nft_address": { + "$ref": "#/definitions/Addr" + } + }, + "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" + } + } + }, + "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" + }, + "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 + } + } + }, + "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-cw721-roles/src/contract.rs b/contracts/voting/dao-voting-cw721-roles/src/contract.rs new file mode 100644 index 000000000..d44bf10ab --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/contract.rs @@ -0,0 +1,232 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response, StdResult, + SubMsg, WasmMsg, +}; +use cw2::set_contract_version; +use cw4::{MemberResponse, TotalWeightResponse}; +use cw721_base::{ + ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg, QueryMsg as Cw721QueryMsg, +}; +use cw_ownable::Action; +use cw_utils::parse_reply_instantiate_data; +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; + +use crate::msg::{ExecuteMsg, InstantiateMsg, NftContract, QueryMsg}; +use crate::state::{Config, CONFIG, DAO, INITIAL_NFTS}; +use crate::ContractError; + +pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cw721-roles"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const INSTANTIATE_NFT_CONTRACT_REPLY_ID: u64 = 0; + +#[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)?; + + DAO.save(deps.storage, &info.sender)?; + + match msg.nft_contract { + NftContract::Existing { address } => { + let config = Config { + nft_address: deps.api.addr_validate(&address)?, + }; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attribute("nft_contract", address)) + } + NftContract::New { + code_id, + label, + name, + symbol, + initial_nfts, + } => { + // Check there is at least one NFT to initialize + if initial_nfts.is_empty() { + return Err(ContractError::NoInitialNfts {}); + } + + // Save initial NFTs for use in reply + INITIAL_NFTS.save(deps.storage, &initial_nfts)?; + + // Create instantiate submessage for NFT roles contract + let msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + code_id, + funds: vec![], + admin: Some(info.sender.to_string()), + label, + msg: to_json_binary(&Cw721InstantiateMsg { + name, + symbol, + // Admin must be set to contract to mint initial NFTs + minter: env.contract.address.to_string(), + })?, + }, + INSTANTIATE_NFT_CONTRACT_REPLY_ID, + ); + + Ok(Response::default().add_submessage(msg)) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsg, +) -> Result, ContractError> { + Err(ContractError::NoExecute {}) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => query_config(deps), + QueryMsg::Dao {} => query_dao(deps), + QueryMsg::VotingPowerAtHeight { address, height } => { + query_voting_power_at_height(deps, env, address, height) + } + QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height), + QueryMsg::Info {} => query_info(deps), + } +} + +pub fn query_voting_power_at_height( + deps: Deps, + env: Env, + address: String, + at_height: Option, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let member: MemberResponse = deps.querier.query_wasm_smart( + config.nft_address, + &Cw721QueryMsg::::Extension { + msg: QueryExt::Member { + addr: address, + at_height, + }, + }, + )?; + + to_json_binary(&dao_interface::voting::VotingPowerAtHeightResponse { + power: member.weight.unwrap_or(0).into(), + height: at_height.unwrap_or(env.block.height), + }) +} + +pub fn query_total_power_at_height( + deps: Deps, + env: Env, + at_height: Option, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let total: TotalWeightResponse = deps.querier.query_wasm_smart( + config.nft_address, + &Cw721QueryMsg::::Extension { + msg: QueryExt::TotalWeight { at_height }, + }, + )?; + + to_json_binary(&dao_interface::voting::TotalPowerAtHeightResponse { + power: total.weight.into(), + height: at_height.unwrap_or(env.block.height), + }) +} + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + to_json_binary(&config) +} + +pub fn query_dao(deps: Deps) -> StdResult { + let dao = DAO.load(deps.storage)?; + to_json_binary(&dao) +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = cw2::get_contract_version(deps.storage)?; + to_json_binary(&dao_interface::voting::InfoResponse { info }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + INSTANTIATE_NFT_CONTRACT_REPLY_ID => { + let res = parse_reply_instantiate_data(msg); + match res { + Ok(res) => { + let dao = DAO.load(deps.storage)?; + let nft_contract = res.contract_address; + + // Save config + let config = Config { + nft_address: deps.api.addr_validate(&nft_contract)?, + }; + CONFIG.save(deps.storage, &config)?; + + let initial_nfts = INITIAL_NFTS.load(deps.storage)?; + + // Add mint submessages + let mint_submessages: Vec = initial_nfts + .iter() + .flat_map(|nft| -> Result { + Ok(SubMsg::new(WasmMsg::Execute { + contract_addr: nft_contract.clone(), + funds: vec![], + msg: to_json_binary( + &Cw721ExecuteMsg::::Mint { + token_id: nft.token_id.clone(), + owner: nft.owner.clone(), + token_uri: nft.token_uri.clone(), + extension: MetadataExt { + role: nft.clone().extension.role, + weight: nft.extension.weight, + }, + }, + )?, + })) + }) + .collect::>(); + + // Clear space + INITIAL_NFTS.remove(deps.storage); + + // Update minter message + let update_minter_msg = WasmMsg::Execute { + contract_addr: nft_contract.clone(), + msg: to_json_binary( + &Cw721ExecuteMsg::::UpdateOwnership( + Action::TransferOwnership { + new_owner: dao.to_string(), + expiry: None, + }, + ), + )?, + funds: vec![], + }; + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attribute("nft_contract", nft_contract) + .add_message(update_minter_msg) + .add_submessages(mint_submessages)) + } + Err(_) => Err(ContractError::NftInstantiateError {}), + } + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/error.rs b/contracts/voting/dao-voting-cw721-roles/src/error.rs new file mode 100644 index 000000000..2fa498222 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/error.rs @@ -0,0 +1,23 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error("Error instantiating cw721-roles contract")] + NftInstantiateError {}, + + #[error("This contract only supports queries")] + NoExecute {}, + + #[error("New cw721-roles contract must be instantiated with at least one NFT")] + NoInitialNfts {}, + + #[error("Only the owner of this contract my execute this message")] + NotOwner {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/lib.rs b/contracts/voting/dao-voting-cw721-roles/src/lib.rs new file mode 100644 index 000000000..d4a73c5be --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/lib.rs @@ -0,0 +1,11 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod testing; + +pub use crate::error::ContractError; diff --git a/contracts/voting/dao-voting-cw721-roles/src/msg.rs b/contracts/voting/dao-voting-cw721-roles/src/msg.rs new file mode 100644 index 000000000..b15099529 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/msg.rs @@ -0,0 +1,55 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use dao_cw721_extensions::roles::MetadataExt; +use dao_dao_macros::voting_module_query; + +#[cw_serde] +pub struct NftMintMsg { + /// Unique ID of the NFT + pub token_id: String, + /// The owner of the newly minter NFT + pub owner: String, + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + /// Any custom extension used by this contract + pub extension: MetadataExt, +} + +#[cw_serde] +pub enum NftContract { + Existing { + /// Address of an already instantiated cw721-weighted-roles token contract. + address: String, + }, + New { + /// Code ID for cw721 token contract. + code_id: u64, + /// Label to use for instantiated cw721 contract. + label: String, + /// NFT collection name + name: String, + /// NFT collection symbol + symbol: String, + /// Initial NFTs to mint when instantiating the new cw721 contract. + /// If empty, an error is thrown. + initial_nfts: Vec, + }, +} + +#[cw_serde] +pub struct InstantiateMsg { + /// Info about the associated NFT contract + pub nft_contract: NftContract, +} + +#[cw_serde] +pub enum ExecuteMsg {} + +#[voting_module_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(crate::state::Config)] + Config {}, +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/state.rs b/contracts/voting/dao-voting-cw721-roles/src/state.rs new file mode 100644 index 000000000..fb6e98779 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/state.rs @@ -0,0 +1,16 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +use crate::msg::NftMintMsg; + +#[cw_serde] +pub struct Config { + pub nft_address: Addr, +} + +pub const CONFIG: Item = Item::new("config"); +pub const DAO: Item = Item::new("dao"); + +// Holds initial NFTs messages during instantiation. +pub const INITIAL_NFTS: Item> = Item::new("initial_nfts"); diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/execute.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/execute.rs new file mode 100644 index 000000000..081a2beaf --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/execute.rs @@ -0,0 +1,28 @@ +use cosmwasm_std::Addr; +use cw_multi_test::{App, AppResponse, Executor}; +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt}; + +use anyhow::Result as AnyResult; + +pub fn mint_nft( + app: &mut App, + cw721: &Addr, + sender: &str, + receiver: &str, + token_id: &str, +) -> AnyResult { + app.execute_contract( + Addr::unchecked(sender), + cw721.clone(), + &cw721_base::ExecuteMsg::::Mint { + token_id: token_id.to_string(), + owner: receiver.to_string(), + token_uri: None, + extension: MetadataExt { + role: Some("admin".to_string()), + weight: 1, + }, + }, + &[], + ) +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/instantiate.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/instantiate.rs new file mode 100644 index 000000000..59cabddc7 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/instantiate.rs @@ -0,0 +1,24 @@ +use cosmwasm_std::Addr; +use cw_multi_test::{App, Executor}; +use dao_testing::contracts::cw721_roles_contract; + +pub fn instantiate_cw721_roles(app: &mut App, sender: &str, minter: &str) -> (Addr, u64) { + let cw721_id = app.store_code(cw721_roles_contract()); + + let cw721_addr = app + .instantiate_contract( + cw721_id, + Addr::unchecked(sender), + &cw721_base::InstantiateMsg { + name: "bad kids".to_string(), + symbol: "bad kids".to_string(), + minter: minter.to_string(), + }, + &[], + "cw721_roles".to_string(), + None, + ) + .unwrap(); + + (cw721_addr, cw721_id) +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/mod.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/mod.rs new file mode 100644 index 000000000..98cebccd9 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/mod.rs @@ -0,0 +1,47 @@ +mod execute; +mod instantiate; +mod queries; +mod tests; + +use cosmwasm_std::Addr; +use cw_multi_test::{App, Executor}; +use dao_testing::contracts::dao_voting_cw721_roles_contract; + +use crate::msg::{InstantiateMsg, NftContract, NftMintMsg}; + +use self::instantiate::instantiate_cw721_roles; + +/// Address used as the owner, instantiator, and minter. +pub(crate) const CREATOR_ADDR: &str = "creator"; + +pub(crate) struct CommonTest { + app: App, + module_addr: Addr, +} + +pub(crate) fn setup_test(initial_nfts: Vec) -> CommonTest { + let mut app = App::default(); + let module_id = app.store_code(dao_voting_cw721_roles_contract()); + + let (_, cw721_id) = instantiate_cw721_roles(&mut app, CREATOR_ADDR, CREATOR_ADDR); + let module_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "cw721-roles".to_string(), + name: "Job Titles".to_string(), + symbol: "TITLES".to_string(), + initial_nfts, + }, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + CommonTest { app, module_addr } +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/queries.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/queries.rs new file mode 100644 index 000000000..dfc3f3468 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/queries.rs @@ -0,0 +1,52 @@ +use cosmwasm_std::{Addr, StdResult}; +use cw_multi_test::App; +use dao_cw721_extensions::roles::QueryExt; +use dao_interface::voting::{ + InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; + +use crate::{msg::QueryMsg, state::Config}; + +pub fn query_config(app: &App, module: &Addr) -> StdResult { + let config = app.wrap().query_wasm_smart(module, &QueryMsg::Config {})?; + Ok(config) +} + +pub fn query_voting_power( + app: &App, + module: &Addr, + addr: &str, + height: Option, +) -> StdResult { + let power = app.wrap().query_wasm_smart( + module, + &QueryMsg::VotingPowerAtHeight { + address: addr.to_string(), + height, + }, + )?; + Ok(power) +} + +pub fn query_total_power( + app: &App, + module: &Addr, + height: Option, +) -> StdResult { + let power = app + .wrap() + .query_wasm_smart(module, &QueryMsg::TotalPowerAtHeight { height })?; + Ok(power) +} + +pub fn query_info(app: &App, module: &Addr) -> StdResult { + let info = app.wrap().query_wasm_smart(module, &QueryMsg::Info {})?; + Ok(info) +} + +pub fn query_minter(app: &App, nft: &Addr) -> StdResult { + let minter = app + .wrap() + .query_wasm_smart(nft, &cw721_base::QueryMsg::::Minter {})?; + Ok(minter) +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/tests.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/tests.rs new file mode 100644 index 000000000..78753a430 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/tests.rs @@ -0,0 +1,126 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_multi_test::{App, Executor}; +use dao_cw721_extensions::roles::MetadataExt; +use dao_testing::contracts::dao_voting_cw721_roles_contract; + +use crate::{ + msg::{InstantiateMsg, NftContract, NftMintMsg}, + state::Config, + testing::{ + execute::mint_nft, + queries::{query_config, query_info, query_minter, query_total_power, query_voting_power}, + }, +}; + +use super::{instantiate::instantiate_cw721_roles, setup_test, CommonTest, CREATOR_ADDR}; + +#[test] +fn test_info_query_works() -> anyhow::Result<()> { + let CommonTest { + app, module_addr, .. + } = setup_test(vec![NftMintMsg { + token_id: "1".to_string(), + owner: CREATOR_ADDR.to_string(), + token_uri: None, + extension: MetadataExt { + role: None, + weight: 1, + }, + }]); + let info = query_info(&app, &module_addr)?; + assert_eq!(info.info.version, env!("CARGO_PKG_VERSION").to_string()); + Ok(()) +} + +#[test] +#[should_panic(expected = "New cw721-roles contract must be instantiated with at least one NFT")] +fn test_instantiate_no_roles_fails() { + setup_test(vec![]); +} + +#[test] +fn test_use_existing_nft_contract() { + let mut app = App::default(); + let module_id = app.store_code(dao_voting_cw721_roles_contract()); + + let (cw721_addr, _) = instantiate_cw721_roles(&mut app, CREATOR_ADDR, CREATOR_ADDR); + let module_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Existing { + address: cw721_addr.clone().to_string(), + }, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + // Get total power + let total = query_total_power(&app, &module_addr, None).unwrap(); + assert_eq!(total.power, Uint128::zero()); + + // Creator mints themselves a new NFT + mint_nft(&mut app, &cw721_addr, CREATOR_ADDR, CREATOR_ADDR, "1").unwrap(); + + // Get voting power for creator + let vp = query_voting_power(&app, &module_addr, CREATOR_ADDR, None).unwrap(); + assert_eq!(vp.power, Uint128::new(1)); +} + +#[test] +fn test_voting_queries() { + let CommonTest { + mut app, + module_addr, + .. + } = setup_test(vec![NftMintMsg { + token_id: "1".to_string(), + owner: CREATOR_ADDR.to_string(), + token_uri: None, + extension: MetadataExt { + role: Some("admin".to_string()), + weight: 1, + }, + }]); + + // Get config + let config: Config = query_config(&app, &module_addr).unwrap(); + let cw721_addr = config.nft_address; + + // Get NFT minter + let minter = query_minter(&app, &cw721_addr.clone()).unwrap(); + // Minter should be the contract that instantiated the cw721 contract. + // In the test setup, this is the module_addr but would normally be + // the dao-core contract. + assert_eq!(minter.minter, Some(module_addr.to_string())); + + // Get total power + let total = query_total_power(&app, &module_addr, None).unwrap(); + assert_eq!(total.power, Uint128::new(1)); + + // Get voting power for creator + let vp = query_voting_power(&app, &module_addr, CREATOR_ADDR, None).unwrap(); + assert_eq!(vp.power, Uint128::new(1)); + + // Mint a new NFT + mint_nft( + &mut app, + &cw721_addr, + module_addr.as_ref(), + CREATOR_ADDR, + "2", + ) + .unwrap(); + + // Get total power + let total = query_total_power(&app, &module_addr, None).unwrap(); + assert_eq!(total.power, Uint128::new(2)); + + // Get voting power for creator + let vp = query_voting_power(&app, &module_addr, CREATOR_ADDR, None).unwrap(); + assert_eq!(vp.power, Uint128::new(2)); +} diff --git a/contracts/voting/dao-voting-cw721-staked/Cargo.toml b/contracts/voting/dao-voting-cw721-staked/Cargo.toml index 0401b42ad..a3ad240f8 100644 --- a/contracts/voting/dao-voting-cw721-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw721-staked/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dao-voting-cw721-staked" -authors = ["CypherApe cypherape@protonmail.com"] +authors = ["CypherApe cypherape@protonmail.com", "Jake Hartnell", "ekez"] description = "A DAO DAO voting module based on staked cw721 tokens." edition = { workspace = true } license = { workspace = true } @@ -11,25 +11,42 @@ version = { workspace = true } 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 } cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } cw-controllers = { workspace = true } -dao-dao-macros = { workspace = true } -dao-interface = { workspace = true } +cw-hooks = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } cw721-controllers = { workspace = true } cw-paginate-storage = { workspace = true } -cw721 = { workspace = true } cw-utils = { workspace = true } cw2 = { workspace = true } +dao-dao-macros = { workspace = true } +dao-hooks = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } thiserror = { workspace = true } [dev-dependencies] -cw721-base = { workspace = true } -cw-multi-test = { workspace = true } anyhow = { workspace = true } -dao-testing = { workspace = true } +cw-multi-test = { workspace = true } +dao-proposal-single = { workspace = true } +dao-proposal-hook-counter = { workspace = true } +dao-test-custom-factory = { workspace = true } +dao-testing = { workspace = true, features = ["test-tube"] } +osmosis-std = { workspace = true } +osmosis-test-tube = { workspace = true } +serde = { workspace = true } diff --git a/contracts/voting/dao-voting-cw721-staked/README.md b/contracts/voting/dao-voting-cw721-staked/README.md index 015c8d32d..595faec5f 100644 --- a/contracts/voting/dao-voting-cw721-staked/README.md +++ b/contracts/voting/dao-voting-cw721-staked/README.md @@ -1,8 +1,18 @@ -# Stake CW721 - -This is a basic implementation of a cw721 staking contract. Staked -tokens can be unbonded with a configurable unbonding period. Staked -balances can be queried at any arbitrary height by external -contracts. 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-cw721-staked` + +[![dao-voting-cw721-staked on crates.io](https://img.shields.io/crates/v/dao-voting-cw721-staked.svg?logo=rust)](https://crates.io/crates/dao-voting-cw721-staked) +[![docs.rs](https://img.shields.io/docsrs/dao-voting-cw721-staked?logo=docsdotrs)](https://docs.rs/dao-voting-cw721-staked/latest/dao_voting_cw721_staked/) + +This is a basic implementation of an NFT staking contract. + +Staked tokens can be unbonded with a configurable unbonding period. Staked balances can be queried at any arbitrary height by external contracts. 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-cw721-staked` can be used with an `existing` NFT collection or to create a `new` `cw721` collection upon instantiation (with the DAO as admin and `minter`). + +To support Stargaze NFTs and other custom NFT contracts or setups with minters (such as the Stargaze Open Edition minter), this contract also supports a `factory` pattern which takes a single `WasmMsg::Execute` message that calls into a custom factory contract. + +**NOTE:** when using the factory pattern, it is important to only use a trusted factory contract, as all validation happens in the factory contract. + +Those implementing custom factory contracts MUST handle any validation that is to happen, and the custom `WasmMsg::Execute` message MUST include `NftFactoryCallback` data respectively. + +The [dao-test-custom-factory contract](../test/dao-test-custom-factory) provides an example of how this can be done and is used for tests. It is NOT production ready, but meant to serve as an example for building factory contracts. diff --git a/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json b/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json index 3287915f8..fc41107e5 100644 --- a/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json +++ b/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json @@ -1,30 +1,34 @@ { "contract_name": "dao-voting-cw721-staked", - "contract_version": "2.2.0", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", "type": "object", "required": [ - "nft_address" + "nft_contract" ], "properties": { - "nft_address": { - "description": "Address of the cw721 NFT contract that may be staked.", - "type": "string" - }, - "owner": { - "description": "May change unstaking duration and add hooks.", + "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" } ] }, + "nft_contract": { + "description": "Address of the cw721 NFT contract that may be staked.", + "allOf": [ + { + "$ref": "#/definitions/NftContract" + } + ] + }, "unstaking_duration": { "description": "Amount of time between unstaking and tokens being avaliable. To unstake with no delay, leave as `None`.", "anyOf": [ @@ -39,24 +43,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 @@ -65,14 +69,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 } }, @@ -80,6 +92,14 @@ } ] }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "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": [ @@ -113,6 +133,92 @@ "additionalProperties": false } ] + }, + "NftContract": { + "oneOf": [ + { + "description": "Uses an existing cw721 or sg721 token contract.", + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of an already instantiated cw721 or sg721 token contract.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Creates a new NFT collection used for staking and governance.", + "type": "object", + "required": [ + "new" + ], + "properties": { + "new": { + "type": "object", + "required": [ + "code_id", + "initial_nfts", + "label", + "msg" + ], + "properties": { + "code_id": { + "description": "Code ID for cw721 token contract.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "initial_nfts": { + "description": "Initial NFTs to mint when creating the NFT contract. If empty, an error is thrown. The binary should be a valid mint message for the corresponding cw721 contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Binary" + } + }, + "label": { + "description": "Label to use for instantiated cw721 contract.", + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Uses a factory contract that must return the address of the NFT contract. The binary must serialize to a `WasmMsg::Execute` message. Validation happens in the factory contract itself, so be sure to use a trusted factory contract.", + "type": "object", + "required": [ + "factory" + ], + "properties": { + "factory": { + "$ref": "#/definitions/Binary" + } + }, + "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" } } }, @@ -159,6 +265,7 @@ "additionalProperties": false }, { + "description": "Claim NFTs that have been unstaked for the specified duration.", "type": "object", "required": [ "claim_nfts" @@ -172,6 +279,7 @@ "additionalProperties": false }, { + "description": "Updates the contract configuration, namely unstaking duration. Only callable by the DAO that initialized this voting contract.", "type": "object", "required": [ "update_config" @@ -189,12 +297,6 @@ "type": "null" } ] - }, - "owner": { - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -203,6 +305,7 @@ "additionalProperties": false }, { + "description": "Adds a hook which is called on staking / unstaking events. Only callable by the DAO that initialized this voting contract.", "type": "object", "required": [ "add_hook" @@ -224,6 +327,7 @@ "additionalProperties": false }, { + "description": "Removes a hook which is called on staking / unstaking events. Only callable by the DAO that initialized this voting contract.", "type": "object", "required": [ "remove_hook" @@ -243,9 +347,84 @@ } }, "additionalProperties": false + }, + { + "description": "Sets the active threshold to a new value. Only callable by the DAO that initialized this voting contract.", + "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 } ], "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 + } + ] + }, "Binary": { "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" @@ -271,6 +450,10 @@ }, "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": [ @@ -304,6 +487,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" } } }, @@ -393,6 +580,32 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "active_threshold" + ], + "properties": { + "active_threshold": { + "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", @@ -480,26 +693,93 @@ "migrate": null, "sudo": null, "responses": { - "config": { + "active_threshold": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Config", + "title": "ActiveThresholdResponse", "type": "object", - "required": [ - "nft_address" - ], "properties": { - "nft_address": { - "$ref": "#/definitions/Addr" - }, - "owner": { + "active_threshold": { "anyOf": [ { - "$ref": "#/definitions/Addr" + "$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" + } + } + }, + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "nft_address" + ], + "properties": { + "nft_address": { + "$ref": "#/definitions/Addr" }, "unstaking_duration": { "anyOf": [ @@ -611,6 +891,11 @@ } } }, + "is_active": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "nft_claims": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "NftClaimsResponse", diff --git a/contracts/voting/dao-voting-cw721-staked/src/contract.rs b/contracts/voting/dao-voting-cw721-staked/src/contract.rs index 1afd033c8..fc83cce13 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/contract.rs @@ -1,24 +1,70 @@ -use crate::hooks::{stake_hook_msgs, unstake_hook_msgs}; #[cfg(not(feature = "library"))] -use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use crate::state::{ - register_staked_nft, register_unstaked_nfts, Config, CONFIG, DAO, HOOKS, MAX_CLAIMS, - NFT_BALANCES, NFT_CLAIMS, STAKED_NFTS_PER_OWNER, TOTAL_STAKED_NFTS, -}; -use crate::ContractError; +use cosmwasm_std::entry_point; use cosmwasm_std::{ - entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, - StdResult, Uint128, WasmMsg, + from_json, to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, + Reply, Response, StdError, StdResult, SubMsg, Uint128, Uint256, WasmMsg, }; -use cw2::set_contract_version; -use cw721::Cw721ReceiveMsg; +use cw2::{get_contract_version, set_contract_version, ContractVersion}; +use cw721::{Cw721QueryMsg, Cw721ReceiveMsg, NumTokensResponse}; use cw_storage_plus::Bound; -use cw_utils::Duration; -use dao_interface::state::Admin; +use cw_utils::{parse_reply_execute_data, parse_reply_instantiate_data, Duration}; +use dao_hooks::nft_stake::{stake_nft_hook_msgs, unstake_nft_hook_msgs}; +use dao_interface::state::ModuleInstantiateCallback; +use dao_interface::{nft::NftFactoryCallback, voting::IsActiveResponse}; +use dao_voting::duration::validate_duration; +use dao_voting::threshold::{ + assert_valid_absolute_count_threshold, assert_valid_percentage_threshold, ActiveThreshold, + ActiveThresholdResponse, +}; + +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg}; +use crate::state::{ + register_staked_nft, register_unstaked_nfts, Config, ACTIVE_THRESHOLD, CONFIG, DAO, HOOKS, + INITIAL_NFTS, MAX_CLAIMS, NFT_BALANCES, NFT_CLAIMS, STAKED_NFTS_PER_OWNER, TOTAL_STAKED_NFTS, +}; +use crate::ContractError; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cw721-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +const INSTANTIATE_NFT_CONTRACT_REPLY_ID: u64 = 0; +const VALIDATE_SUPPLY_REPLY_ID: u64 = 1; +const FACTORY_EXECUTE_REPLY_ID: u64 = 2; + +// We multiply by this when calculating needed power for being active +// when using active threshold with percent +const PRECISION_FACTOR: u128 = 10u128.pow(9); + +// Supported NFT instantiation messages +pub enum NftInstantiateMsg { + Cw721(cw721_base::InstantiateMsg), +} + +impl NftInstantiateMsg { + fn modify_instantiate_msg(&mut self, minter: &str) { + match self { + // Update minter for cw721 NFTs + NftInstantiateMsg::Cw721(msg) => msg.minter = minter.to_string(), + } + } + + fn to_json_binary(&self) -> Result { + match self { + NftInstantiateMsg::Cw721(msg) => to_json_binary(&msg), + } + } +} + +pub fn try_deserialize_nft_instantiate_msg( + instantiate_msg: Binary, +) -> Result { + if let Ok(cw721_msg) = from_json::(&instantiate_msg) { + return Ok(NftInstantiateMsg::Cw721(cw721_msg)); + } + + Err(ContractError::NftInstantiateError {}) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -30,33 +76,122 @@ pub fn instantiate( DAO.save(deps.storage, &info.sender)?; - let owner = msg - .owner - .as_ref() - .map(|owner| match owner { - Admin::Address { addr } => deps.api.addr_validate(addr), - Admin::CoreModule {} => Ok(info.sender), - }) - .transpose()?; + // Validate unstaking duration + validate_duration(msg.unstaking_duration)?; - let config = Config { - owner: owner.clone(), - nft_address: deps.api.addr_validate(&msg.nft_address)?, - unstaking_duration: msg.unstaking_duration, - }; - CONFIG.save(deps.storage, &config)?; + // Validate active threshold if configured + if let Some(active_threshold) = msg.active_threshold.as_ref() { + match active_threshold { + ActiveThreshold::Percentage { percent } => { + assert_valid_percentage_threshold(*percent)?; + } + ActiveThreshold::AbsoluteCount { count } => { + // Check Absolute count is less than the supply of NFTs for existing + // NFT contracts. For new NFT contracts, we will check this in the reply. + if let NftContract::Existing { ref address } = msg.nft_contract { + let nft_supply: NumTokensResponse = deps + .querier + .query_wasm_smart(address, &Cw721QueryMsg::NumTokens {})?; + // Check the absolute count is less than the supply of NFTs and + // greater than zero. + assert_valid_absolute_count_threshold( + *count, + Uint128::new(nft_supply.count.into()), + )?; + } + } + } + ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?; + } TOTAL_STAKED_NFTS.save(deps.storage, &Uint128::zero(), env.block.height)?; - Ok(Response::default() - .add_attribute("method", "instantiate") - .add_attribute("nft_contract", msg.nft_address) - .add_attribute( - "owner", - owner - .map(|a| a.into_string()) - .unwrap_or_else(|| "None".to_string()), - )) + match msg.nft_contract { + NftContract::Existing { address } => { + let config = Config { + nft_address: deps.api.addr_validate(&address)?, + unstaking_duration: msg.unstaking_duration, + }; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attribute("nft_contract", address)) + } + NftContract::New { + code_id, + label, + msg: instantiate_msg, + initial_nfts, + } => { + // Deserialize the binary msg into cw721 + let mut instantiate_msg = try_deserialize_nft_instantiate_msg(instantiate_msg)?; + + // Modify the InstantiateMsg such that the minter is now this contract. + // We will update ownership of the NFT contract to be the DAO in the submessage reply. + instantiate_msg.modify_instantiate_msg(env.contract.address.as_str()); + + // Check there is at least one NFT to initialize + if initial_nfts.is_empty() { + return Err(ContractError::NoInitialNfts {}); + } + + // Save config with empty nft_address + let config = Config { + nft_address: Addr::unchecked(""), + unstaking_duration: msg.unstaking_duration, + }; + CONFIG.save(deps.storage, &config)?; + + // Save initial NFTs for use in reply + INITIAL_NFTS.save(deps.storage, &initial_nfts)?; + + // Create instantiate submessage for NFT contract + let instantiate_msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + code_id, + funds: vec![], + admin: Some(info.sender.to_string()), + label, + msg: instantiate_msg.to_json_binary()?, + }, + INSTANTIATE_NFT_CONTRACT_REPLY_ID, + ); + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_submessage(instantiate_msg)) + } + NftContract::Factory(binary) => match from_json(binary)? { + WasmMsg::Execute { + msg: wasm_msg, + contract_addr, + funds, + } => { + // Save config with empty nft_address + let config = Config { + nft_address: Addr::unchecked(""), + unstaking_duration: msg.unstaking_duration, + }; + CONFIG.save(deps.storage, &config)?; + + // Call factory contract. Use only a trusted factory contract, + // as this is a critical security component and valdiation of + // setup will happen in the factory. + Ok(Response::new() + .add_attribute("action", "intantiate") + .add_submessage(SubMsg::reply_on_success( + WasmMsg::Execute { + contract_addr, + msg: wasm_msg, + funds, + }, + FACTORY_EXECUTE_REPLY_ID, + ))) + } + _ => Err(ContractError::UnsupportedFactoryMsg {}), + }, + } } #[cfg_attr(not(feature = "library"), entry_point)] @@ -70,11 +205,12 @@ pub fn execute( ExecuteMsg::ReceiveNft(msg) => execute_stake(deps, env, info, msg), ExecuteMsg::Unstake { token_ids } => execute_unstake(deps, env, info, token_ids), ExecuteMsg::ClaimNfts {} => execute_claim_nfts(deps, env, info), - ExecuteMsg::UpdateConfig { owner, duration } => { - execute_update_config(info, deps, owner, duration) - } + ExecuteMsg::UpdateConfig { duration } => execute_update_config(info, deps, duration), ExecuteMsg::AddHook { addr } => execute_add_hook(deps, info, addr), ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, info, addr), + ExecuteMsg::UpdateActiveThreshold { new_threshold } => { + execute_update_active_threshold(deps, env, info, new_threshold) + } } } @@ -93,7 +229,12 @@ pub fn execute_stake( } let staker = deps.api.addr_validate(&wrapper.sender)?; register_staked_nft(deps.storage, env.block.height, &staker, &wrapper.token_id)?; - let hook_msgs = stake_hook_msgs(deps.storage, staker.clone(), wrapper.token_id.clone())?; + let hook_msgs = stake_nft_hook_msgs( + HOOKS, + deps.storage, + staker.clone(), + wrapper.token_id.clone(), + )?; Ok(Response::default() .add_submessages(hook_msgs) .add_attribute("action", "stake") @@ -144,7 +285,8 @@ pub fn execute_unstake( // so if we reach this point in execution, we may safely create // claims. - let hook_msgs = unstake_hook_msgs(deps.storage, info.sender.clone(), token_ids.clone())?; + let hook_msgs = + unstake_nft_hook_msgs(HOOKS, deps.storage, info.sender.clone(), token_ids.clone())?; let config = CONFIG.load(deps.storage)?; match config.unstaking_duration { @@ -154,7 +296,7 @@ pub fn execute_unstake( .map(|token_id| -> StdResult { Ok(cosmwasm_std::WasmMsg::Execute { contract_addr: config.nft_address.to_string(), - msg: to_binary(&cw721::Cw721ExecuteMsg::TransferNft { + msg: to_json_binary(&cw721::Cw721ExecuteMsg::TransferNft { recipient: info.sender.to_string(), token_id, })?, @@ -214,7 +356,7 @@ pub fn execute_claim_nfts( .map(|nft| -> StdResult { Ok(WasmMsg::Execute { contract_addr: config.nft_address.to_string(), - msg: to_binary(&cw721::Cw721ExecuteMsg::TransferNft { + msg: to_json_binary(&cw721::Cw721ExecuteMsg::TransferNft { recipient: info.sender.to_string(), token_id: nft, })?, @@ -233,32 +375,24 @@ pub fn execute_claim_nfts( pub fn execute_update_config( info: MessageInfo, deps: DepsMut, - new_owner: Option, duration: Option, ) -> Result { let mut config: Config = CONFIG.load(deps.storage)?; + let dao = DAO.load(deps.storage)?; - if config.owner.map_or(true, |owner| owner != info.sender) { - return Err(ContractError::NotOwner {}); + // Only the DAO can update the config. + if info.sender != dao { + return Err(ContractError::Unauthorized {}); } - let new_owner = new_owner - .map(|new_owner| deps.api.addr_validate(&new_owner)) - .transpose()?; + // Validate unstaking duration + validate_duration(duration)?; - config.owner = new_owner; config.unstaking_duration = duration; CONFIG.save(deps.storage, &config)?; Ok(Response::default() .add_attribute("action", "update_config") - .add_attribute( - "owner", - config - .owner - .map(|a| a.to_string()) - .unwrap_or_else(|| "none".to_string()), - ) .add_attribute( "unstaking_duration", config @@ -273,9 +407,11 @@ pub fn execute_add_hook( info: MessageInfo, addr: String, ) -> Result { - let config: Config = CONFIG.load(deps.storage)?; - if config.owner.map_or(true, |owner| owner != info.sender) { - return Err(ContractError::NotOwner {}); + let dao = DAO.load(deps.storage)?; + + // Only the DAO can add a hook + if info.sender != dao { + return Err(ContractError::Unauthorized {}); } let hook = deps.api.addr_validate(&addr)?; @@ -291,9 +427,11 @@ pub fn execute_remove_hook( info: MessageInfo, addr: String, ) -> Result { - let config: Config = CONFIG.load(deps.storage)?; - if config.owner.map_or(true, |owner| owner != info.sender) { - return Err(ContractError::NotOwner {}); + let dao = DAO.load(deps.storage)?; + + // Only the DAO can remove a hook + if info.sender != dao { + return Err(ContractError::Unauthorized {}); } let hook = deps.api.addr_validate(&addr)?; @@ -304,23 +442,135 @@ pub fn execute_remove_hook( .add_attribute("hook", addr)) } +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 {}); + } + + let config = CONFIG.load(deps.storage)?; + if let Some(active_threshold) = new_active_threshold { + match active_threshold { + ActiveThreshold::Percentage { percent } => { + assert_valid_percentage_threshold(percent)?; + } + ActiveThreshold::AbsoluteCount { count } => { + let nft_supply: NumTokensResponse = deps + .querier + .query_wasm_smart(config.nft_address, &Cw721QueryMsg::NumTokens {})?; + assert_valid_absolute_count_threshold( + count, + Uint128::new(nft_supply.count.into()), + )?; + } + } + ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; + } else { + ACTIVE_THRESHOLD.remove(deps.storage); + } + + Ok(Response::new().add_attribute("action", "update_active_threshold")) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { + QueryMsg::ActiveThreshold {} => query_active_threshold(deps), QueryMsg::Config {} => query_config(deps), QueryMsg::Dao {} => query_dao(deps), + QueryMsg::Info {} => query_info(deps), + QueryMsg::IsActive {} => query_is_active(deps, env), QueryMsg::NftClaims { address } => query_nft_claims(deps, address), QueryMsg::Hooks {} => query_hooks(deps), - QueryMsg::VotingPowerAtHeight { address, height } => { - query_voting_power_at_height(deps, env, address, height) - } - QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height), - QueryMsg::Info {} => query_info(deps), QueryMsg::StakedNfts { address, start_after, limit, } => query_staked_nfts(deps, address, start_after, limit), + QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height), + QueryMsg::VotingPowerAtHeight { address, height } => { + query_voting_power_at_height(deps, env, address, height) + } + } +} + +pub fn query_active_threshold(deps: Deps) -> StdResult { + to_json_binary(&ActiveThresholdResponse { + active_threshold: ACTIVE_THRESHOLD.may_load(deps.storage)?, + }) +} + +pub fn query_is_active(deps: Deps, env: Env) -> StdResult { + let threshold = ACTIVE_THRESHOLD.may_load(deps.storage)?; + if let Some(threshold) = threshold { + let config = CONFIG.load(deps.storage)?; + let staked_nfts = TOTAL_STAKED_NFTS + .may_load_at_height(deps.storage, env.block.height)? + .unwrap_or_default(); + let total_nfts: NumTokensResponse = deps.querier.query_wasm_smart( + config.nft_address, + &cw721_base::msg::QueryMsg::::NumTokens {}, + )?; + + match threshold { + ActiveThreshold::AbsoluteCount { count } => to_json_binary(&IsActiveResponse { + active: staked_nfts >= count, + }), + ActiveThreshold::Percentage { percent } => { + // Check if there are any staked NFTs + if staked_nfts.is_zero() { + return to_json_binary(&IsActiveResponse { active: false }); + } + + // 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^64] + // as it tracks the count of NFT tokens which has + // a max supply of 2^64. + // + // with our precision factor being 10^9: + // + // total_nfts <= 2^64 * 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_nfts_count = Uint128::from(total_nfts.count).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_nfts_count.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(); + + // staked_nfts >= total_nfts * percent + to_json_binary(&IsActiveResponse { + active: staked_nfts >= count, + }) + } + } + } else { + to_json_binary(&IsActiveResponse { active: true }) } } @@ -335,7 +585,7 @@ pub fn query_voting_power_at_height( let power = NFT_BALANCES .may_load_at_height(deps.storage, &address, height)? .unwrap_or_default(); - to_binary(&dao_interface::voting::VotingPowerAtHeightResponse { power, height }) + to_json_binary(&dao_interface::voting::VotingPowerAtHeightResponse { power, height }) } pub fn query_total_power_at_height(deps: Deps, env: Env, height: Option) -> StdResult { @@ -343,30 +593,30 @@ pub fn query_total_power_at_height(deps: Deps, env: Env, height: Option) -> let power = TOTAL_STAKED_NFTS .may_load_at_height(deps.storage, height)? .unwrap_or_default(); - to_binary(&dao_interface::voting::TotalPowerAtHeightResponse { power, height }) + to_json_binary(&dao_interface::voting::TotalPowerAtHeightResponse { power, height }) } pub fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; - to_binary(&config) + to_json_binary(&config) } pub fn query_dao(deps: Deps) -> StdResult { let dao = DAO.load(deps.storage)?; - to_binary(&dao) + to_json_binary(&dao) } pub fn query_nft_claims(deps: Deps, address: String) -> StdResult { - to_binary(&NFT_CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?) + to_json_binary(&NFT_CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?) } pub fn query_hooks(deps: Deps) -> StdResult { - to_binary(&HOOKS.query_hooks(deps)?) + to_json_binary(&HOOKS.query_hooks(deps)?) } pub fn query_info(deps: Deps) -> StdResult { let info = cw2::get_contract_version(deps.storage)?; - to_binary(&dao_interface::voting::InfoResponse { info }) + to_json_binary(&dao_interface::voting::InfoResponse { info }) } pub fn query_staked_nfts( @@ -389,5 +639,163 @@ pub fn query_staked_nfts( Some(l) => range.take(l as usize).collect(), None => range.collect(), }; - to_binary(&range?) + to_json_binary(&range?) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let storage_version: ContractVersion = get_contract_version(deps.storage)?; + + // Only migrate if newer + if storage_version.version.as_str() < CONTRACT_VERSION { + // 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 { + match msg.id { + INSTANTIATE_NFT_CONTRACT_REPLY_ID => { + let res = parse_reply_instantiate_data(msg); + match res { + Ok(res) => { + let dao = DAO.load(deps.storage)?; + let nft_contract = res.contract_address; + + // Save NFT contract to config + let mut config = CONFIG.load(deps.storage)?; + config.nft_address = deps.api.addr_validate(&nft_contract)?; + CONFIG.save(deps.storage, &config)?; + + let initial_nfts = INITIAL_NFTS.load(deps.storage)?; + + // Add mint submessages + let mut submessages: Vec = initial_nfts + .iter() + .flat_map(|nft| -> Result { + Ok(SubMsg::new(WasmMsg::Execute { + contract_addr: nft_contract.clone(), + funds: vec![], + msg: nft.clone(), + })) + }) + .collect::>(); + + // Clear space + INITIAL_NFTS.remove(deps.storage); + + // The last submessage updates the minter / owner of the NFT contract, + // and triggers a reply. The reply is used for validation after setup. + submessages.push(SubMsg::reply_on_success( + WasmMsg::Execute { + contract_addr: nft_contract.clone(), + msg: to_json_binary( + &cw721_base::msg::ExecuteMsg::::UpdateOwnership( + cw721_base::Action::TransferOwnership { + new_owner: dao.to_string(), + expiry: None, + }, + ), + )?, + funds: vec![], + }, + VALIDATE_SUPPLY_REPLY_ID, + )); + + Ok(Response::default() + .add_attribute("nft_contract", nft_contract) + .add_submessages(submessages)) + } + Err(_) => Err(ContractError::NftInstantiateError {}), + } + } + VALIDATE_SUPPLY_REPLY_ID => { + // Check that NFTs have actually been minted, and that supply is greater than zero + // NOTE: we have to check this in a reply as it is potentially possible + // to include non-mint messages in `initial_nfts`. + // + // Load config for nft contract address + let collection_addr = CONFIG.load(deps.storage)?.nft_address; + + // Query the total supply of the NFT contract + let nft_supply: NumTokensResponse = deps + .querier + .query_wasm_smart(collection_addr.clone(), &Cw721QueryMsg::NumTokens {})?; + + // Check greater than zero + if nft_supply.count == 0 { + return Err(ContractError::NoInitialNfts {}); + } + + // If Active Threshold absolute count is configured, + // check the count is not greater than supply + if let Some(ActiveThreshold::AbsoluteCount { count }) = + ACTIVE_THRESHOLD.may_load(deps.storage)? + { + assert_valid_absolute_count_threshold( + count, + Uint128::new(nft_supply.count.into()), + )?; + } + + // On setup success, have the DAO complete the second part of + // ownership transfer by accepting ownership in a + // ModuleInstantiateCallback. + let callback = to_json_binary(&ModuleInstantiateCallback { + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: collection_addr.to_string(), + msg: to_json_binary( + &&cw721_base::msg::ExecuteMsg::::UpdateOwnership( + cw721_base::Action::AcceptOwnership {}, + ), + )?, + funds: vec![], + })], + })?; + + Ok(Response::new().set_data(callback)) + } + FACTORY_EXECUTE_REPLY_ID => { + // Parse reply data + let res = parse_reply_execute_data(msg)?; + match res.data { + Some(data) => { + let mut config = CONFIG.load(deps.storage)?; + + // Parse info from the callback, this will fail + // if incorrectly formatted. + let info: NftFactoryCallback = from_json(data)?; + + // Validate NFT contract address + let nft_address = deps.api.addr_validate(&info.nft_contract)?; + + // Validate that this is an NFT with a query + deps.querier.query_wasm_smart::( + nft_address.clone(), + &Cw721QueryMsg::NumTokens {}, + )?; + + // Update NFT contract + config.nft_address = nft_address; + CONFIG.save(deps.storage, &config)?; + + // Construct the response + let mut res = Response::new().add_attribute("nft_contract", info.nft_contract); + + // If a callback has been configured, set the module + // instantiate callback data. + if let Some(callback) = info.module_instantiate_callback { + res = res.set_data(to_json_binary(&callback)?); + } + + Ok(res) + } + None => Err(ContractError::NoFactoryCallback {}), + } + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } } diff --git a/contracts/voting/dao-voting-cw721-staked/src/error.rs b/contracts/voting/dao-voting-cw721-staked/src/error.rs index d61728c5a..287c7a509 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/error.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/error.rs @@ -1,4 +1,6 @@ use cosmwasm_std::{Addr, StdError}; +use cw_utils::ParseReplyError; +use dao_voting::threshold::ActiveThresholdError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -6,26 +8,53 @@ pub enum ContractError { #[error(transparent)] Std(#[from] StdError), - #[error("Nothing to claim")] - NothingToClaim {}, + #[error(transparent)] + ActiveThresholdError(#[from] ActiveThresholdError), + + #[error(transparent)] + HookError(#[from] cw_hooks::HookError), + + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + + #[error(transparent)] + UnstakingDurationError(#[from] dao_voting::duration::UnstakingDurationError), + + #[error("Can not stake that which has already been staked")] + AlreadyStaked {}, #[error("Invalid token. Got ({received}), expected ({expected})")] InvalidToken { received: Addr, expected: Addr }, + #[error("Error instantiating NFT contract")] + NftInstantiateError {}, + + #[error("New NFT contract must be instantiated with at least one NFT")] + NoInitialNfts {}, + + #[error("Factory contract did not implment the required NftFactoryCallback interface")] + NoFactoryCallback {}, + + #[error("Nothing to claim")] + NothingToClaim {}, + #[error("Only the owner of this contract my execute this message")] NotOwner {}, #[error("Can not unstake that which you have not staked (unstaking {token_id})")] NotStaked { token_id: String }, - #[error("Can not stake that which has already been staked")] - AlreadyStaked {}, - #[error("Too many outstanding claims. Claim some tokens before unstaking more.")] TooManyClaims {}, - #[error(transparent)] - HookError(#[from] cw_controllers::HookError), + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Factory message must serialize to WasmMsg::Execute")] + UnsupportedFactoryMsg {}, #[error("Can't unstake zero NFTs.")] ZeroUnstake {}, diff --git a/contracts/voting/dao-voting-cw721-staked/src/hooks.rs b/contracts/voting/dao-voting-cw721-staked/src/hooks.rs deleted file mode 100644 index b8ec5f175..000000000 --- a/contracts/voting/dao-voting-cw721-staked/src/hooks.rs +++ /dev/null @@ -1,156 +0,0 @@ -use crate::state::HOOKS; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{to_binary, Addr, StdResult, Storage, SubMsg, WasmMsg}; - -// This is just a helper to properly serialize the above message -#[cw_serde] -pub enum StakeChangedHookMsg { - Stake { addr: Addr, token_id: String }, - Unstake { addr: Addr, token_ids: Vec }, -} - -pub fn stake_hook_msgs( - storage: &dyn Storage, - addr: Addr, - token_id: String, -) -> StdResult> { - let msg = to_binary(&StakeChangedExecuteMsg::StakeChangeHook( - StakeChangedHookMsg::Stake { addr, token_id }, - ))?; - HOOKS.prepare_hooks(storage, |a| { - let execute = WasmMsg::Execute { - contract_addr: a.into_string(), - msg: msg.clone(), - funds: vec![], - }; - Ok(SubMsg::new(execute)) - }) -} - -pub fn unstake_hook_msgs( - storage: &dyn Storage, - addr: Addr, - token_ids: Vec, -) -> StdResult> { - let msg = to_binary(&StakeChangedExecuteMsg::StakeChangeHook( - StakeChangedHookMsg::Unstake { addr, token_ids }, - ))?; - - HOOKS.prepare_hooks(storage, |a| { - let execute = WasmMsg::Execute { - contract_addr: a.into_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), -} - -#[cfg(test)] -mod tests { - use crate::{ - contract::execute, - state::{Config, CONFIG}, - }; - - use super::*; - - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - - #[test] - fn test_hooks() { - let mut deps = mock_dependencies(); - - let messages = stake_hook_msgs( - &deps.storage, - Addr::unchecked("ekez"), - "ekez-token".to_string(), - ) - .unwrap(); - assert_eq!(messages.len(), 0); - - let messages = unstake_hook_msgs( - &deps.storage, - Addr::unchecked("ekez"), - vec!["ekez-token".to_string()], - ) - .unwrap(); - assert_eq!(messages.len(), 0); - - // Save a config for the execute messages we're testing. - CONFIG - .save( - deps.as_mut().storage, - &Config { - owner: Some(Addr::unchecked("ekez")), - nft_address: Addr::unchecked("ekez-token"), - unstaking_duration: None, - }, - ) - .unwrap(); - - let env = mock_env(); - let info = mock_info("ekez", &[]); - - execute( - deps.as_mut(), - env, - info, - crate::msg::ExecuteMsg::AddHook { - addr: "ekez".to_string(), - }, - ) - .unwrap(); - - let messages = stake_hook_msgs( - &deps.storage, - Addr::unchecked("ekez"), - "ekez-token".to_string(), - ) - .unwrap(); - assert_eq!(messages.len(), 1); - - let messages = unstake_hook_msgs( - &deps.storage, - Addr::unchecked("ekez"), - vec!["ekez-token".to_string()], - ) - .unwrap(); - assert_eq!(messages.len(), 1); - - let env = mock_env(); - let info = mock_info("ekez", &[]); - - execute( - deps.as_mut(), - env, - info, - crate::msg::ExecuteMsg::RemoveHook { - addr: "ekez".to_string(), - }, - ) - .unwrap(); - - let messages = stake_hook_msgs( - &deps.storage, - Addr::unchecked("ekez"), - "ekez-token".to_string(), - ) - .unwrap(); - assert_eq!(messages.len(), 0); - - let messages = unstake_hook_msgs( - &deps.storage, - Addr::unchecked("ekez"), - vec!["ekez-token".to_string()], - ) - .unwrap(); - assert_eq!(messages.len(), 0); - } -} diff --git a/contracts/voting/dao-voting-cw721-staked/src/lib.rs b/contracts/voting/dao-voting-cw721-staked/src/lib.rs index 51ae5c619..d4a73c5be 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/lib.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/lib.rs @@ -2,7 +2,6 @@ pub mod contract; mod error; -pub mod hooks; pub mod msg; pub mod state; diff --git a/contracts/voting/dao-voting-cw721-staked/src/msg.rs b/contracts/voting/dao-voting-cw721-staked/src/msg.rs index bf6012e5c..837851ed3 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/msg.rs @@ -1,18 +1,47 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Binary; use cw721::Cw721ReceiveMsg; 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] +#[allow(clippy::large_enum_variant)] +pub enum NftContract { + /// Uses an existing cw721 or sg721 token contract. + Existing { + /// Address of an already instantiated cw721 or sg721 token contract. + address: String, + }, + /// Creates a new NFT collection used for staking and governance. + New { + /// Code ID for cw721 token contract. + code_id: u64, + /// Label to use for instantiated cw721 contract. + label: String, + msg: Binary, + /// Initial NFTs to mint when creating the NFT contract. + /// If empty, an error is thrown. The binary should be a + /// valid mint message for the corresponding cw721 contract. + initial_nfts: Vec, + }, + /// Uses a factory contract that must return the address of the NFT contract. + /// The binary must serialize to a `WasmMsg::Execute` message. + /// Validation happens in the factory contract itself, so be sure to use a + /// trusted factory contract. + Factory(Binary), +} #[cw_serde] pub struct InstantiateMsg { - /// May change unstaking duration and add hooks. - pub owner: Option, /// Address of the cw721 NFT contract that may be staked. - pub nft_address: String, + pub nft_contract: NftContract, /// Amount of time between unstaking and tokens being /// avaliable. To unstake with no delay, leave as `None`. 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] @@ -24,22 +53,26 @@ pub enum ExecuteMsg { /// Unstakes the specified token_ids on behalf of the /// sender. token_ids must have unique values and have non-zero /// length. - Unstake { - token_ids: Vec, - }, + Unstake { token_ids: Vec }, + /// Claim NFTs that have been unstaked for the specified duration. ClaimNfts {}, - UpdateConfig { - owner: Option, - duration: Option, - }, - AddHook { - addr: String, - }, - RemoveHook { - addr: String, + /// Updates the contract configuration, namely unstaking duration. + /// Only callable by the DAO that initialized this voting contract. + UpdateConfig { duration: Option }, + /// Adds a hook which is called on staking / unstaking events. + /// Only callable by the DAO that initialized this voting contract. + AddHook { addr: String }, + /// Removes a hook which is called on staking / unstaking events. + /// Only callable by the DAO that initialized this voting contract. + RemoveHook { addr: String }, + /// Sets the active threshold to a new value. + /// Only callable by the DAO that initialized this voting contract. + UpdateActiveThreshold { + new_threshold: Option, }, } +#[active_query] #[voting_module_query] #[cw_serde] #[derive(QueryResponses)] @@ -57,4 +90,9 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + #[returns(ActiveThresholdResponse)] + ActiveThreshold {}, } + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/contracts/voting/dao-voting-cw721-staked/src/state.rs b/contracts/voting/dao-voting-cw721-staked/src/state.rs index f2a932bcb..0d8e8b62f 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/state.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/state.rs @@ -1,22 +1,26 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Empty, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{Addr, Binary, Empty, StdError, StdResult, Storage, Uint128}; use cw721_controllers::NftClaims; -use cw_controllers::Hooks; +use cw_hooks::Hooks; use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy}; use cw_utils::Duration; +use dao_voting::threshold::ActiveThreshold; use crate::ContractError; #[cw_serde] pub struct Config { - pub owner: Option, pub nft_address: Addr, pub unstaking_duration: Option, } +pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); pub const CONFIG: Item = Item::new("config"); pub const DAO: Item = Item::new("dao"); +// Holds initial NFTs messages during instantiation. +pub const INITIAL_NFTS: Item> = Item::new("initial_nfts"); + /// The set of NFTs currently staked by each address. The existence of /// an `(address, token_id)` pair implies that `address` has staked /// `token_id`. diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/adversarial.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/adversarial.rs index f8479c1c3..e5c587564 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/adversarial.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/adversarial.rs @@ -27,7 +27,7 @@ fn test_circular_stake() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(None, None); + } = setup_test(None); mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1")?; mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "2")?; @@ -72,7 +72,7 @@ fn test_immediate_unstake() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(None, None); + } = setup_test(None); mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1")?; mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "2")?; @@ -94,7 +94,7 @@ fn test_immediate_unstake() -> anyhow::Result<()> { fn test_stake_wrong_nft() -> anyhow::Result<()> { let CommonTest { mut app, module, .. - } = setup_test(None, None); + } = setup_test(None); let other_nft = instantiate_cw721_base(&mut app, CREATOR_ADDR, CREATOR_ADDR); let res = mint_and_stake_nft(&mut app, &other_nft, &module, CREATOR_ADDR, "1"); @@ -115,7 +115,7 @@ fn test_query_the_future() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(None, None); + } = setup_test(None); mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1")?; @@ -154,7 +154,7 @@ fn test_bypass_max_claims() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(None, Some(Duration::Height(1))); + } = setup_test(Some(Duration::Height(1))); let mut to_stake = vec![]; for i in 1..(MAX_CLAIMS + 10) { let i_str = &i.to_string(); diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/execute.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/execute.rs index d4dbf388c..dd5b60877 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/execute.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/execute.rs @@ -1,6 +1,5 @@ use cosmwasm_std::{Addr, Binary, Empty}; use cw721::Cw721ExecuteMsg; -use cw721_base::MintMsg; use cw_multi_test::{App, AppResponse, Executor}; use anyhow::Result as AnyResult; @@ -45,12 +44,12 @@ pub fn mint_nft( app.execute_contract( addr!(sender), cw721.clone(), - &cw721_base::ExecuteMsg::Mint::(MintMsg { + &cw721_base::ExecuteMsg::Mint:: { token_id: token_id.to_string(), owner: receiver.to_string(), token_uri: None, extension: Empty::default(), - }), + }, &[], ) } @@ -97,16 +96,12 @@ pub fn update_config( app: &mut App, module: &Addr, sender: &str, - owner: Option<&str>, duration: Option, ) -> AnyResult { app.execute_contract( addr!(sender), module.clone(), - &ExecuteMsg::UpdateConfig { - owner: owner.map(str::to_string), - duration, - }, + &ExecuteMsg::UpdateConfig { duration }, &[], ) } diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/hooks.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/hooks.rs new file mode 100644 index 000000000..4bcac6fbc --- /dev/null +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/hooks.rs @@ -0,0 +1,110 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + Addr, +}; +use dao_hooks::nft_stake::{stake_nft_hook_msgs, unstake_nft_hook_msgs}; + +use crate::{ + contract::execute, + state::{Config, CONFIG, DAO, HOOKS}, +}; + +#[test] +fn test_hooks() { + let mut deps = mock_dependencies(); + + let messages = stake_nft_hook_msgs( + HOOKS, + &deps.storage, + Addr::unchecked("ekez"), + "ekez-token".to_string(), + ) + .unwrap(); + assert_eq!(messages.len(), 0); + + let messages = unstake_nft_hook_msgs( + HOOKS, + &deps.storage, + Addr::unchecked("ekez"), + vec!["ekez-token".to_string()], + ) + .unwrap(); + assert_eq!(messages.len(), 0); + + // Save a DAO address for the execute messages we're testing. + DAO.save(deps.as_mut().storage, &Addr::unchecked("ekez")) + .unwrap(); + + // Save a config for the execute messages we're testing. + CONFIG + .save( + deps.as_mut().storage, + &Config { + nft_address: Addr::unchecked("ekez-token"), + unstaking_duration: None, + }, + ) + .unwrap(); + + let env = mock_env(); + let info = mock_info("ekez", &[]); + + execute( + deps.as_mut(), + env, + info, + crate::msg::ExecuteMsg::AddHook { + addr: "ekez".to_string(), + }, + ) + .unwrap(); + + let messages = stake_nft_hook_msgs( + HOOKS, + &deps.storage, + Addr::unchecked("ekez"), + "ekez-token".to_string(), + ) + .unwrap(); + assert_eq!(messages.len(), 1); + + let messages = unstake_nft_hook_msgs( + HOOKS, + &deps.storage, + Addr::unchecked("ekez"), + vec!["ekez-token".to_string()], + ) + .unwrap(); + assert_eq!(messages.len(), 1); + + let env = mock_env(); + let info = mock_info("ekez", &[]); + + execute( + deps.as_mut(), + env, + info, + crate::msg::ExecuteMsg::RemoveHook { + addr: "ekez".to_string(), + }, + ) + .unwrap(); + + let messages = stake_nft_hook_msgs( + HOOKS, + &deps.storage, + Addr::unchecked("ekez"), + "ekez-token".to_string(), + ) + .unwrap(); + assert_eq!(messages.len(), 0); + + let messages = unstake_nft_hook_msgs( + HOOKS, + &deps.storage, + Addr::unchecked("ekez"), + vec!["ekez-token".to_string()], + ) + .unwrap(); + assert_eq!(messages.len(), 0); +} diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs new file mode 100644 index 000000000..aedf0ca32 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs @@ -0,0 +1,160 @@ +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Empty, Uint128, WasmMsg}; +use cw721_base::{ + msg::{ + ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg, + QueryMsg as Cw721QueryMsg, + }, + MinterResponse, +}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo}, +}; +use dao_testing::test_tube::{cw721_base::Cw721Base, dao_dao_core::DaoCore}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, +}; +use osmosis_test_tube::{Account, OsmosisTestApp, RunnerError}; + +use crate::{ + msg::{InstantiateMsg, NftContract, QueryMsg}, + state::Config, + testing::test_tube_env::Cw721VotingContract, +}; + +use super::test_tube_env::{TestEnv, TestEnvBuilder}; + +#[test] +fn test_full_integration_with_factory() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + + // Setup defaults to creating a NFT DAO with the factory contract + // This does not use funds when instantiating the NFT contract. + // We will test that below. + let TestEnv { + vp_contract, + proposal_single, + custom_factory, + accounts, + cw721, + .. + } = env.setup(&app); + + // Test instantiating a DAO with a factory contract that requires funds + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_json_binary(&InstantiateMsg { + nft_contract: NftContract::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: custom_factory.contract_addr.clone(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWithFunds { + code_id: cw721.code_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: accounts[0].address(), + }, + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::< + Empty, + Empty, + >::Mint { + owner: accounts[0].address(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + ) + .unwrap(), + funds: vec![Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.code_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiating without funds fails + let err = DaoCore::new(&app, &msg, &accounts[0], &[]).unwrap_err(); + + // Error is insufficient funds as no funds were sent + assert_eq!( + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: 0uosmo is smaller than 1000uosmo: insufficient funds".to_string() + }, + err + ); + + // Instantiate DAO succeeds with funds + let dao = DaoCore::new( + &app, + &msg, + &accounts[0], + &[Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + ) + .unwrap(); + + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + Cw721VotingContract::new_with_values(&app, vp_contract.code_id, vp_addr.to_string()) + .unwrap(); + + let vp_config: Config = vp_contract.query(&QueryMsg::Config {}).unwrap(); + let cw721_contract = + Cw721Base::new_with_values(&app, cw721.code_id, vp_config.nft_address.to_string()).unwrap(); + + // Check DAO was initialized to minter + let minter: MinterResponse = cw721_contract.query(&Cw721QueryMsg::Minter {}).unwrap(); + assert_eq!(minter.minter, Some(dao.contract_addr.to_string())); +} diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs index 68fb1a9c0..de0824f52 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs @@ -1,17 +1,26 @@ mod adversarial; mod execute; +mod hooks; mod instantiate; mod queries; mod tests; +// 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 integration_tests; +#[cfg(test)] +#[cfg(feature = "test-tube")] +mod test_tube_env; + use cosmwasm_std::Addr; use cw_multi_test::{App, Executor}; use cw_utils::Duration; - -use dao_interface::state::Admin; use dao_testing::contracts::voting_cw721_staked_contract; -use crate::msg::InstantiateMsg; +use crate::msg::{InstantiateMsg, NftContract}; use self::instantiate::instantiate_cw721_base; @@ -24,7 +33,7 @@ pub(crate) struct CommonTest { nft: Addr, } -pub(crate) fn setup_test(owner: Option, unstaking_duration: Option) -> CommonTest { +pub(crate) fn setup_test(unstaking_duration: Option) -> CommonTest { let mut app = App::default(); let module_id = app.store_code(voting_cw721_staked_contract()); @@ -34,9 +43,11 @@ pub(crate) fn setup_test(owner: Option, unstaking_duration: Option { + pub app: &'a OsmosisTestApp, + pub dao: DaoCore<'a>, + pub proposal_single: DaoProposalSingle<'a>, + pub custom_factory: CustomFactoryContract<'a>, + pub vp_contract: Cw721VotingContract<'a>, + pub accounts: Vec, + pub cw721: Cw721Base<'a>, +} + +impl<'a> TestEnv<'a> { + pub fn bank(&self) -> Bank<'_, OsmosisTestApp> { + Bank::new(self.app) + } +} + +pub struct TestEnvBuilder {} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self {} + } + + // Full DAO setup + pub fn setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + // Upload all needed code ids + let vp_contract_id = Cw721VotingContract::upload(app, &accounts[0]).unwrap(); + let proposal_single_id = DaoProposalSingle::upload(app, &accounts[0]).unwrap(); + let cw721_id = Cw721Base::upload(app, &accounts[0]).unwrap(); + + // Instantiate Custom Factory + let custom_factory = CustomFactoryContract::new( + app, + &dao_test_custom_factory::msg::InstantiateMsg {}, + &accounts[0], + ) + .unwrap(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract_id, + msg: to_json_binary(&InstantiateMsg { + nft_contract: NftContract::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: custom_factory.contract_addr.clone(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactory { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: accounts[0].address(), + }, + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::< + Empty, + Empty, + >::Mint { + owner: accounts[0].address(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO + let dao = DaoCore::new(app, &msg, &accounts[0], &[]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + Cw721VotingContract::new_with_values(app, vp_contract_id, vp_addr.to_string()).unwrap(); + + let vp_config: Config = vp_contract.query(&QueryMsg::Config {}).unwrap(); + + let cw721 = + Cw721Base::new_with_values(app, cw721_id, vp_config.nft_address.to_string()).unwrap(); + + // Get proposal module address, setup proposal_single helper + let proposal_modules: Vec = dao + .query(&DaoQueryMsg::ProposalModules { + limit: None, + start_after: None, + }) + .unwrap(); + let proposal_single = DaoProposalSingle::new_with_values( + app, + proposal_single_id, + proposal_modules[0].address.to_string(), + ) + .unwrap(); + + TestEnv { + app, + dao, + vp_contract, + proposal_single, + custom_factory, + accounts, + cw721, + } + } +} + +#[derive(Debug)] +pub struct Cw721VotingContract<'a> { + pub app: &'a OsmosisTestApp, + pub contract_addr: String, + pub code_id: u64, +} + +impl<'a> Cw721VotingContract<'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 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) + } + + 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) + } + + 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_cw721_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_cw721_staked-aarch64.wasm"), + ) + .unwrap(), + } + } +} 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 2ca2debb8..cdc057bb7 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs @@ -1,11 +1,19 @@ -use cosmwasm_std::Uint128; +use cosmwasm_std::testing::{mock_dependencies, mock_env}; +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Empty, Uint128, WasmMsg}; +use cw721_base::msg::{ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg}; use cw721_controllers::{NftClaim, NftClaimsResponse}; -use cw_multi_test::next_block; +use cw_multi_test::{next_block, App, BankSudo, Executor, SudoMsg}; use cw_utils::Duration; -use dao_interface::state::Admin; +use dao_interface::voting::IsActiveResponse; +use dao_testing::contracts::{ + cw721_base_contract, dao_test_custom_factory, voting_cw721_staked_contract, +}; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; use crate::{ - state::{Config, MAX_CLAIMS}, + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg}, + state::MAX_CLAIMS, testing::{ execute::{ claim_nfts, mint_and_stake_nft, mint_nft, stake_nft, unstake_nfts, update_config, @@ -14,6 +22,7 @@ use crate::{ }, }; +use super::instantiate::instantiate_cw721_base; use super::{ execute::{add_hook, remove_hook}, is_error, @@ -21,6 +30,52 @@ use super::{ setup_test, CommonTest, CREATOR_ADDR, }; +// I can create new NFT collection when creating a dao-voting-cw721-staked contract +#[test] +fn test_instantiate_with_new_cw721_collection() -> anyhow::Result<()> { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + let module_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + })?, + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + })?], + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + let config = query_config(&app, &module_addr)?; + let cw721_addr = config.nft_address; + + // Check that the NFT contract was created + let owner = query_nft_owner(&app, &cw721_addr, "1")?; + assert_eq!(owner.owner, CREATOR_ADDR); + + Ok(()) +} + // I can stake tokens, voting power and total power is updated one // block later. #[test] @@ -29,7 +84,7 @@ fn test_stake_tokens() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(None, None); + } = setup_test(None); let total_power = query_total_power(&app, &module, None)?; let voting_power = query_voting_power(&app, &module, CREATOR_ADDR, None)?; @@ -66,7 +121,7 @@ fn test_unstake_tokens_no_claims() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(None, None); + } = setup_test(None); let friend = "friend"; @@ -124,7 +179,7 @@ fn test_update_config() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(Some(Admin::CoreModule {}), Some(Duration::Height(3))); + } = setup_test(Some(Duration::Height(3))); mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1")?; mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "2")?; @@ -142,14 +197,15 @@ fn test_update_config() -> anyhow::Result<()> { } ); - // Make friend the new owner. - update_config( - &mut app, - &module, - CREATOR_ADDR, - Some("friend"), - Some(Duration::Time(1)), - )?; + // Update config to invalid duration fails + let err = update_config(&mut app, &module, CREATOR_ADDR, Some(Duration::Time(0))).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Invalid unstaking duration, unstaking duration cannot be 0".to_string() + ); + + // Update duration + update_config(&mut app, &module, CREATOR_ADDR, Some(Duration::Time(1)))?; // Existing claims should remain unchanged. let claims = query_claims(&app, &module, CREATOR_ADDR)?; @@ -184,7 +240,7 @@ fn test_update_config() -> anyhow::Result<()> { ); let info = app.block_info(); - app.update_block(|mut block| { + app.update_block(|block| { block.height += 3; block.time = match Duration::Time(1).after(&info) { cw_utils::Expiration::AtTime(timestamp) => timestamp, @@ -197,39 +253,6 @@ fn test_update_config() -> anyhow::Result<()> { let claims = query_claims(&app, &module, CREATOR_ADDR)?; assert_eq!(claims, NftClaimsResponse { nft_claims: vec![] }); - // Creator can no longer do config updates. - let res = update_config( - &mut app, - &module, - CREATOR_ADDR, - Some("friend"), - Some(Duration::Time(1)), - ); - is_error!(res => "Only the owner of this contract my execute this message"); - - // Friend can still do config updates, and even remove themselves - // as the owner. - update_config(&mut app, &module, "friend", None, None)?; - let config = query_config(&app, &module)?; - assert_eq!( - config, - Config { - owner: None, - nft_address: nft, - unstaking_duration: None - } - ); - - // Friend has removed themselves. - let res = update_config( - &mut app, - &module, - "friend", - Some("friend"), - Some(Duration::Time(1)), - ); - is_error!(res => "Only the owner of this contract my execute this message"); - Ok(()) } @@ -242,7 +265,7 @@ fn test_claims() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(Some(Admin::CoreModule {}), Some(Duration::Height(1))); + } = setup_test(Some(Duration::Height(1))); mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1")?; mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "2")?; @@ -285,7 +308,7 @@ fn test_max_claims() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(None, Some(Duration::Height(1))); + } = setup_test(Some(Duration::Height(1))); for i in 0..MAX_CLAIMS { let i_str = &i.to_string(); @@ -307,7 +330,7 @@ fn test_list_staked_nfts() -> anyhow::Result<()> { mut app, module, nft, - } = setup_test(Some(Admin::CoreModule {}), Some(Duration::Height(1))); + } = setup_test(Some(Duration::Height(1))); mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1")?; mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "2")?; @@ -352,7 +375,7 @@ fn test_list_staked_nfts() -> anyhow::Result<()> { #[test] fn test_info_query_works() -> anyhow::Result<()> { - let CommonTest { app, module, .. } = setup_test(None, None); + let CommonTest { app, module, .. } = setup_test(None); let info = query_info(&app, &module)?; assert_eq!(info.info.version, env!("CARGO_PKG_VERSION").to_string()); Ok(()) @@ -362,22 +385,27 @@ fn test_info_query_works() -> anyhow::Result<()> { #[test] fn test_add_remove_hooks() -> anyhow::Result<()> { let CommonTest { - mut app, module, .. - } = setup_test( - Some(Admin::Address { - addr: CREATOR_ADDR.to_string(), - }), - None, - ); + mut app, + module, + nft, + } = setup_test(None); add_hook(&mut app, &module, CREATOR_ADDR, "meow")?; remove_hook(&mut app, &module, CREATOR_ADDR, "meow")?; + // Minting NFT works if no hooks + mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1").unwrap(); + + // Add a hook to a fake contract called "meow" add_hook(&mut app, &module, CREATOR_ADDR, "meow")?; let hooks = query_hooks(&app, &module)?; assert_eq!(hooks.hooks, vec!["meow".to_string()]); + // Minting / staking now doesn't work because meow isn't a contract + // This failure means the hook is working + mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1").unwrap_err(); + let res = add_hook(&mut app, &module, CREATOR_ADDR, "meow"); is_error!(res => "Given address already registered as a hook"); @@ -385,7 +413,1043 @@ fn test_add_remove_hooks() -> anyhow::Result<()> { is_error!(res => "Given address not registered as a hook"); let res = add_hook(&mut app, &module, "ekez", "evil"); - is_error!(res => "Only the owner of this contract my execute this message"); + is_error!(res => "Unauthorized"); Ok(()) } + +#[test] +fn test_instantiate_with_invalid_duration_fails() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_json_binary( + &Cw721ExecuteMsg::::Extension { msg: Empty {} }, + ) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "New NFT contract must be instantiated with at least one NFT".to_string() + ); +} + +#[test] +#[should_panic(expected = "Active threshold count must be greater than zero")] +fn test_instantiate_zero_active_threshold_count() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::zero(), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] +fn test_instantiate_invalid_active_threshold_count_new_nft() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] +fn test_instantiate_invalid_active_threshold_count_existing_nft() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_addr = instantiate_cw721_base(&mut app, CREATOR_ADDR, CREATOR_ADDR); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Existing { + address: cw721_addr.to_string(), + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +fn test_active_threshold_absolute_count() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let voting_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![ + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap(), + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "2".to_string(), + extension: Empty {}, + }) + .unwrap(), + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "3".to_string(), + extension: Empty {}, + }) + .unwrap(), + ], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(3), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + // Get NFT contract address + let nft_addr = query_config(&app, &voting_addr).unwrap().nft_address; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake NFTs + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "1").unwrap(); + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "2").unwrap(); + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "3").unwrap(); + + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let voting_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(20), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + // Get NFT contract address + let nft_addr = query_config(&app, &voting_addr).unwrap().nft_address; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake NFTs + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "1").unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent_rounds_up() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let voting_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![ + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap(), + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "2".to_string(), + extension: Empty {}, + }) + .unwrap(), + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "3".to_string(), + extension: Empty {}, + }) + .unwrap(), + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "4".to_string(), + extension: Empty {}, + }) + .unwrap(), + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "5".to_string(), + extension: Empty {}, + }) + .unwrap(), + ], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + // Get NFT contract address + let nft_addr = query_config(&app, &voting_addr).unwrap().nft_address; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 2 token as creator, should not be active. + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "1").unwrap(); + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "2").unwrap(); + + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 1 more token as creator, should now be active. + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "3").unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_update_active_threshold() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let voting_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!(resp.active_threshold, None); + + let msg = ExecuteMsg::UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), + }; + + // Expect failure as sender is not the DAO + app.execute_contract(Addr::unchecked("bob"), voting_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Expect success as sender is the DAO (in this case the creator) + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + voting_addr.clone(), + &msg, + &[], + ) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!( + resp.active_threshold, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1) + }) + ); +} + +#[test] +#[should_panic( + expected = "Active threshold percentage must be greater than 0 and not greater than 1" +)] +fn test_active_threshold_percentage_gt_100() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(120), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic( + expected = "Active threshold percentage must be greater than 0 and not greater than 1" +)] +fn test_active_threshold_percentage_lte_0() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(0), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +fn test_invalid_instantiate_msg() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Empty {}).unwrap(), + initial_nfts: vec![to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Error instantiating NFT contract".to_string() + ); +} + +#[test] +fn test_invalid_initial_nft_msg() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_json_binary( + &Cw721ExecuteMsg::::Extension { msg: Empty {} }, + ) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "New NFT contract must be instantiated with at least one NFT".to_string() + ); +} + +#[test] +fn test_invalid_initial_nft_msg_wrong_absolute_count() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![ + to_json_binary(&Cw721ExecuteMsg::::Extension { + msg: Empty {}, + }) + .unwrap(), + to_json_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap(), + ], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(2), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Absolute count threshold cannot be greater than the total token supply".to_string() + ); +} + +#[test] +fn test_no_initial_nfts_fails() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_json_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "New NFT contract must be instantiated with at least one NFT".to_string() + ); +} + +#[test] +fn test_factory() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + let factory_id = app.store_code(dao_test_custom_factory()); + + // Instantiate factory + let factory_addr = app + .instantiate_contract( + factory_id, + Addr::unchecked(CREATOR_ADDR), + &dao_test_custom_factory::msg::InstantiateMsg {}, + &[], + "test factory".to_string(), + None, + ) + .unwrap(); + + // Instantiate using factory succeeds + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_json_binary(&dao_test_custom_factory::msg::ExecuteMsg::NftFactory { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }, + initial_nfts: vec![], + }) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +fn test_factory_with_funds_pass_through() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + let factory_id = app.store_code(dao_test_custom_factory()); + + // Mint some tokens to creator + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: CREATOR_ADDR.to_string(), + amount: vec![Coin { + denom: "ujuno".to_string(), + amount: Uint128::new(10000), + }], + })) + .unwrap(); + + // Instantiate factory + let factory_addr = app + .instantiate_contract( + factory_id, + Addr::unchecked(CREATOR_ADDR), + &dao_test_custom_factory::msg::InstantiateMsg {}, + &[], + "test factory".to_string(), + None, + ) + .unwrap(); + + // Instantiate without funds fails + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWithFunds { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }, + initial_nfts: vec![to_json_binary( + &Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }, + ) + .unwrap()], + }, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + + // Instantiate using factory succeeds + let funds = vec![Coin { + denom: "ujuno".to_string(), + amount: Uint128::new(100), + }]; + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWithFunds { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }, + initial_nfts: vec![to_json_binary( + &Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }, + ) + .unwrap()], + }, + ) + .unwrap(), + funds: funds.clone(), + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &funds, + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Factory message must serialize to WasmMsg::Execute")] +fn test_unsupported_factory_msg() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + // Instantiate using factory succeeds + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_json_binary(&WasmMsg::Instantiate { + code_id: cw721_id, + msg: to_json_binary(&dao_test_custom_factory::msg::ExecuteMsg::NftFactory { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }, + initial_nfts: vec![], + }) + .unwrap(), + admin: None, + label: "Test NFT".to_string(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic( + expected = "Error parsing into type dao_interface::nft::NftFactoryCallback: unknown field `denom`, expected `nft_contract`" +)] +fn test_factory_wrong_callback() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let _cw721_id = app.store_code(cw721_base_contract()); + let factory_id = app.store_code(dao_test_custom_factory()); + + // Instantiate factory + let factory_addr = app + .instantiate_contract( + factory_id, + Addr::unchecked(CREATOR_ADDR), + &dao_test_custom_factory::msg::InstantiateMsg {}, + &[], + "test factory".to_string(), + None, + ) + .unwrap(); + + // Instantiate using factory succeeds + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWrongCallback {}, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Invalid reply from sub-message: Missing reply data")] +fn test_factory_no_callback() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let _cw721_id = app.store_code(cw721_base_contract()); + let factory_id = app.store_code(dao_test_custom_factory()); + + // Instantiate factory + let factory_addr = app + .instantiate_contract( + factory_id, + Addr::unchecked(CREATOR_ADDR), + &dao_test_custom_factory::msg::InstantiateMsg {}, + &[], + "test factory".to_string(), + None, + ) + .unwrap(); + + // Instantiate using factory succeeds + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryNoCallback {}, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +pub fn test_migrate_update_version() { + let mut deps = mock_dependencies(); + cw2::set_contract_version(&mut deps.storage, "my-contract", "1.0.0").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-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/README.md b/contracts/voting/dao-voting-native-staked/README.md deleted file mode 100644 index eacb5d9e8..000000000 --- a/contracts/voting/dao-voting-native-staked/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# CW Native Staked Balance Voting - -Simple native token voting 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). - diff --git a/contracts/voting/dao-voting-native-staked/src/contract.rs b/contracts/voting/dao-voting-native-staked/src/contract.rs deleted file mode 100644 index eb93fa0fe..000000000 --- a/contracts/voting/dao-voting-native-staked/src/contract.rs +++ /dev/null @@ -1,368 +0,0 @@ -#[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, -}; -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 crate::error::ContractError; -use crate::msg::{ - ExecuteMsg, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, -}; -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"); - -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 { - 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, - 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()), - )) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - 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::Claim {} => execute_claim(deps, env, info), - } -} - -pub fn execute_stake( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - let config = CONFIG.load(deps.storage)?; - let amount = must_pay(&info, &config.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)?) }, - )?; - - Ok(Response::new() - .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 { - 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 config = CONFIG.load(deps.storage)?; - match config.unstaking_duration { - None => { - let msg = CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: coins(amount.u128(), config.denom), - }); - Ok(Response::new() - .add_message(msg) - .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_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, - 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 { - 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()), - )) -} - -pub fn execute_claim( - deps: DepsMut, - env: Env, - info: MessageInfo, -) -> Result { - let release = CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, None)?; - if release.is_zero() { - return Err(ContractError::NothingToClaim {}); - } - - let config = CONFIG.load(deps.storage)?; - let msg = CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: coins(release.u128(), config.denom), - }); - - Ok(Response::new() - .add_message(msg) - .add_attribute("action", "claim") - .add_attribute("from", info.sender) - .add_attribute("amount", release)) -} - -#[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::ListStakers { start_after, limit } => { - query_list_stakers(deps, start_after, limit) - } - } -} - -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 start_at = start_after - .map(|addr| deps.api.addr_validate(&addr)) - .transpose()?; - - let stakers = cw_paginate_storage::paginate_snapshot_map( - deps, - &STAKED_BALANCES, - start_at.as_ref(), - limit, - cosmwasm_std::Order::Ascending, - )?; - - let stakers = stakers - .into_iter() - .map(|(address, balance)| StakerBalanceResponse { - address: address.into_string(), - balance, - }) - .collect(); - - to_binary(&ListStakersResponse { stakers }) -} - -#[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::default()) -} diff --git a/contracts/voting/dao-voting-native-staked/src/error.rs b/contracts/voting/dao-voting-native-staked/src/error.rs deleted file mode 100644 index dc8cf9f83..000000000 --- a/contracts/voting/dao-voting-native-staked/src/error.rs +++ /dev/null @@ -1,33 +0,0 @@ -use cosmwasm_std::StdError; -use cw_utils::PaymentError; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("{0}")] - PaymentError(#[from] PaymentError), - - #[error("Unauthorized")] - Unauthorized {}, - - #[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("Only owner can change owner")] - OnlyOwnerCanChangeOwner {}, - - #[error("Can only unstake less than or equal to the amount you have staked")] - InvalidUnstakeAmount {}, - - #[error("Amount being unstaked must be non-zero")] - ZeroUnstake {}, -} diff --git a/contracts/voting/dao-voting-native-staked/src/msg.rs b/contracts/voting/dao-voting-native-staked/src/msg.rs deleted file mode 100644 index bf836ca12..000000000 --- a/contracts/voting/dao-voting-native-staked/src/msg.rs +++ /dev/null @@ -1,60 +0,0 @@ -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; - -#[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 - pub denom: String, - // How long until the tokens become liquid again - pub unstaking_duration: Option, -} - -#[cw_serde] -pub enum ExecuteMsg { - Stake {}, - Unstake { - amount: Uint128, - }, - UpdateConfig { - owner: Option, - manager: Option, - duration: Option, - }, - Claim {}, -} - -#[voting_module_query] -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - #[returns(crate::state::Config)] - GetConfig {}, - #[returns(cw_controllers::ClaimsResponse)] - Claims { address: String }, - #[returns(ListStakersResponse)] - ListStakers { - start_after: Option, - limit: Option, - }, -} - -#[cw_serde] -pub struct MigrateMsg {} - -#[cw_serde] -pub struct ListStakersResponse { - pub stakers: Vec, -} - -#[cw_serde] -pub struct StakerBalanceResponse { - pub address: String, - pub balance: Uint128, -} diff --git a/contracts/voting/dao-voting-native-staked/src/state.rs b/contracts/voting/dao-voting-native-staked/src/state.rs deleted file mode 100644 index 65849e89e..000000000 --- a/contracts/voting/dao-voting-native-staked/src/state.rs +++ /dev/null @@ -1,34 +0,0 @@ -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128}; -use cw_controllers::Claims; -use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy}; -use cw_utils::Duration; - -#[cw_serde] -pub struct Config { - pub owner: Option, - pub manager: Option, - pub denom: String, - pub unstaking_duration: Option, -} - -pub const CONFIG: Item = Item::new("config"); -pub const DAO: Item = Item::new("dao"); -pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( - "staked_balances", - "staked_balance__checkpoints", - "staked_balance__changelog", - Strategy::EveryBlock, -); - -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"); diff --git a/contracts/voting/dao-voting-token-staked/.cargo/config b/contracts/voting/dao-voting-token-staked/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/voting/dao-voting-token-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-staked/Cargo.toml b/contracts/voting/dao-voting-token-staked/Cargo.toml new file mode 100644 index 000000000..8df86dfe2 --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "dao-voting-token-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-ownable = { 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-hooks = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } +cw-paginate-storage = { workspace = true } +cw-tokenfactory-issuer = { workspace = true, features = ["library"] } + +[dev-dependencies] +anyhow = { workspace = true } +cw-multi-test = { workspace = true } +cw-tokenfactory-issuer = { workspace = true } +dao-proposal-single = { workspace = true } +dao-proposal-hook-counter = { workspace = true } +dao-test-custom-factory = { workspace = true } +dao-testing = { workspace = true, features = ["test-tube"] } +osmosis-std = { workspace = true } +osmosis-test-tube = { workspace = true } +serde = { workspace = true } diff --git a/contracts/voting/dao-voting-token-staked/README.md b/contracts/voting/dao-voting-token-staked/README.md new file mode 100644 index 000000000..288a6cda8 --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/README.md @@ -0,0 +1,92 @@ +# `dao_voting_token_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). + +### Token Factory support +`dao_voting_token_staked` leverages the `cw_tokenfactory_issuer` contract for tokenfactory functionality. When instantiated, `dao_voting_token_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). + +The `cw_tokenfactory_issuer` contract supports many features, see the [cw_tokenfactory_issuer contract README](../../external/cw-tokenfactory-issuer/README.md) for more information. + +## Instantiation +When instantiating a new `dao_voting_token_staked` contract there are two required fields: +- `token_info`: you have the option to leverage an `existing` native token or creating a `new` one using the Token Factory module. + +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 +- `token_issuer_code_id`: must be set to a valid Code ID for the `cw_tokenfactory_issuer` contract. +- `initial_balances`: the initial distribution of the new token, there must be at least 1 account with a balance so as the DAO is not locked. + +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 treasury. + +Example insantiation mesggage: +``` json +{ + "token_info": { + "new": { + "token_issuer_code_id": , + "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 Native Token +`dao-voting-token-staked` can also be used with existing native tokens. They could be in the form of a native denom like `ion`, an IBC token, or a Token Factory token. + +Example insantiation mesggage: + +``` json +{ + "token_info": { + "existing": { + "denom": "uion", + } + } +} +``` + +NOTE: if using an existing Token Factory token, double check the Token Factory admin and consider changing the Token Factory to be the DAO after the DAO is created. + +### Use a factory +Occassionally, more customization is needed. Maybe you want to have an Augmented Bonding Curve contract or LP pool that requires additional setup? It's possible with factory contracts! + +The `factory` pattern takes a single `WasmMsg::Execute` message that calls into a custom factory contract. + +**NOTE:** when using the factory pattern, it is important to only use a trusted factory contract, as all validation happens in the factory contract. + +Those implementing custom factory contracts MUST handle any validation that is to happen, and the custom `WasmMsg::Execute` message MUST include `TokenFactoryCallback` data respectively. + +The [dao-test-custom-factory contract](../test/dao-test-custom-factory) provides an example of how this can be done and is used for tests. It is NOT production ready, but meant to serve as an example for building factory contracts. diff --git a/contracts/voting/dao-voting-native-staked/examples/schema.rs b/contracts/voting/dao-voting-token-staked/examples/schema.rs similarity index 68% rename from contracts/voting/dao-voting-native-staked/examples/schema.rs rename to contracts/voting/dao-voting-token-staked/examples/schema.rs index f7ad58f28..0bb3d7c39 100644 --- a/contracts/voting/dao-voting-native-staked/examples/schema.rs +++ b/contracts/voting/dao-voting-token-staked/examples/schema.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::write_api; -use dao_voting_native_staked::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use dao_voting_token_staked::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; fn main() { write_api! { diff --git a/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json b/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json similarity index 54% rename from contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json rename to contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json index f553cbbab..a8d2ff482 100644 --- a/contracts/voting/dao-voting-native-staked/schema/dao-voting-native-staked.json +++ b/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json @@ -1,35 +1,36 @@ { - "contract_name": "dao-voting-native-staked", - "contract_version": "2.2.0", + "contract_name": "dao-voting-token-staked", + "contract_version": "2.3.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", "type": "object", "required": [ - "denom" + "token_info" ], "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" } ] }, + "token_info": { + "description": "New or existing native token to use for voting power.", + "allOf": [ + { + "$ref": "#/definitions/TokenInfo" + } + ] + }, "unstaking_duration": { + "description": "How long until the tokens become liquid again", "anyOf": [ { "$ref": "#/definitions/Duration" @@ -42,24 +43,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 +69,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 +92,42 @@ } ] }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "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": [ @@ -116,6 +161,167 @@ "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", + "token_issuer_code_id" + ], + "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" + }, + "token_issuer_code_id": { + "description": "The code id of the cw-tokenfactory-issuer contract", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "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.", + "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 + }, + { + "description": "Uses a factory contract that must return the denom, optionally a Token Contract address. The binary must serialize to a `WasmMsg::Execute` message. Validation happens in the factory contract itself, so be sure to use a trusted factory contract.", + "type": "object", + "required": [ + "factory" + ], + "properties": { + "factory": { + "$ref": "#/definitions/Binary" + } + }, + "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 +330,7 @@ "title": "ExecuteMsg", "oneOf": [ { + "description": "Stakes tokens with the contract to get voting power in the DAO", "type": "object", "required": [ "stake" @@ -137,6 +344,7 @@ "additionalProperties": false }, { + "description": "Unstakes tokens so that they begin unbonding", "type": "object", "required": [ "unstake" @@ -158,6 +366,7 @@ "additionalProperties": false }, { + "description": "Updates the contract configuration", "type": "object", "required": [ "update_config" @@ -175,18 +384,6 @@ "type": "null" } ] - }, - "manager": { - "type": [ - "string", - "null" - ] - }, - "owner": { - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -195,6 +392,7 @@ "additionalProperties": false }, { + "description": "Claims unstaked tokens that have completed the unbonding period", "type": "object", "required": [ "claim" @@ -206,9 +404,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": [ @@ -266,6 +587,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -316,6 +650,58 @@ }, "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": [ + "token_contract" + ], + "properties": { + "token_contract": { + "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", @@ -408,6 +794,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", @@ -512,9 +975,9 @@ "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" }, - "get_config": { + "denom": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Config", + "title": "DenomResponse", "type": "object", "required": [ "denom" @@ -522,27 +985,15 @@ "properties": { "denom": { "type": "string" - }, - "manager": { - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, - "owner": { - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - }, + } + }, + "additionalProperties": false + }, + "get_config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "properties": { "unstaking_duration": { "anyOf": [ { @@ -556,10 +1007,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 +1043,23 @@ } } }, + "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 +1094,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", @@ -669,6 +1138,24 @@ } } }, + "token_contract": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Nullable_Addr", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ], + "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" + } + } + }, "total_power_at_height": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "TotalPowerAtHeightResponse", diff --git a/contracts/voting/dao-voting-token-staked/src/contract.rs b/contracts/voting/dao-voting-token-staked/src/contract.rs new file mode 100644 index 000000000..ffcba6902 --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/src/contract.rs @@ -0,0 +1,763 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; + +use cosmwasm_std::{ + coins, from_json, to_json_binary, BankMsg, BankQuery, Binary, Coin, CosmosMsg, Deps, DepsMut, + Env, MessageInfo, Order, Reply, Response, StdResult, SubMsg, Uint128, Uint256, WasmMsg, +}; +use cw2::{get_contract_version, set_contract_version, ContractVersion}; +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_execute_data, parse_reply_instantiate_data, Duration, +}; +use dao_hooks::stake::{stake_hook_msgs, unstake_hook_msgs}; +use dao_interface::{ + state::ModuleInstantiateCallback, + token::{InitialBalance, NewTokenInfo, TokenFactoryCallback}, + voting::{IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse}, +}; +use dao_voting::{ + duration::validate_duration, + threshold::{ + assert_valid_absolute_count_threshold, assert_valid_percentage_threshold, ActiveThreshold, + ActiveThresholdResponse, + }, +}; + +use crate::error::ContractError; +use crate::msg::{ + DenomResponse, ExecuteMsg, GetHooksResponse, 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-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; +const FACTORY_EXECUTE_REPLY_ID: u64 = 2; + +// We multiply by this when calculating needed power for being active +// when using active threshold with percent +const PRECISION_FACTOR: u128 = 10u128.pow(9); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + validate_duration(msg.unstaking_duration)?; + + let config = Config { + unstaking_duration: msg.unstaking_duration, + }; + + CONFIG.save(deps.storage, &config)?; + DAO.save(deps.storage, &info.sender)?; + + // Validate Active Threshold + if let Some(active_threshold) = msg.active_threshold.as_ref() { + // Only check active threshold percentage as new tokens don't exist yet + // We will check Absolute count (if configured) later for both existing + // and new tokens. + if let ActiveThreshold::Percentage { percent } = active_threshold { + assert_valid_percentage_threshold(*percent)?; + } + ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?; + } + + match msg.token_info { + TokenInfo::Existing { denom } => { + // Validate active threshold absolute count if configured + if let Some(ActiveThreshold::AbsoluteCount { count }) = msg.active_threshold { + let supply: Coin = deps.querier.query_supply(denom.clone())?; + assert_valid_absolute_count_threshold(count, supply.amount)?; + } + + DENOM.save(deps.storage, &denom)?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute("token", "existing_token") + .add_attribute("denom", denom)) + } + TokenInfo::New(ref token) => { + let NewTokenInfo { + subdenom, + token_issuer_code_id, + .. + } = token; + + // Save new token info for use in reply + TOKEN_INSTANTIATION_INFO.save(deps.storage, &msg.token_info)?; + + // Tnstantiate cw-token-factory-issuer contract + // DAO (sender) is set as contract admin + let issuer_instantiate_msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + admin: Some(info.sender.to_string()), + code_id: *token_issuer_code_id, + msg: to_json_binary(&IssuerInstantiateMsg::NewToken { + subdenom: subdenom.to_string(), + })?, + 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)) + } + TokenInfo::Factory(binary) => match from_json(binary)? { + WasmMsg::Execute { + msg, + contract_addr, + funds, + } => { + // Call factory contract. Use only a trusted factory contract, + // as this is a critical security component and valdiation of + // setup will happen in the factory. + Ok(Response::new() + .add_attribute("action", "intantiate") + .add_attribute("token", "custom_factory") + .add_submessage(SubMsg::reply_on_success( + WasmMsg::Execute { + contract_addr, + msg, + funds, + }, + FACTORY_EXECUTE_REPLY_ID, + ))) + } + _ => Err(ContractError::UnsupportedFactoryMsg {}), + }, + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + 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 { + 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)?) }, + )?; + + // Add stake hook messages + let hook_msgs = stake_hook_msgs(HOOKS, 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 { + 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 {}) + }, + )?; + + // Add unstake hook messages + let hook_msgs = unstake_hook_msgs(HOOKS, 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 { + 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 { + 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 { + 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 } => { + assert_valid_percentage_threshold(percent)?; + } + ActiveThreshold::AbsoluteCount { count } => { + let denom = DENOM.load(deps.storage)?; + let supply: Coin = deps.querier.query_supply(denom.to_string())?; + assert_valid_absolute_count_threshold(count, supply.amount)?; + } + } + 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 { + QueryMsg::VotingPowerAtHeight { address, height } => { + to_json_binary(&query_voting_power_at_height(deps, env, address, height)?) + } + QueryMsg::TotalPowerAtHeight { height } => { + to_json_binary(&query_total_power_at_height(deps, env, height)?) + } + QueryMsg::Info {} => query_info(deps), + QueryMsg::Dao {} => query_dao(deps), + QueryMsg::Claims { address } => to_json_binary(&query_claims(deps, address)?), + QueryMsg::GetConfig {} => to_json_binary(&CONFIG.load(deps.storage)?), + QueryMsg::Denom {} => to_json_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_json_binary(&query_hooks(deps)?), + QueryMsg::TokenContract {} => { + to_json_binary(&TOKEN_ISSUER_CONTRACT.may_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_json_binary(&dao_interface::voting::InfoResponse { info }) +} + +pub fn query_dao(deps: Deps) -> StdResult { + let dao = DAO.load(deps.storage)?; + to_json_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_json_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_json_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_json_binary(&IsActiveResponse { + active: actual_power >= count, + }) + } + } + } else { + to_json_binary(&IsActiveResponse { active: true }) + } +} + +pub fn query_active_threshold(deps: Deps) -> StdResult { + to_json_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 { + let storage_version: ContractVersion = get_contract_version(deps.storage)?; + + // Only migrate if newer + if storage_version.version.as_str() < CONTRACT_VERSION { + // 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 { + 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::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(); + + // Validate active threshold absolute count if configured + if let Some(ActiveThreshold::AbsoluteCount { count }) = + ACTIVE_THRESHOLD.may_load(deps.storage)? + { + // We use initial_supply here because the DAO balance is not + // able to be staked by users. + assert_valid_absolute_count_threshold(count, initial_supply)?; + } + + // 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_json_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_json_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_json_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_json_binary(&IssuerExecuteMsg::Mint { + to_address: dao.to_string(), + amount: initial_dao_balance, + })?, + funds: vec![], + }); + } + } + + // Begin update issuer contract owner to be the DAO, this is a + // two-step ownership transfer. + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_json_binary(&IssuerExecuteMsg::UpdateOwnership( + cw_ownable::Action::TransferOwnership { + new_owner: dao.to_string(), + expiry: None, + }, + ))?, + funds: vec![], + }); + + // On setup success, have the DAO complete the second part of + // ownership transfer by accepting ownership in a + // ModuleInstantiateCallback. + let callback = to_json_binary(&ModuleInstantiateCallback { + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_json_binary(&IssuerExecuteMsg::UpdateOwnership( + cw_ownable::Action::AcceptOwnership {}, + ))?, + funds: vec![], + })], + })?; + + Ok(Response::new() + .add_attribute("denom", denom) + .add_attribute("token_contract", issuer_addr) + .add_messages(msgs) + .set_data(callback)) + } + _ => unreachable!(), + } + } + FACTORY_EXECUTE_REPLY_ID => { + // Parse reply + let res = parse_reply_execute_data(msg)?; + match res.data { + Some(data) => { + // Parse info from the callback, this will fail + // if incorrectly formatted. + let info: TokenFactoryCallback = from_json(data)?; + + // Save Denom + DENOM.save(deps.storage, &info.denom)?; + + // Save token issuer contract if one is returned + if let Some(ref token_contract) = info.token_contract { + TOKEN_ISSUER_CONTRACT + .save(deps.storage, &deps.api.addr_validate(token_contract)?)?; + } + + // Construct the response + let mut res = Response::new() + .add_attribute("denom", info.denom) + .add_attribute("token_contract", info.token_contract.unwrap_or_default()); + + // If a callback has been configured, set the module + // instantiate callback data. + if let Some(callback) = info.module_instantiate_callback { + res = res.set_data(to_json_binary(&callback)?); + } + + Ok(res) + } + None => Err(ContractError::NoFactoryCallback {}), + } + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/voting/dao-voting-token-staked/src/error.rs b/contracts/voting/dao-voting-token-staked/src/error.rs new file mode 100644 index 000000000..7086f9bb6 --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/src/error.rs @@ -0,0 +1,52 @@ +use cosmwasm_std::StdError; +use cw_utils::{ParseReplyError, PaymentError}; +use dao_voting::threshold::ActiveThresholdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + ActiveThresholdError(#[from] ActiveThresholdError), + + #[error(transparent)] + HookError(#[from] cw_hooks::HookError), + + #[error(transparent)] + PaymentError(#[from] PaymentError), + + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + + #[error(transparent)] + UnstakingDurationError(#[from] dao_voting::duration::UnstakingDurationError), + + #[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("Factory contract did not implment the required TokenFactoryCallback interface")] + NoFactoryCallback {}, + + #[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("Factory message must serialize to WasmMsg::Execute")] + UnsupportedFactoryMsg {}, + + #[error("Amount being unstaked must be non-zero")] + ZeroUnstake {}, +} diff --git a/contracts/voting/dao-voting-native-staked/src/lib.rs b/contracts/voting/dao-voting-token-staked/src/lib.rs similarity index 100% rename from contracts/voting/dao-voting-native-staked/src/lib.rs rename to contracts/voting/dao-voting-token-staked/src/lib.rs diff --git a/contracts/voting/dao-voting-token-staked/src/msg.rs b/contracts/voting/dao-voting-token-staked/src/msg.rs new file mode 100644 index 000000000..52c71baa9 --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/src/msg.rs @@ -0,0 +1,106 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Binary, Uint128}; +use cw_utils::Duration; +use dao_dao_macros::{active_query, voting_module_query}; +use dao_interface::token::NewTokenInfo; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; + +#[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. + 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), + /// Uses a factory contract that must return the denom, optionally a Token Contract address. + /// The binary must serialize to a `WasmMsg::Execute` message. + /// Validation happens in the factory contract itself, so be sure to use a + /// trusted factory contract. + Factory(Binary), +} + +#[cw_serde] +pub struct InstantiateMsg { + /// 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] +#[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 {}, + #[returns(Option)] + TokenContract {}, +} + +#[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-staked/src/state.rs b/contracts/voting/dao-voting-token-staked/src/state.rs new file mode 100644 index 000000000..6fb8bc4a0 --- /dev/null +++ b/contracts/voting/dao-voting-token-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-staked/src/tests/mod.rs b/contracts/voting/dao-voting-token-staked/src/tests/mod.rs new file mode 100644 index 000000000..d4cde99cd --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/src/tests/mod.rs @@ -0,0 +1,10 @@ +// Tests for the crate, using cw-multi-test +// Most coverage lives here +mod multitest; + +// Integration 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-staked/src/tests/multitest/mod.rs b/contracts/voting/dao-voting-token-staked/src/tests/multitest/mod.rs new file mode 100644 index 000000000..14f00389d --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/src/tests/multitest/mod.rs @@ -0,0 +1 @@ +mod tests; diff --git a/contracts/voting/dao-voting-native-staked/src/tests.rs b/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs similarity index 56% rename from contracts/voting/dao-voting-native-staked/src/tests.rs rename to contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs index b6973ff33..3498764ec 100644 --- a/contracts/voting/dao-voting-native-staked/src/tests.rs +++ b/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs @@ -1,86 +1,97 @@ use crate::contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}; use crate::msg::{ - ExecuteMsg, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg, StakerBalanceResponse, + DenomResponse, ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, + QueryMsg, StakerBalanceResponse, TokenInfo, }; 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, + next_block, App, AppResponse, BankSudo, Contract, ContractWrapper, Executor, SudoMsg, }; 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 hook_counter_contract() -> Box> { + let contract = ContractWrapper::new( + dao_proposal_hook_counter::contract::execute, + dao_proposal_hook_counter::contract::instantiate, + dao_proposal_hook_counter::contract::query, + ); + 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) + .with_migrate(crate::contract::migrate); Box::new(contract) } fn mock_app() -> App { - custom_app(|r, _a, s| { - r.bank - .init_balance( - s, - &Addr::unchecked(DAO_ADDR), - vec![ - Coin { - denom: DENOM.to_string(), - amount: Uint128::new(10000), - }, - Coin { - denom: INVALID_DENOM.to_string(), - amount: Uint128::new(10000), - }, - ], - ) - .unwrap(); - r.bank - .init_balance( - s, - &Addr::unchecked(ADDR1), - vec![ - Coin { - denom: DENOM.to_string(), - amount: Uint128::new(10000), - }, - Coin { - denom: INVALID_DENOM.to_string(), - amount: Uint128::new(10000), - }, - ], - ) - .unwrap(); - r.bank - .init_balance( - s, - &Addr::unchecked(ADDR2), - vec![ - Coin { - denom: DENOM.to_string(), - amount: Uint128::new(10000), - }, - Coin { - denom: INVALID_DENOM.to_string(), - amount: Uint128::new(10000), - }, - ], - ) - .unwrap(); - }) + let mut app = App::default(); + 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 { @@ -139,18 +150,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 }, &[], ) } @@ -196,85 +201,90 @@ fn get_balance(app: &mut App, address: &str, denom: &str) -> Uint128 { } #[test] -fn test_instantiate() { +fn test_instantiate_existing() { let mut app = mock_app(); + let staking_id = app.store_code(staking_contract()); // Populated fields - let _addr = instantiate_staking( + 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); // Non populated fields - let _addr = instantiate_staking( + let addr = instantiate_staking( &mut app, staking_id, InstantiateMsg { - owner: None, - manager: None, - denom: DENOM.to_string(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: None, + active_threshold: None, }, ); + + let denom: DenomResponse = app + .wrap() + .query_wasm_smart(addr, &QueryMsg::Denom {}) + .unwrap(); + assert_eq!( + denom, + DenomResponse { + denom: DENOM.to_string() + } + ); } #[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( + 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)), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + 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, + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Time(0)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1), + }), }, ); } @@ -283,15 +293,17 @@ fn test_instantiate_invalid_unstaking_duration() { #[should_panic(expected = "Must send reserve token 'ujuno'")] fn test_stake_invalid_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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -302,15 +314,17 @@ fn test_stake_invalid_denom() { #[test] fn test_stake_valid_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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -323,15 +337,17 @@ fn test_stake_valid_denom() { #[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 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -342,15 +358,17 @@ fn test_unstake_none_staked() { #[should_panic(expected = "Amount being unstaked must be non-zero")] fn test_unstake_zero_tokens() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -361,15 +379,17 @@ fn test_unstake_zero_tokens() { #[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 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -384,15 +404,17 @@ fn test_unstake_invalid_balance() { #[test] fn test_unstake() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -419,15 +441,17 @@ fn test_unstake() { #[test] fn test_unstake_no_unstaking_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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: None, + active_threshold: None, }, ); @@ -456,15 +480,17 @@ fn test_unstake_no_unstaking_duration() { #[should_panic(expected = "Nothing to claim")] fn test_claim_no_claims() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -475,15 +501,17 @@ fn test_claim_no_claims() { #[should_panic(expected = "Nothing to claim")] fn test_claim_claim_not_reached() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -502,15 +530,17 @@ fn test_claim_claim_not_reached() { #[test] fn test_claim() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -553,121 +583,48 @@ fn test_claim() { #[should_panic(expected = "Unauthorized")] fn test_update_config_invalid_sender() { 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)), - }, - ); - - // 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(); -} -#[test] -#[should_panic(expected = "Only owner can change owner")] -fn test_update_config_non_owner_changes_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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); - // ADDR1 is the manager so cannot change the owner - update_config(&mut app, addr, ADDR1, Some(ADDR2.to_string()), None, None).unwrap(); + // 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 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)), - }, - ); - // 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(); - - 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(), - }, - config - ); -} - -#[test] -fn test_update_config_as_manager() { - 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(), + 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.clone(), - ADDR1, - Some(DAO_ADDR.to_string()), - Some(ADDR2.to_string()), - Some(Duration::Height(10)), - ) - .unwrap(); + // 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 { - owner: Some(Addr::unchecked(DAO_ADDR)), - manager: Some(Addr::unchecked(ADDR2)), unstaking_duration: Some(Duration::Height(10)), - denom: DENOM.to_string(), }, config ); @@ -677,42 +634,38 @@ 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(), + 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, - ADDR1, - Some(DAO_ADDR.to_string()), - Some(ADDR2.to_string()), - Some(Duration::Height(0)), - ) - .unwrap(); + update_config(&mut app, addr, DAO_ADDR, Some(Duration::Height(0))).unwrap(); } #[test] 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -724,35 +677,39 @@ fn test_query_dao() { #[test] fn test_query_info() { 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(), + 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-native-staked"); + assert_eq!(resp.info.contract, "crates.io:dao-voting-token-staked"); } #[test] fn test_query_claims() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -780,15 +737,17 @@ fn test_query_claims() { #[test] fn test_query_get_config() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -796,10 +755,7 @@ 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(), } ) } @@ -807,15 +763,17 @@ fn test_query_get_config() { #[test] fn test_voting_power_queries() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -913,15 +871,17 @@ fn test_voting_power_queries() { #[test] fn test_query_list_stakers() { 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(), + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, }, ); @@ -994,10 +954,416 @@ 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 { + 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 staking_id = app.store_code(staking_contract()); + + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + 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 staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + 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 staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + 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 staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + 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 staking_id = app.store_code(staking_contract()); + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + 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 not greater 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 { + 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 not greater 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 { + 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 staking_id = app.store_code(staking_contract()); + instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + 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 staking_id = app.store_code(staking_contract()); + + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + 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] +fn test_staking_hooks() { + let mut app = mock_app(); + + let staking_id = app.store_code(staking_contract()); + let hook_id = app.store_code(hook_counter_contract()); + + let hook = app + .instantiate_contract( + hook_id, + Addr::unchecked(DAO_ADDR), + &dao_proposal_hook_counter::msg::InstantiateMsg { + should_error: false, + }, + &[], + "hook counter".to_string(), + None, + ) + .unwrap(); + + let addr = instantiate_staking( + &mut app, + staking_id, + InstantiateMsg { + token_info: TokenInfo::Existing { + denom: DENOM.to_string(), + }, + unstaking_duration: Some(Duration::Height(5)), + active_threshold: None, + }, + ); + + // Add a staking hook. + app.execute_contract( + Addr::unchecked(DAO_ADDR), + addr.clone(), + &ExecuteMsg::AddHook { + addr: hook.to_string(), + }, + &[], + ) + .unwrap(); + + // Stake some tokens + let res = stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap(); + + // Make sure hook is included in response + assert_eq!("stake_hook", res.events.last().unwrap().attributes[1].value); + + app.update_block(next_block); + + // Unstake some + let res = unstake_tokens(&mut app, addr, ADDR1, 75).unwrap(); + + // Make sure hook is included in response + assert_eq!("stake_hook", res.events.last().unwrap().attributes[1].value); +} + #[test] pub fn test_migrate_update_version() { let mut deps = mock_dependencies(); - cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); + cw2::set_contract_version(&mut deps.storage, "my-contract", "1.0.0").unwrap(); migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); let version = cw2::get_contract_version(&deps.storage).unwrap(); assert_eq!(version.version, CONTRACT_VERSION); diff --git a/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs new file mode 100644 index 000000000..77effcda6 --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs @@ -0,0 +1,736 @@ +use crate::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg, TokenInfo}, + tests::test_tube::test_env::TokenVotingContract, + ContractError, +}; +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Uint128, WasmMsg}; +use cw_ownable::Ownership; +use cw_tokenfactory_issuer::msg::{DenomUnit, QueryMsg as IssuerQueryMsg}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo}, + token::{InitialBalance, NewDenomMetadata, NewTokenInfo}, +}; +use dao_testing::test_tube::{cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{ActiveThreshold, ActiveThresholdError, PercentageThreshold, Threshold}, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest, Account, OsmosisTestApp, + RunnerError, +}; + +use super::test_env::{TestEnv, TestEnvBuilder, DENOM}; + +#[test] +fn test_full_integration_correct_setup() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { dao, tf_issuer, .. } = env.full_dao_setup(&app); + + // Issuer owner should be set to the DAO + let issuer_admin = tf_issuer + .query::>(&cw_tokenfactory_issuer::msg::QueryMsg::Ownership {}) + .unwrap() + .owner; + assert_eq!( + issuer_admin, + Some(Addr::unchecked(dao.unwrap().contract_addr)) + ); +} + +#[test] +fn test_stake_unstake_new_denom() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + vp_contract, + accounts, + .. + } = env.full_dao_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_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: tf_issuer_id, + 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_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: tf_issuer_id, + 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_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: tf_issuer_id, + 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_invalid_active_threshold_count_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(); + + let err = env + .instantiate( + &InstantiateMsg { + token_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: tf_issuer_id, + 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: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1000), + }), + }, + dao, + ) + .unwrap_err(); + + assert_eq!( + err, + TokenVotingContract::execute_submessage_error(ContractError::ActiveThresholdError( + ActiveThresholdError::InvalidAbsoluteCount {} + )) + ); +} + +#[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_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: tf_issuer_id, + 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, + TokenVotingContract::execute_submessage_error(ContractError::InitialBalancesError {}) + ); +} + +#[test] +fn test_factory() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + tf_issuer, + vp_contract, + proposal_single, + custom_factory, + accounts, + .. + } = env.full_dao_setup(&app); + + let factory_addr = custom_factory.unwrap().contract_addr.to_string(); + + // Instantiate a new voting contract using the factory pattern + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_json_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.clone(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactory( + NewTokenInfo { + token_issuer_code_id: tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: None, + initial_balances: vec![InitialBalance { + address: accounts[0].address(), + amount: Uint128::new(100), + }], + initial_dao_balance: None, + }, + ), + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.unwrap().code_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO succeeds + let dao = DaoCore::new(&app, &msg, &accounts[0], &vec![]).unwrap(); + + // Query voting module + let voting_module: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let voting = + TokenVotingContract::new_with_values(&app, vp_contract.code_id, voting_module.to_string()) + .unwrap(); + + // Query denom + let denom = voting.query_denom().unwrap().denom; + + // Query token contract + let token_contract: Addr = voting.query(&QueryMsg::TokenContract {}).unwrap(); + + // Check the TF denom is as expected + assert_eq!(denom, format!("factory/{}/{}", token_contract, DENOM)); + + // Check issuer ownership is the DAO and the ModuleInstantiateCallback + // has successfully accepted ownership. + let issuer = + TokenfactoryIssuer::new_with_values(&app, tf_issuer.code_id, token_contract.to_string()) + .unwrap(); + let ownership: Ownership = issuer.query(&IssuerQueryMsg::Ownership {}).unwrap(); + let owner = ownership.owner.unwrap(); + assert_eq!(owner, dao.contract_addr); +} + +#[test] +fn test_factory_funds_pass_through() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + tf_issuer, + vp_contract, + proposal_single, + custom_factory, + accounts, + .. + } = env.full_dao_setup(&app); + + let factory_addr = custom_factory.unwrap().contract_addr.to_string(); + + // Instantiate a new voting contract using the factory pattern + let mut msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_json_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.clone(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactoryWithFunds( + NewTokenInfo { + token_issuer_code_id: tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: None, + initial_balances: vec![InitialBalance { + address: accounts[0].address(), + amount: Uint128::new(100), + }], + initial_dao_balance: None, + }, + ), + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.unwrap().code_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO fails because no funds to create the token were sent + let err = DaoCore::new(&app, &msg, &accounts[0], &vec![]).unwrap_err(); + + // Check error is no funds sent + assert_eq!( + err, + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: dispatch: submessages: No funds sent: execute wasm contract failed".to_string(), + } + ); + + // Include funds in ModuleInstantiateInfo + let funds = vec![Coin { + denom: "uosmo".to_string(), + amount: Uint128::new(100), + }]; + msg.voting_module_instantiate_info = ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_json_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr, + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactoryWithFunds( + NewTokenInfo { + token_issuer_code_id: tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: None, + initial_balances: vec![InitialBalance { + address: accounts[0].address(), + amount: Uint128::new(100), + }], + initial_dao_balance: None, + }, + ), + ) + .unwrap(), + funds: funds.clone(), + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: funds.clone(), + label: "DAO DAO Voting Module".to_string(), + }; + + // Creating the DAO now succeeds + DaoCore::new(&app, &msg, &accounts[0], &funds).unwrap(); +} + +#[test] +fn test_factory_no_callback() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + vp_contract, + proposal_single, + custom_factory, + accounts, + .. + } = env.full_dao_setup(&app); + + let factory_addr = custom_factory.unwrap().contract_addr.to_string(); + + // Instantiate a new voting contract using the factory pattern + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_json_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.clone(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactoryNoCallback{}, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.unwrap().code_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO fails because no callback + let err = DaoCore::new(&app, &msg, &accounts[0], &vec![]).unwrap_err(); + + // Check error is no reply data + assert_eq!( + err, + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: dispatch: submessages: reply: Invalid reply from sub-message: Missing reply data: execute wasm contract failed".to_string(), + } + ); +} + +#[test] +fn test_factory_wrong_callback() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + vp_contract, + proposal_single, + custom_factory, + accounts, + .. + } = env.full_dao_setup(&app); + + let factory_addr = custom_factory.unwrap().contract_addr.to_string(); + // Instantiate a new voting contract using the factory pattern + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_json_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_json_binary(&WasmMsg::Execute { + contract_addr: factory_addr.clone(), + msg: to_json_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactoryWrongCallback{}, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.unwrap().code_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO fails because of wrong callback + let err = DaoCore::new(&app, &msg, &accounts[0], &vec![]).unwrap_err(); + + // Check error is wrong reply type + assert_eq!( + err, + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: dispatch: submessages: reply: Error parsing into type dao_interface::token::TokenFactoryCallback: unknown field `nft_contract`, expected one of `denom`, `token_contract`, `module_instantiate_callback`: execute wasm contract failed".to_string(), + } + ); +} diff --git a/contracts/voting/dao-voting-token-staked/src/tests/test_tube/mod.rs b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/mod.rs new file mode 100644 index 000000000..fe51e9fb6 --- /dev/null +++ b/contracts/voting/dao-voting-token-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-staked/src/tests/test_tube/test_env.rs b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/test_env.rs new file mode 100644 index 000000000..3c93c9ee6 --- /dev/null +++ b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/test_env.rs @@ -0,0 +1,508 @@ +// 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, InstantiateMsg, QueryMsg, TokenInfo}, + ContractError, +}; + +use cosmwasm_std::{to_json_binary, Addr, Coin, Decimal, Uint128}; +use cw_tokenfactory_issuer::msg::{DenomResponse, DenomUnit}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo, ProposalModule}, + token::{InitialBalance, NewDenomMetadata, NewTokenInfo}, + voting::{IsActiveResponse, VotingPowerAtHeightResponse}, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, threshold::PercentageThreshold, threshold::Threshold, +}; + +use dao_testing::test_tube::{ + cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore, + dao_proposal_single::DaoProposalSingle, dao_test_custom_factory::CustomFactoryContract, +}; +use dao_voting::threshold::ActiveThreshold; +use osmosis_test_tube::{ + osmosis_std::types::{ + cosmos::bank::v1beta1::QueryAllBalancesRequest, + cosmwasm::wasm::v1::MsgExecuteContractResponse, + }, + 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 dao: Option>, + pub proposal_single: Option>, + pub custom_factory: Option>, + pub vp_contract: TokenVotingContract<'a>, + pub tf_issuer: TokenfactoryIssuer<'a>, + pub accounts: Vec, +} + +impl<'a> TestEnv<'a> { + pub fn instantiate( + &self, + msg: &InstantiateMsg, + signer: SigningAccount, + ) -> Result { + TokenVotingContract::<'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, + } + } + + // Minimal default setup with just the key contracts + 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 = TokenVotingContract::deploy( + app, + &InstantiateMsg { + token_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: issuer_id, + subdenom: DENOM.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, + 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 = + TokenVotingContract::query(&vp_contract, &QueryMsg::TokenContract {}).unwrap(); + + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + + TestEnv { + app, + accounts, + dao: None, + proposal_single: None, + custom_factory: None, + tf_issuer, + vp_contract, + } + } + + // Full DAO setup + pub fn full_dao_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(); + + // Upload all needed code ids + let issuer_id = TokenfactoryIssuer::upload(app, &accounts[0]).unwrap(); + let vp_contract_id = TokenVotingContract::upload(app, &accounts[0]).unwrap(); + let proposal_single_id = DaoProposalSingle::upload(app, &accounts[0]).unwrap(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract_id, + msg: to_json_binary(&InstantiateMsg { + token_info: TokenInfo::New(NewTokenInfo { + token_issuer_code_id: issuer_id, + subdenom: DENOM.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, + initial_dao_balance: Some(Uint128::new(900)), + }), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single_id, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO + let dao = DaoCore::new(app, &msg, &accounts[0], &vec![]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + TokenVotingContract::new_with_values(app, vp_contract_id, vp_addr.to_string()).unwrap(); + + // Get proposal module address, setup proposal_single helper + let proposal_modules: Vec = dao + .query(&DaoQueryMsg::ProposalModules { + limit: None, + start_after: None, + }) + .unwrap(); + let proposal_single = DaoProposalSingle::new_with_values( + app, + proposal_single_id, + proposal_modules[0].address.to_string(), + ) + .unwrap(); + + // Get issuer address, setup tf_issuer helper + let issuer_addr = + TokenVotingContract::query(&vp_contract, &QueryMsg::TokenContract {}).unwrap(); + let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + + // Instantiate Custom Factory + let custom_factory = CustomFactoryContract::new( + app, + &dao_test_custom_factory::msg::InstantiateMsg {}, + &accounts[0], + ) + .unwrap(); + + TestEnv { + app, + dao: Some(dao), + vp_contract, + proposal_single: Some(proposal_single), + custom_factory: Some(custom_factory), + 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 TokenVotingContract<'a> { + pub app: &'a OsmosisTestApp, + pub contract_addr: String, + pub code_id: u64, +} + +impl<'a> TokenVotingContract<'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 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) + } + + 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_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_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 bc0779bb5..5ceec3503 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.11 - -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.11 + #!/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.14.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.14.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.14.0; fi diff --git a/packages/cw-denom/src/lib.rs b/packages/cw-denom/src/lib.rs index ec6a5dfc9..1babe5a21 100644 --- a/packages/cw-denom/src/lib.rs +++ b/packages/cw-denom/src/lib.rs @@ -7,7 +7,7 @@ use std::fmt::{self}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - to_binary, Addr, BankMsg, Coin, CosmosMsg, CustomQuery, Deps, QuerierWrapper, StdError, + to_json_binary, Addr, BankMsg, Coin, CosmosMsg, CustomQuery, Deps, QuerierWrapper, StdError, StdResult, Uint128, WasmMsg, }; @@ -151,7 +151,7 @@ impl CheckedDenom { .into(), CheckedDenom::Cw20(address) => WasmMsg::Execute { contract_addr: address.to_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient: who.to_string(), amount, })?, @@ -203,7 +203,8 @@ impl fmt::Display for CheckedDenom { mod tests { use cosmwasm_std::{ testing::{mock_dependencies, MockQuerier}, - to_binary, Addr, ContractResult, QuerierResult, StdError, SystemError, Uint128, WasmQuery, + to_json_binary, Addr, ContractResult, QuerierResult, StdError, SystemError, Uint128, + WasmQuery, }; use super::*; @@ -217,7 +218,7 @@ mod tests { if *contract_addr == CW20_ADDR { if works { QuerierResult::Ok(ContractResult::Ok( - to_binary(&cw20::TokenInfoResponse { + to_json_binary(&cw20::TokenInfoResponse { name: "coin".to_string(), symbol: "symbol".to_string(), decimals: 6, diff --git a/packages/cw-hooks/README.md b/packages/cw-hooks/README.md index d597f945c..f078a6069 100644 --- a/packages/cw-hooks/README.md +++ b/packages/cw-hooks/README.md @@ -1,7 +1,7 @@ # CosmWasm DAO Hooks This package provides shared hook functionality used for -[proposal](../dao-proposal-hooks) and [vote](../dao-vote-hooks) hooks. +[dao-hooks](../dao-hooks). It deviates from other CosmWasm hook packages in that hooks can be modified based on their index in the hook list AND based on the 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/cw-paginate-storage/README.md b/packages/cw-paginate-storage/README.md index 03a9d03eb..8ab92c658 100644 --- a/packages/cw-paginate-storage/README.md +++ b/packages/cw-paginate-storage/README.md @@ -18,7 +18,7 @@ pub const ITEMS: Map = Map::new("items"); You can use this package to write a query to list it's contents like: ```rust -use cosmwasm_std::{Deps, Binary, to_binary, StdResult}; +use cosmwasm_std::{Deps, Binary, to_json_binary, StdResult}; use cw_storage_plus::Map; use cw_paginate_storage::paginate_map; @@ -29,7 +29,7 @@ pub fn query_list_items( start_after: Option, limit: Option, ) -> StdResult { - to_binary(&paginate_map( + to_json_binary(&paginate_map( deps, &ITEMS, start_after, diff --git a/packages/cw-stake-tracker/src/lib.rs b/packages/cw-stake-tracker/src/lib.rs index fc9a774cf..b9c633966 100644 --- a/packages/cw-stake-tracker/src/lib.rs +++ b/packages/cw-stake-tracker/src/lib.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{to_binary, Binary, StdResult, Storage, Timestamp, Uint128}; +use cosmwasm_std::{to_json_binary, Binary, StdResult, Storage, Timestamp, Uint128}; use cw_wormhole::Wormhole; #[cfg(test)] @@ -261,12 +261,12 @@ impl<'a> StakeTracker<'a> { /// API. pub fn query(&self, storage: &dyn Storage, msg: StakeTrackerQuery) -> StdResult { match msg { - StakeTrackerQuery::Cardinality { t } => to_binary(&Uint128::new( + StakeTrackerQuery::Cardinality { t } => to_json_binary(&Uint128::new( self.validator_cardinality(storage, t)?.into(), )), - StakeTrackerQuery::TotalStaked { t } => to_binary(&self.total_staked(storage, t)?), + StakeTrackerQuery::TotalStaked { t } => to_json_binary(&self.total_staked(storage, t)?), StakeTrackerQuery::ValidatorStaked { validator, t } => { - to_binary(&self.validator_staked(storage, t, validator)?) + to_json_binary(&self.validator_staked(storage, t, validator)?) } } } diff --git a/packages/cw-stake-tracker/src/tests.rs b/packages/cw-stake-tracker/src/tests.rs index 3d0c46b37..2454f9af7 100644 --- a/packages/cw-stake-tracker/src/tests.rs +++ b/packages/cw-stake-tracker/src/tests.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{from_binary, testing::mock_dependencies, Timestamp, Uint128}; +use cosmwasm_std::{from_json, testing::mock_dependencies, Timestamp, Uint128}; use crate::{StakeTracker, StakeTrackerQuery}; @@ -431,8 +431,8 @@ fn test_queries() { ) .unwrap(); - let cardinality: Uint128 = from_binary( - &st.query( + let cardinality: Uint128 = from_json( + st.query( storage, StakeTrackerQuery::Cardinality { t: Timestamp::from_seconds(11), @@ -443,8 +443,8 @@ fn test_queries() { .unwrap(); assert_eq!(cardinality, Uint128::one()); - let total_staked: Uint128 = from_binary( - &st.query( + let total_staked: Uint128 = from_json( + st.query( storage, StakeTrackerQuery::TotalStaked { t: Timestamp::from_seconds(10), @@ -455,8 +455,8 @@ fn test_queries() { .unwrap(); assert_eq!(total_staked, Uint128::new(42)); - let val_staked: Uint128 = from_binary( - &st.query( + let val_staked: Uint128 = from_json( + st.query( storage, StakeTrackerQuery::ValidatorStaked { t: Timestamp::from_seconds(10), @@ -468,8 +468,8 @@ fn test_queries() { .unwrap(); assert_eq!(val_staked, Uint128::new(42)); - let val_staked_before_staking: Uint128 = from_binary( - &st.query( + let val_staked_before_staking: Uint128 = from_json( + st.query( storage, StakeTrackerQuery::ValidatorStaked { t: Timestamp::from_seconds(9), diff --git a/packages/dao-vote-hooks/Cargo.toml b/packages/dao-cw721-extensions/Cargo.toml similarity index 55% rename from packages/dao-vote-hooks/Cargo.toml rename to packages/dao-cw721-extensions/Cargo.toml index 067e11158..609e89d0d 100644 --- a/packages/dao-vote-hooks/Cargo.toml +++ b/packages/dao-cw721-extensions/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "dao-vote-hooks" -authors = ["ekez ekez@withoutdoing.com"] -description = "A package for managing vote hooks." +name = "dao-cw721-extensions" +authors = ["Jake Hartnell"] +description = "A package for DAO cw721 extensions." edition = { workspace = true } license = { workspace = true } repository = { workspace = true } @@ -10,5 +10,5 @@ version = { workspace = true } [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } -cw-hooks = { workspace = true } -dao-voting = { workspace = true } +cw-controllers = { workspace = true } +cw4 = { workspace = true } diff --git a/packages/dao-cw721-extensions/README.md b/packages/dao-cw721-extensions/README.md new file mode 100644 index 000000000..4fae07714 --- /dev/null +++ b/packages/dao-cw721-extensions/README.md @@ -0,0 +1,3 @@ +# DAO CW721 Extensions: extensions for DAO related NFT contracts + +This implements extensions for cw721 NFT contracts integrating with DAO DAO (for example `cw721-roles`). diff --git a/packages/dao-cw721-extensions/src/lib.rs b/packages/dao-cw721-extensions/src/lib.rs new file mode 100644 index 000000000..22bacf03c --- /dev/null +++ b/packages/dao-cw721-extensions/src/lib.rs @@ -0,0 +1,3 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod roles; diff --git a/packages/dao-cw721-extensions/src/roles.rs b/packages/dao-cw721-extensions/src/roles.rs new file mode 100644 index 000000000..0f2c9166a --- /dev/null +++ b/packages/dao-cw721-extensions/src/roles.rs @@ -0,0 +1,56 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::CustomMsg; + +#[cw_serde] +pub struct MetadataExt { + /// Optional on-chain role for this member, can be used by other contracts to enforce permissions + pub role: Option, + /// The voting weight of this role + pub weight: u64, +} + +#[cw_serde] +pub enum ExecuteExt { + /// Add a new hook to be informed of all membership changes. + /// Must be called by Admin + AddHook { addr: String }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: String }, + /// Update the token_uri for a particular NFT. Must be called by minter / admin + UpdateTokenUri { + token_id: String, + token_uri: Option, + }, + /// Updates the voting weight of a token. Must be called by minter / admin + UpdateTokenWeight { token_id: String, weight: u64 }, + /// Udates the role of a token. Must be called by minter / admin + UpdateTokenRole { + token_id: String, + role: Option, + }, +} +impl CustomMsg for ExecuteExt {} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryExt { + /// Total weight at a given height + #[returns(cw4::TotalWeightResponse)] + TotalWeight { at_height: Option }, + /// Returns a list of Members + #[returns(cw4::MemberListResponse)] + ListMembers { + start_after: Option, + limit: Option, + }, + /// Returns the weight of a certain member + #[returns(cw4::MemberResponse)] + Member { + addr: String, + at_height: Option, + }, + /// Shows all registered hooks. + #[returns(cw_controllers::HooksResponse)] + Hooks {}, +} +impl CustomMsg for QueryExt {} diff --git a/packages/dao-dao-macros/src/lib.rs b/packages/dao-dao-macros/src/lib.rs index 4c11c8b71..8b2dddda9 100644 --- a/packages/dao-dao-macros/src/lib.rs +++ b/packages/dao-dao-macros/src/lib.rs @@ -26,7 +26,7 @@ fn merge_variants(metadata: TokenStream, left: TokenStream, right: TokenStream) }), ) = (&mut left.data, right.data) { - variants.extend(to_add.into_iter()); + variants.extend(to_add); quote! { #left }.into() } else { diff --git a/packages/dao-proposal-hooks/Cargo.toml b/packages/dao-hooks/Cargo.toml similarity index 54% rename from packages/dao-proposal-hooks/Cargo.toml rename to packages/dao-hooks/Cargo.toml index 7ecb29919..7e9a4f0eb 100644 --- a/packages/dao-proposal-hooks/Cargo.toml +++ b/packages/dao-hooks/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "dao-proposal-hooks" -authors = ["Callum Anderson "] -description = "A package for managing proposal hooks." +name = "dao-hooks" +authors = ["ekez ekez@withoutdoing.com", "Jake Hartnell "] +description = "A package for managing DAO vote, proposal, and stake hooks." edition = { workspace = true } license = { workspace = true } repository = { workspace = true } @@ -10,5 +10,7 @@ version = { workspace = true } [dependencies] cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } +cw4 = { workspace = true } cw-hooks = { workspace = true } +dao-pre-propose-base = { workspace = true } dao-voting = { workspace = true } diff --git a/packages/dao-hooks/README.md b/packages/dao-hooks/README.md new file mode 100644 index 000000000..b2e8686c8 --- /dev/null +++ b/packages/dao-hooks/README.md @@ -0,0 +1,21 @@ +# DAO Hooks +This package provides an interface for managing and dispatching proposal, +staking, and voting related hooks. + +### NFT Stake Hooks +Staking hooks are fired when NFTs are staked or unstaked in a DAO. + +### Proposal Hooks +There are two types of proposal hooks: +- **New Proposal Hook:** fired when a new proposal is created. +- **Proposal Staus Changed Hook:** fired when a proposal's status changes. + +Our wiki contains more info on [Proposal Hooks](https://github.com/DA0-DA0/dao-contracts/wiki/Proposal-Hooks-Interactions). + +### Stake Hooks +Staking hooks are fired when tokens are staked or unstaked in a DAO. + +### Vote Hooks +Vote hooks are fired when new votes are cast. + +You can read more about vote hooks in our [wiki](https://github.com/DA0-DA0/dao-contracts/wiki/Proposal-Hooks-Interactions). diff --git a/packages/dao-hooks/src/all_hooks.rs b/packages/dao-hooks/src/all_hooks.rs new file mode 100644 index 000000000..580e773f1 --- /dev/null +++ b/packages/dao-hooks/src/all_hooks.rs @@ -0,0 +1,25 @@ +use cosmwasm_schema::cw_serde; +use cw4::MemberChangedHookMsg; + +use crate::nft_stake::NftStakeChangedHookMsg; +use crate::proposal::{PreProposeHookMsg, ProposalHookMsg}; +use crate::stake::StakeChangedHookMsg; +use crate::vote::VoteHookMsg; + +/// An enum representing all possible DAO hooks. +#[cw_serde] +pub enum DaoHooks { + /// Called when a member is added or removed + /// to a cw4-groups or cw721-roles contract. + MemberChangedHook(MemberChangedHookMsg), + /// Called when NFTs are staked or unstaked. + NftStakeChangeHook(NftStakeChangedHookMsg), + /// Pre-propose hooks + PreProposeHook(PreProposeHookMsg), + /// Called when a proposal status changes. + ProposalHook(ProposalHookMsg), + /// Called when tokens are staked or unstaked. + StakeChangeHook(StakeChangedHookMsg), + /// Called when a vote is cast. + VoteHook(VoteHookMsg), +} diff --git a/packages/dao-hooks/src/lib.rs b/packages/dao-hooks/src/lib.rs new file mode 100644 index 000000000..41e5ef970 --- /dev/null +++ b/packages/dao-hooks/src/lib.rs @@ -0,0 +1,10 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +mod all_hooks; +pub mod nft_stake; +pub mod proposal; +pub mod stake; +pub mod vote; + +pub use all_hooks::DaoHooks; +pub use cw4::MemberChangedHookMsg; diff --git a/packages/dao-hooks/src/nft_stake.rs b/packages/dao-hooks/src/nft_stake.rs new file mode 100644 index 000000000..adae78ca7 --- /dev/null +++ b/packages/dao-hooks/src/nft_stake.rs @@ -0,0 +1,58 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{to_json_binary, Addr, StdResult, Storage, SubMsg, WasmMsg}; +use cw_hooks::Hooks; + +/// An enum representing NFT staking hooks. +#[cw_serde] +pub enum NftStakeChangedHookMsg { + Stake { addr: Addr, token_id: String }, + Unstake { addr: Addr, token_ids: Vec }, +} + +/// Prepares NftStakeChangedHookMsg::Stake hook SubMsgs, +/// containing the address and the token_id staked. +pub fn stake_nft_hook_msgs( + hooks: Hooks, + storage: &dyn Storage, + addr: Addr, + token_id: String, +) -> StdResult> { + let msg = to_json_binary(&NftStakeChangedExecuteMsg::NftStakeChangeHook( + NftStakeChangedHookMsg::Stake { addr, token_id }, + ))?; + hooks.prepare_hooks(storage, |a| { + let execute = WasmMsg::Execute { + contract_addr: a.into_string(), + msg: msg.clone(), + funds: vec![], + }; + Ok(SubMsg::new(execute)) + }) +} + +/// Prepares NftStakeChangedHookMsg::Unstake hook SubMsgs, +/// containing the address and the token_ids unstaked. +pub fn unstake_nft_hook_msgs( + hooks: Hooks, + storage: &dyn Storage, + addr: Addr, + token_ids: Vec, +) -> StdResult> { + let msg = to_json_binary(&NftStakeChangedExecuteMsg::NftStakeChangeHook( + NftStakeChangedHookMsg::Unstake { addr, token_ids }, + ))?; + + hooks.prepare_hooks(storage, |a| { + let execute = WasmMsg::Execute { + contract_addr: a.into_string(), + msg: msg.clone(), + funds: vec![], + }; + Ok(SubMsg::new(execute)) + }) +} + +#[cw_serde] +pub enum NftStakeChangedExecuteMsg { + NftStakeChangeHook(NftStakeChangedHookMsg), +} diff --git a/packages/dao-proposal-hooks/src/lib.rs b/packages/dao-hooks/src/proposal.rs similarity index 57% rename from packages/dao-proposal-hooks/src/lib.rs rename to packages/dao-hooks/src/proposal.rs index 9e0142a88..c98cfdc45 100644 --- a/packages/dao-proposal-hooks/src/lib.rs +++ b/packages/dao-hooks/src/proposal.rs @@ -1,10 +1,15 @@ -#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] - use cosmwasm_schema::cw_serde; -use cosmwasm_std::{to_binary, StdResult, Storage, SubMsg, WasmMsg}; +use cosmwasm_std::{to_json_binary, Empty, StdResult, Storage, SubMsg, WasmMsg}; use cw_hooks::Hooks; -use dao_voting::reply::mask_proposal_hook_index; +use dao_voting::{ + pre_propose::ProposalCreationPolicy, + reply::{failed_pre_propose_module_hook_id, mask_proposal_hook_index}, + status::Status, +}; +/// An enum representing proposal hook messages. +/// Either a new propsoal hook, fired when a new proposal is created, +/// or a proposal status hook, fired when a proposal changes status. #[cw_serde] pub enum ProposalHookMsg { NewProposal { @@ -18,12 +23,6 @@ pub enum ProposalHookMsg { }, } -// This is just a helper to properly serialize the above message -#[cw_serde] -pub enum ProposalHookExecuteMsg { - ProposalHook(ProposalHookMsg), -} - /// Prepares new proposal hook messages. These messages reply on error /// and have even reply IDs. /// IDs are set to even numbers to then be interleaved with the vote hooks. @@ -33,7 +32,7 @@ pub fn new_proposal_hooks( id: u64, proposer: &str, ) -> StdResult> { - let msg = to_binary(&ProposalHookExecuteMsg::ProposalHook( + let msg = to_json_binary(&ProposalHookExecuteMsg::ProposalHook( ProposalHookMsg::NewProposal { id, proposer: proposer.to_string(), @@ -70,7 +69,7 @@ pub fn proposal_status_changed_hooks( return Ok(vec![]); } - let msg = to_binary(&ProposalHookExecuteMsg::ProposalHook( + let msg = to_json_binary(&ProposalHookExecuteMsg::ProposalHook( ProposalHookMsg::ProposalStatusChanged { id, old_status, @@ -92,3 +91,39 @@ pub fn proposal_status_changed_hooks( Ok(messages) } + +/// Message type used for firing hooks to a proposal module's pre-propose +/// module, if one is installed. +pub type PreProposeHookMsg = dao_pre_propose_base::msg::ExecuteMsg; + +/// Adds prepropose / deposit module hook which will handle deposit refunds. +pub fn proposal_completed_hooks( + proposal_creation_policy: ProposalCreationPolicy, + proposal_id: u64, + new_status: Status, +) -> StdResult> { + let mut hooks: Vec = vec![]; + match proposal_creation_policy { + ProposalCreationPolicy::Anyone {} => (), + ProposalCreationPolicy::Module { addr } => { + let msg = to_json_binary(&PreProposeHookMsg::ProposalCompletedHook { + proposal_id, + new_status, + })?; + hooks.push(SubMsg::reply_on_error( + WasmMsg::Execute { + contract_addr: addr.into_string(), + msg, + funds: vec![], + }, + failed_pre_propose_module_hook_id(), + )); + } + }; + Ok(hooks) +} + +#[cw_serde] +pub enum ProposalHookExecuteMsg { + ProposalHook(ProposalHookMsg), +} diff --git a/contracts/staking/cw20-stake/src/hooks.rs b/packages/dao-hooks/src/stake.rs similarity index 59% rename from contracts/staking/cw20-stake/src/hooks.rs rename to packages/dao-hooks/src/stake.rs index 5867d8ff4..f74203173 100644 --- a/contracts/staking/cw20-stake/src/hooks.rs +++ b/packages/dao-hooks/src/stake.rs @@ -1,23 +1,26 @@ -use crate::state::HOOKS; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{to_binary, Addr, StdResult, Storage, SubMsg, Uint128, WasmMsg}; +use cosmwasm_std::{to_json_binary, Addr, StdResult, Storage, SubMsg, Uint128, WasmMsg}; +use cw_hooks::Hooks; -// This is just a helper to properly serialize the above message +/// An enum representing staking hooks. #[cw_serde] pub enum StakeChangedHookMsg { Stake { addr: Addr, amount: Uint128 }, Unstake { addr: Addr, amount: Uint128 }, } +/// Prepares StakeChangedHookMsg::Stake hook SubMsgs, +/// containing the address and the amount staked. pub fn stake_hook_msgs( + hooks: Hooks, storage: &dyn Storage, addr: Addr, amount: Uint128, ) -> StdResult> { - let msg = to_binary(&StakeChangedExecuteMsg::StakeChangeHook( + let msg = to_json_binary(&StakeChangedExecuteMsg::StakeChangeHook( StakeChangedHookMsg::Stake { addr, amount }, ))?; - HOOKS.prepare_hooks(storage, |a| { + hooks.prepare_hooks(storage, |a| { let execute = WasmMsg::Execute { contract_addr: a.to_string(), msg: msg.clone(), @@ -27,15 +30,18 @@ pub fn stake_hook_msgs( }) } +/// Prepares StakeChangedHookMsg::Unstake hook SubMsgs, +/// containing the address and the amount unstaked. pub fn unstake_hook_msgs( + hooks: Hooks, storage: &dyn Storage, addr: Addr, amount: Uint128, ) -> StdResult> { - let msg = to_binary(&StakeChangedExecuteMsg::StakeChangeHook( + let msg = to_json_binary(&StakeChangedExecuteMsg::StakeChangeHook( StakeChangedHookMsg::Unstake { addr, amount }, ))?; - HOOKS.prepare_hooks(storage, |a| { + hooks.prepare_hooks(storage, |a| { let execute = WasmMsg::Execute { contract_addr: a.to_string(), msg: msg.clone(), @@ -45,8 +51,7 @@ pub fn unstake_hook_msgs( }) } -// This is just a helper to properly serialize the above message #[cw_serde] -enum StakeChangedExecuteMsg { +pub enum StakeChangedExecuteMsg { StakeChangeHook(StakeChangedHookMsg), } diff --git a/packages/dao-vote-hooks/src/lib.rs b/packages/dao-hooks/src/vote.rs similarity index 79% rename from packages/dao-vote-hooks/src/lib.rs rename to packages/dao-hooks/src/vote.rs index cfc13aedd..9d5dedf3e 100644 --- a/packages/dao-vote-hooks/src/lib.rs +++ b/packages/dao-hooks/src/vote.rs @@ -1,10 +1,9 @@ -#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] - use cosmwasm_schema::cw_serde; -use cosmwasm_std::{to_binary, StdResult, Storage, SubMsg, WasmMsg}; +use cosmwasm_std::{to_json_binary, StdResult, Storage, SubMsg, WasmMsg}; use cw_hooks::Hooks; use dao_voting::reply::mask_vote_hook_index; +/// An enum representing vote hooks, fired when new votes are cast. #[cw_serde] pub enum VoteHookMsg { NewVote { @@ -14,12 +13,6 @@ pub enum VoteHookMsg { }, } -// This is just a helper to properly serialize the above message -#[cw_serde] -pub enum VoteHookExecuteMsg { - VoteHook(VoteHookMsg), -} - /// Prepares new vote hook messages. These messages reply on error /// and have even reply IDs. /// IDs are set to odd numbers to then be interleaved with the proposal hooks. @@ -30,7 +23,7 @@ pub fn new_vote_hooks( voter: String, vote: String, ) -> StdResult> { - let msg = to_binary(&VoteHookExecuteMsg::VoteHook(VoteHookMsg::NewVote { + let msg = to_json_binary(&VoteHookExecuteMsg::VoteHook(VoteHookMsg::NewVote { proposal_id, voter, vote, @@ -48,3 +41,8 @@ pub fn new_vote_hooks( Ok(tmp) }) } + +#[cw_serde] +pub enum VoteHookExecuteMsg { + VoteHook(VoteHookMsg), +} diff --git a/packages/dao-interface/Cargo.toml b/packages/dao-interface/Cargo.toml index c82abc5e4..f05475090 100644 --- a/packages/dao-interface/Cargo.toml +++ b/packages/dao-interface/Cargo.toml @@ -15,6 +15,7 @@ cw20 = { workspace = true } cw721 = { workspace = true } cw-hooks = { workspace = true } cw-utils = { workspace = true } +osmosis-std = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } diff --git a/packages/dao-interface/src/lib.rs b/packages/dao-interface/src/lib.rs index f46f381c2..aae0678fd 100644 --- a/packages/dao-interface/src/lib.rs +++ b/packages/dao-interface/src/lib.rs @@ -2,7 +2,9 @@ pub mod migrate_msg; pub mod msg; +pub mod nft; pub mod proposal; pub mod query; pub mod state; +pub mod token; pub mod voting; diff --git a/packages/dao-interface/src/nft.rs b/packages/dao-interface/src/nft.rs new file mode 100644 index 000000000..82e7da52e --- /dev/null +++ b/packages/dao-interface/src/nft.rs @@ -0,0 +1,9 @@ +use cosmwasm_schema::cw_serde; + +use crate::state::ModuleInstantiateCallback; + +#[cw_serde] +pub struct NftFactoryCallback { + pub nft_contract: String, + pub module_instantiate_callback: Option, +} diff --git a/packages/dao-interface/src/state.rs b/packages/dao-interface/src/state.rs index 3a0841f9b..2022640eb 100644 --- a/packages/dao-interface/src/state.rs +++ b/packages/dao-interface/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Binary, CosmosMsg, WasmMsg}; +use cosmwasm_std::{Addr, Binary, Coin, CosmosMsg, WasmMsg}; /// Top level config type for core module. #[cw_serde] @@ -60,6 +60,8 @@ pub struct ModuleInstantiateInfo { /// CosmWasm level admin of the instantiated contract. See: /// pub admin: Option, + /// Funds to be sent to the instantiated contract. + pub funds: Vec, /// Label for the instantiated contract. pub label: String, } @@ -73,7 +75,7 @@ impl ModuleInstantiateInfo { }), code_id: self.code_id, msg: self.msg, - funds: vec![], + funds: self.funds, label: self.label, } } @@ -89,22 +91,23 @@ pub struct ModuleInstantiateCallback { mod tests { use super::*; - use cosmwasm_std::{to_binary, Addr, WasmMsg}; + use cosmwasm_std::{to_json_binary, Addr, WasmMsg}; #[test] fn test_module_instantiate_admin_none() { let no_admin = ModuleInstantiateInfo { code_id: 42, - msg: to_binary("foo").unwrap(), + msg: to_json_binary("foo").unwrap(), admin: None, label: "bar".to_string(), + funds: vec![], }; assert_eq!( no_admin.into_wasm_msg(Addr::unchecked("ekez")), WasmMsg::Instantiate { admin: None, code_id: 42, - msg: to_binary("foo").unwrap(), + msg: to_json_binary("foo").unwrap(), funds: vec![], label: "bar".to_string() } @@ -115,18 +118,19 @@ mod tests { fn test_module_instantiate_admin_addr() { let no_admin = ModuleInstantiateInfo { code_id: 42, - msg: to_binary("foo").unwrap(), + msg: to_json_binary("foo").unwrap(), admin: Some(Admin::Address { addr: "core".to_string(), }), label: "bar".to_string(), + funds: vec![], }; assert_eq!( no_admin.into_wasm_msg(Addr::unchecked("ekez")), WasmMsg::Instantiate { admin: Some("core".to_string()), code_id: 42, - msg: to_binary("foo").unwrap(), + msg: to_json_binary("foo").unwrap(), funds: vec![], label: "bar".to_string() } @@ -137,16 +141,17 @@ mod tests { fn test_module_instantiate_instantiator_addr() { let no_admin = ModuleInstantiateInfo { code_id: 42, - msg: to_binary("foo").unwrap(), + msg: to_json_binary("foo").unwrap(), admin: Some(Admin::CoreModule {}), label: "bar".to_string(), + funds: vec![], }; assert_eq!( no_admin.into_wasm_msg(Addr::unchecked("ekez")), WasmMsg::Instantiate { admin: Some("ekez".to_string()), code_id: 42, - msg: to_binary("foo").unwrap(), + msg: to_json_binary("foo").unwrap(), funds: vec![], label: "bar".to_string() } diff --git a/packages/dao-interface/src/token.rs b/packages/dao-interface/src/token.rs new file mode 100644 index 000000000..c735a15c4 --- /dev/null +++ b/packages/dao-interface/src/token.rs @@ -0,0 +1,52 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint128; + +// These are Cosmos Proto types used for Denom Metadata. +// We re-export them here for convenience. +pub use osmosis_std::types::cosmos::bank::v1beta1::{DenomUnit, Metadata}; + +use crate::state::ModuleInstantiateCallback; + +#[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 code id of the cw-tokenfactory-issuer contract + pub token_issuer_code_id: u64, + /// 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 struct TokenFactoryCallback { + pub denom: String, + pub token_contract: Option, + pub module_instantiate_callback: Option, +} diff --git a/packages/dao-interface/src/voting.rs b/packages/dao-interface/src/voting.rs index f2df11a44..cf6d43707 100644 --- a/packages/dao-interface/src/voting.rs +++ b/packages/dao-interface/src/voting.rs @@ -30,6 +30,11 @@ pub enum Query { IsActive {}, } +#[cw_serde] +pub enum ActiveThresholdQuery { + ActiveThreshold {}, +} + #[cw_serde] pub struct VotingPowerAtHeightResponse { pub power: Uint128, diff --git a/packages/dao-pre-propose-base/Cargo.toml b/packages/dao-pre-propose-base/Cargo.toml index ebc864973..c063d34f9 100644 --- a/packages/dao-pre-propose-base/Cargo.toml +++ b/packages/dao-pre-propose-base/Cargo.toml @@ -24,7 +24,6 @@ cw-denom = { workspace = true } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } cw-hooks = { workspace = true } -dao-proposal-hooks = { workspace = true } dao-interface = { workspace = true } dao-voting = { workspace = true } serde = { workspace = true } diff --git a/packages/dao-pre-propose-base/src/error.rs b/packages/dao-pre-propose-base/src/error.rs index 8ccfbb140..127996166 100644 --- a/packages/dao-pre-propose-base/src/error.rs +++ b/packages/dao-pre-propose-base/src/error.rs @@ -38,8 +38,8 @@ pub enum PreProposeError { #[error("Nothing to withdraw")] NothingToWithdraw {}, - #[error("Proposal status ({status}) not closed or executed")] - NotClosedOrExecuted { status: Status }, + #[error("Proposal status ({status}) is not completed")] + NotCompleted { status: Status }, #[error("Proposal not found")] ProposalNotFound {}, diff --git a/packages/dao-pre-propose-base/src/execute.rs b/packages/dao-pre-propose-base/src/execute.rs index 225a465fe..64e0ed3d6 100644 --- a/packages/dao-pre-propose-base/src/execute.rs +++ b/packages/dao-pre-propose-base/src/execute.rs @@ -1,6 +1,7 @@ use cosmwasm_schema::schemars::JsonSchema; use cosmwasm_std::{ - to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, WasmMsg, + to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, + WasmMsg, }; use cw2::set_contract_version; @@ -138,7 +139,7 @@ where let propose_messsage = WasmMsg::Execute { contract_addr: proposal_module.into_string(), - msg: to_binary(&msg)?, + msg: to_json_binary(&msg)?, funds: vec![], }; @@ -147,7 +148,7 @@ where .prepare_hooks(deps.storage, |a| { let execute = WasmMsg::Execute { contract_addr: a.into_string(), - msg: to_binary(&msg)?, + msg: to_json_binary(&msg)?, funds: vec![], }; Ok(SubMsg::new(execute)) @@ -285,19 +286,28 @@ where // bizare has happened. In that event, this message errors // which ought to cause the proposal module to remove this // module and open proposal submission to anyone. - if new_status != Status::Closed && new_status != Status::Executed { - return Err(PreProposeError::NotClosedOrExecuted { status: new_status }); + if new_status != Status::Closed + && new_status != Status::Executed + && new_status != Status::Vetoed + { + return Err(PreProposeError::NotCompleted { status: new_status }); } match self.deposits.may_load(deps.storage, id)? { Some((deposit_info, proposer)) => { let messages = if let Some(ref deposit_info) = deposit_info { - // Refund can be issued if proposal if it is going to - // closed or executed. - let should_refund_to_proposer = (new_status == Status::Closed - && deposit_info.refund_policy == DepositRefundPolicy::Always) - || (new_status == Status::Executed - && deposit_info.refund_policy != DepositRefundPolicy::Never); + // Determine if refund can be issued + let should_refund_to_proposer = + match (new_status, deposit_info.clone().refund_policy) { + // If policy is refund only passed props, refund for executed status + (Status::Executed, DepositRefundPolicy::OnlyPassed) => true, + // Don't refund other statuses for OnlyPassed policy + (_, DepositRefundPolicy::OnlyPassed) => false, + // Refund if the refund policy is always refund + (_, DepositRefundPolicy::Always) => true, + // Don't refund if the refund is never refund + (_, DepositRefundPolicy::Never) => false, + }; if should_refund_to_proposer { deposit_info.get_return_deposit_message(&proposer)? @@ -314,7 +324,7 @@ where Ok(Response::default() .add_attribute("method", "execute_proposal_completed_hook") .add_attribute("proposal", id.to_string()) - .add_attribute("deposit_info", to_binary(&deposit_info)?.to_string()) + .add_attribute("deposit_info", to_json_binary(&deposit_info)?.to_string()) .add_messages(messages)) } @@ -349,18 +359,20 @@ where pub fn query(&self, deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::ProposalModule {} => to_binary(&self.proposal_module.load(deps.storage)?), - QueryMsg::Dao {} => to_binary(&self.dao.load(deps.storage)?), - QueryMsg::Config {} => to_binary(&self.config.load(deps.storage)?), + QueryMsg::ProposalModule {} => { + to_json_binary(&self.proposal_module.load(deps.storage)?) + } + QueryMsg::Dao {} => to_json_binary(&self.dao.load(deps.storage)?), + QueryMsg::Config {} => to_json_binary(&self.config.load(deps.storage)?), QueryMsg::DepositInfo { proposal_id } => { let (deposit_info, proposer) = self.deposits.load(deps.storage, proposal_id)?; - to_binary(&DepositInfoResponse { + to_json_binary(&DepositInfoResponse { deposit_info, proposer, }) } QueryMsg::ProposalSubmittedHooks {} => { - to_binary(&self.proposal_submitted_hooks.query_hooks(deps)?) + to_json_binary(&self.proposal_submitted_hooks.query_hooks(deps)?) } QueryMsg::QueryExtension { .. } => Ok(Binary::default()), } diff --git a/packages/dao-pre-propose-base/src/tests.rs b/packages/dao-pre-propose-base/src/tests.rs index f04c6ed5a..a6b12a74d 100644 --- a/packages/dao-pre-propose-base/src/tests.rs +++ b/packages/dao-pre-propose-base/src/tests.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{ - from_binary, + from_json, testing::{mock_dependencies, mock_env, mock_info}, - to_binary, Addr, Binary, ContractResult, Empty, Response, SubMsg, WasmMsg, + to_json_binary, Addr, Binary, ContractResult, Empty, Response, SubMsg, WasmMsg, }; use cw_hooks::HooksResponse; use dao_voting::status::Status; @@ -38,7 +38,7 @@ fn test_completed_hook_status_invariant() { assert_eq!( res.unwrap_err(), - PreProposeError::NotClosedOrExecuted { + PreProposeError::NotCompleted { status: Status::Passed } ); @@ -97,8 +97,8 @@ fn test_proposal_submitted_hooks() { module .execute_add_proposal_submitted_hook(deps.as_mut(), info, "one".to_string()) .unwrap(); - let hooks: HooksResponse = from_binary( - &module + let hooks: HooksResponse = from_json( + module .query( deps.as_ref(), mock_env(), @@ -118,7 +118,7 @@ fn test_proposal_submitted_hooks() { deps.querier.update_wasm(|_| { // for responding to the next proposal ID query that gets fired by propose. - cosmwasm_std::SystemResult::Ok(ContractResult::Ok(to_binary(&1u64).unwrap())) + cosmwasm_std::SystemResult::Ok(ContractResult::Ok(to_json_binary(&1u64).unwrap())) }); // The hooks fire when a proposal is created. @@ -136,7 +136,7 @@ fn test_proposal_submitted_hooks() { res.messages[1], SubMsg::new(WasmMsg::Execute { contract_addr: "one".to_string(), - msg: to_binary(&Empty::default()).unwrap(), + msg: to_json_binary(&Empty::default()).unwrap(), funds: vec![], }) ); @@ -153,8 +153,8 @@ fn test_proposal_submitted_hooks() { module .execute_remove_proposal_submitted_hook(deps.as_mut(), info, "one".to_string()) .unwrap(); - let hooks: HooksResponse = from_binary( - &module + let hooks: HooksResponse = from_json( + module .query( deps.as_ref(), mock_env(), diff --git a/packages/dao-proposal-hooks/README.md b/packages/dao-proposal-hooks/README.md deleted file mode 100644 index 2ab9ba5bb..000000000 --- a/packages/dao-proposal-hooks/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# CosmWasm DAO Proposal Hooks - -This package provides an interface for managing and dispatching -proposal hooks from a proposal module. - -There are two types of proposal hooks: -- **New Proposal Hook:** fired when a new proposal is created. -- **Proposal Staus Changed Hook:** fired when a proposal's status changes. - -Our wiki contains more info on [Proposal Hooks](https://github.com/DA0-DA0/dao-contracts/wiki/Proposal-Hooks-Interactions). diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index b53e2d15e..677709d53 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,12 @@ 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 } +token-bindings = { workspace = true } cw-core-v1 = { workspace = true, features = ["library"] } cw-hooks = { workspace = true } @@ -31,17 +41,21 @@ cw-proposal-single-v1 = { workspace = true } 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 } dao-pre-propose-single = { workspace = true } dao-proposal-condorcet = { workspace = true } dao-proposal-single = { workspace = true } +dao-test-custom-factory = { workspace = true } dao-voting = { workspace = true } dao-voting-cw20-balance = { workspace = true } dao-voting-cw20-staked = { workspace = true } dao-voting-cw4 = { workspace = true } dao-voting-cw721-staked = { workspace = true } -dao-voting-native-staked = { workspace = true } +dao-voting-cw721-roles = { workspace = true } +dao-voting-token-staked = { workspace = true } voting-v1 = { workspace = true } stake-cw20-v03 = { workspace = true } diff --git a/packages/dao-testing/src/contracts.rs b/packages/dao-testing/src/contracts.rs index 3f51f9a0a..a0418a48f 100644 --- a/packages/dao-testing/src/contracts.rs +++ b/packages/dao-testing/src/contracts.rs @@ -31,6 +31,15 @@ pub fn cw721_base_contract() -> Box> { Box::new(contract) } +pub fn cw721_roles_contract() -> Box> { + let contract = ContractWrapper::new( + cw721_roles::contract::execute, + cw721_roles::contract::instantiate, + cw721_roles::contract::query, + ); + Box::new(contract) +} + pub fn cw20_stake_contract() -> Box> { let contract = ContractWrapper::new( cw20_stake::contract::execute, @@ -101,9 +110,9 @@ pub fn cw20_balances_voting_contract() -> Box> { pub fn native_staked_balances_voting_contract() -> Box> { let contract = ContractWrapper::new( - dao_voting_native_staked::contract::execute, - dao_voting_native_staked::contract::instantiate, - dao_voting_native_staked::contract::query, + dao_voting_token_staked::contract::execute, + dao_voting_token_staked::contract::instantiate, + dao_voting_token_staked::contract::query, ); Box::new(contract) } @@ -113,7 +122,8 @@ pub fn voting_cw721_staked_contract() -> Box> { dao_voting_cw721_staked::contract::execute, dao_voting_cw721_staked::contract::instantiate, dao_voting_cw721_staked::contract::query, - ); + ) + .with_reply(dao_voting_cw721_staked::contract::reply); Box::new(contract) } @@ -138,6 +148,16 @@ pub fn dao_voting_cw4_contract() -> Box> { Box::new(contract) } +pub fn dao_voting_cw721_roles_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_cw721_roles::contract::execute, + dao_voting_cw721_roles::contract::instantiate, + dao_voting_cw721_roles::contract::query, + ) + .with_reply(dao_voting_cw721_roles::contract::reply); + Box::new(contract) +} + pub fn v1_proposal_single_contract() -> Box> { let contract = ContractWrapper::new( cw_proposal_single_v1::contract::execute, @@ -176,3 +196,13 @@ pub fn stake_cw20_v03_contract() -> Box> { ); Box::new(contract) } + +pub fn dao_test_custom_factory() -> Box> { + let contract = ContractWrapper::new( + dao_test_custom_factory::contract::execute, + dao_test_custom_factory::contract::instantiate, + dao_test_custom_factory::contract::query, + ) + .with_reply(dao_test_custom_factory::contract::reply); + Box::new(contract) +} diff --git a/packages/dao-testing/src/helpers.rs b/packages/dao-testing/src/helpers.rs index c9a24d0c5..b629e7ba0 100644 --- a/packages/dao-testing/src/helpers.rs +++ b/packages/dao-testing/src/helpers.rs @@ -1,9 +1,16 @@ -use cosmwasm_std::{to_binary, Addr, Binary, Empty, Uint128}; +use cosmwasm_std::{to_json_binary, Addr, Binary, Uint128}; use cw20::Cw20Coin; -use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use cw_multi_test::{App, Executor}; use cw_utils::Duration; use dao_interface::state::{Admin, ModuleInstantiateInfo}; -use dao_voting_cw20_staked::msg::ActiveThreshold; +use dao_voting::threshold::ActiveThreshold; +use dao_voting_cw4::msg::GroupContract; + +use crate::contracts::{ + cw20_balances_voting_contract, cw20_base_contract, cw20_stake_contract, + cw20_staked_balances_voting_contract, cw4_group_contract, dao_dao_contract, + dao_voting_cw4_contract, +}; const CREATOR_ADDR: &str = "creator"; @@ -13,9 +20,9 @@ pub fn instantiate_with_cw20_balances_governance( governance_instantiate: Binary, initial_balances: Option>, ) -> Addr { - let cw20_id = app.store_code(cw20_contract()); - let core_id = app.store_code(cw_gov_contract()); - let votemod_id = app.store_code(cw20_balances_voting()); + let cw20_id = app.store_code(cw20_base_contract()); + let core_id = app.store_code(dao_dao_contract()); + let votemod_id = app.store_code(cw20_balances_voting_contract()); let initial_balances = initial_balances.unwrap_or_else(|| { vec![Cw20Coin { @@ -50,7 +57,7 @@ pub fn instantiate_with_cw20_balances_governance( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw20_balance::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_balance::msg::InstantiateMsg { token_info: dao_voting_cw20_balance::msg::TokenInfo::New { code_id: cw20_id, label: "DAO DAO governance token".to_string(), @@ -63,12 +70,14 @@ pub fn instantiate_with_cw20_balances_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: governance_code_id, msg: governance_instantiate, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -114,10 +123,10 @@ pub fn instantiate_with_staked_balances_governance( .collect() }; - let cw20_id = app.store_code(cw20_contract()); - let cw20_stake_id = app.store_code(cw20_stake()); - let staked_balances_voting_id = app.store_code(staked_balances_voting()); - let core_contract_id = app.store_code(cw_gov_contract()); + let cw20_id = app.store_code(cw20_base_contract()); + let cw20_stake_id = app.store_code(cw20_stake_contract()); + let staked_balances_voting_id = app.store_code(cw20_staked_balances_voting_contract()); + let core_contract_id = app.store_code(dao_dao_contract()); let instantiate_core = dao_interface::msg::InstantiateMsg { dao_uri: None, @@ -129,7 +138,7 @@ pub fn instantiate_with_staked_balances_governance( automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: staked_balances_voting_id, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { active_threshold: None, token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: cw20_id, @@ -146,6 +155,7 @@ pub fn instantiate_with_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { @@ -153,6 +163,7 @@ pub fn instantiate_with_staked_balances_governance( label: "DAO DAO governance module.".to_string(), admin: Some(Admin::CoreModule {}), msg: governance_instantiate, + funds: vec![], }], initial_items: None, }; @@ -200,7 +211,7 @@ pub fn instantiate_with_staked_balances_governance( &cw20::Cw20ExecuteMsg::Send { contract: staking_contract.to_string(), amount, - msg: to_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), + msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }, &[], ) @@ -220,10 +231,10 @@ pub fn instantiate_with_staking_active_threshold( initial_balances: Option>, active_threshold: Option, ) -> Addr { - let cw20_id = app.store_code(cw20_contract()); + let cw20_id = app.store_code(cw20_base_contract()); let cw20_staking_id = app.store_code(cw20_stake_contract()); - let governance_id = app.store_code(cw_gov_contract()); - let votemod_id = app.store_code(cw20_staked_balances_voting()); + let governance_id = app.store_code(dao_dao_contract()); + let votemod_id = app.store_code(cw20_staked_balances_voting_contract()); let initial_balances = initial_balances.unwrap_or_else(|| { vec![ @@ -248,7 +259,7 @@ pub fn instantiate_with_staking_active_threshold( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { + msg: to_json_binary(&dao_voting_cw20_staked::msg::InstantiateMsg { token_info: dao_voting_cw20_staked::msg::TokenInfo::New { code_id: cw20_id, label: "DAO DAO governance token".to_string(), @@ -265,12 +276,14 @@ pub fn instantiate_with_staking_active_threshold( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id, msg: governance_instantiate, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -293,9 +306,9 @@ pub fn instantiate_with_cw4_groups_governance( proposal_module_instantiate: Binary, initial_weights: Option>, ) -> Addr { - let cw4_id = app.store_code(cw4_contract()); - let core_id = app.store_code(cw_gov_contract()); - let votemod_id = app.store_code(cw4_voting_contract()); + let cw4_id = app.store_code(cw4_group_contract()); + let core_id = app.store_code(dao_dao_contract()); + let votemod_id = app.store_code(dao_voting_cw4_contract()); let initial_weights = initial_weights.unwrap_or_default(); @@ -329,18 +342,22 @@ pub fn instantiate_with_cw4_groups_governance( automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: votemod_id, - msg: to_binary(&dao_voting_cw4::msg::InstantiateMsg { - cw4_group_code_id: cw4_id, - initial_members: initial_weights, + msg: to_json_binary(&dao_voting_cw4::msg::InstantiateMsg { + group_contract: GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: initial_weights, + }, }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: core_code_id, msg: proposal_module_instantiate, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -362,89 +379,3 @@ pub fn instantiate_with_cw4_groups_governance( addr } - -pub fn cw20_contract() -> Box> { - let contract = ContractWrapper::new( - cw20_base::contract::execute, - cw20_base::contract::instantiate, - cw20_base::contract::query, - ); - Box::new(contract) -} - -pub fn cw20_stake_contract() -> Box> { - let contract = ContractWrapper::new( - cw20_stake::contract::execute, - cw20_stake::contract::instantiate, - cw20_stake::contract::query, - ); - Box::new(contract) -} - -pub fn cw20_balances_voting() -> Box> { - let contract = ContractWrapper::new( - dao_voting_cw20_balance::contract::execute, - dao_voting_cw20_balance::contract::instantiate, - dao_voting_cw20_balance::contract::query, - ) - .with_reply(dao_voting_cw20_balance::contract::reply); - Box::new(contract) -} - -fn cw20_staked_balances_voting() -> Box> { - let contract = ContractWrapper::new( - dao_voting_cw20_staked::contract::execute, - dao_voting_cw20_staked::contract::instantiate, - dao_voting_cw20_staked::contract::query, - ) - .with_reply(dao_voting_cw20_staked::contract::reply); - Box::new(contract) -} - -fn cw_gov_contract() -> Box> { - let contract = ContractWrapper::new( - dao_dao_core::contract::execute, - dao_dao_core::contract::instantiate, - dao_dao_core::contract::query, - ) - .with_reply(dao_dao_core::contract::reply); - Box::new(contract) -} - -fn staked_balances_voting() -> Box> { - let contract = ContractWrapper::new( - dao_voting_cw20_staked::contract::execute, - dao_voting_cw20_staked::contract::instantiate, - dao_voting_cw20_staked::contract::query, - ) - .with_reply(dao_voting_cw20_staked::contract::reply); - Box::new(contract) -} - -fn cw20_stake() -> Box> { - let contract = ContractWrapper::new( - cw20_stake::contract::execute, - cw20_stake::contract::instantiate, - cw20_stake::contract::query, - ); - Box::new(contract) -} - -fn cw4_contract() -> Box> { - let contract = ContractWrapper::new( - cw4_group::contract::execute, - cw4_group::contract::instantiate, - cw4_group::contract::query, - ); - Box::new(contract) -} - -fn cw4_voting_contract() -> Box> { - let contract = ContractWrapper::new( - dao_voting_cw4::contract::execute, - dao_voting_cw4::contract::instantiate, - dao_voting_cw4::contract::query, - ) - .with_reply(dao_voting_cw4::contract::reply); - Box::new(contract) -} 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/cw721_base.rs b/packages/dao-testing/src/test_tube/cw721_base.rs new file mode 100644 index 000000000..84c80b8fe --- /dev/null +++ b/packages/dao-testing/src/test_tube/cw721_base.rs @@ -0,0 +1,128 @@ +use cosmwasm_std::{Coin, Empty}; +use cw721_base::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct Cw721Base<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> Cw721Base<'a> { + pub fn new( + 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 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) + } + + 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("cw721_base.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("cw721_base-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/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/dao_dao_core.rs b/packages/dao-testing/src/test_tube/dao_dao_core.rs new file mode 100644 index 000000000..3fc9b73e7 --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_dao_core.rs @@ -0,0 +1,126 @@ +use cosmwasm_std::Coin; +use dao_dao_core::ContractError; +use dao_interface::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct DaoCore<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> DaoCore<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + funds: &[Coin], + ) -> 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, + funds, + 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) + } + + 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("dao_dao_core.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("dao_dao_core-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/dao_proposal_single.rs b/packages/dao-testing/src/test_tube/dao_proposal_single.rs new file mode 100644 index 000000000..4f6d51a96 --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_proposal_single.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::Coin; +use dao_proposal_single::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct DaoProposalSingle<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> DaoProposalSingle<'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) + } + + 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("dao_proposal_single.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("dao_proposal_single-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/dao_test_custom_factory.rs b/packages/dao-testing/src/test_tube/dao_test_custom_factory.rs new file mode 100644 index 000000000..754374884 --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_test_custom_factory.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::Coin; +use dao_test_custom_factory::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct CustomFactoryContract<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> CustomFactoryContract<'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) + } + + 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("dao_test_custom_factory.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("dao_test_custom_factory-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..0af2999a4 --- /dev/null +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -0,0 +1,21 @@ +// 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; + +#[cfg(feature = "test-tube")] +pub mod cw721_base; + +#[cfg(feature = "test-tube")] +pub mod dao_dao_core; + +#[cfg(feature = "test-tube")] +pub mod dao_proposal_single; + +#[cfg(feature = "test-tube")] +pub mod dao_test_custom_factory; diff --git a/packages/dao-vote-hooks/README.md b/packages/dao-vote-hooks/README.md deleted file mode 100644 index ae2be9cca..000000000 --- a/packages/dao-vote-hooks/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# CosmWasm DAO Vote Hooks - -This package provides an interface for managing and dispatching -vote hooks from a proposal module. Vote hooks are fired when new -votes are cast. - -You can read more about vote hooks in our [wiki](https://github.com/DA0-DA0/dao-contracts/wiki/Proposal-Hooks-Interactions). diff --git a/packages/dao-voting/src/deposit.rs b/packages/dao-voting/src/deposit.rs index bf0852ea8..33fbb832c 100644 --- a/packages/dao-voting/src/deposit.rs +++ b/packages/dao-voting/src/deposit.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - to_binary, Addr, CosmosMsg, Deps, MessageInfo, StdError, StdResult, Uint128, WasmMsg, + to_json_binary, Addr, CosmosMsg, Deps, MessageInfo, StdError, StdResult, Uint128, WasmMsg, }; use cw_utils::{must_pay, PaymentError}; @@ -170,7 +170,7 @@ impl CheckedDepositInfo { vec![WasmMsg::Execute { contract_addr: address.to_string(), funds: vec![], - msg: to_binary(&cw20::Cw20ExecuteMsg::TransferFrom { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::TransferFrom { owner: depositor.to_string(), recipient: contract.to_string(), amount: *amount, @@ -312,7 +312,7 @@ pub mod tests { messages, vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: CW20.to_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::TransferFrom { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::TransferFrom { owner: "ekez".to_string(), recipient: "contract".to_string(), amount: Uint128::new(10) @@ -371,7 +371,7 @@ pub mod tests { messages, vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: CW20.to_string(), - msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer { + msg: to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { recipient: "ekez".to_string(), amount: Uint128::new(10) }) diff --git a/packages/dao-voting/src/duration.rs b/packages/dao-voting/src/duration.rs new file mode 100644 index 000000000..bea32880d --- /dev/null +++ b/packages/dao-voting/src/duration.rs @@ -0,0 +1,26 @@ +use cw_utils::Duration; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum UnstakingDurationError { + #[error("Invalid unstaking duration, unstaking duration cannot be 0")] + InvalidUnstakingDuration {}, +} + +pub fn validate_duration(duration: Option) -> Result<(), UnstakingDurationError> { + if let Some(unstaking_duration) = duration { + match unstaking_duration { + Duration::Height(height) => { + if height == 0 { + return Err(UnstakingDurationError::InvalidUnstakingDuration {}); + } + } + Duration::Time(time) => { + if time == 0 { + return Err(UnstakingDurationError::InvalidUnstakingDuration {}); + } + } + } + } + Ok(()) +} diff --git a/packages/dao-voting/src/lib.rs b/packages/dao-voting/src/lib.rs index 208cfc468..747274849 100644 --- a/packages/dao-voting/src/lib.rs +++ b/packages/dao-voting/src/lib.rs @@ -1,6 +1,7 @@ #![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] pub mod deposit; +pub mod duration; pub mod error; pub mod multiple_choice; pub mod pre_propose; @@ -8,4 +9,5 @@ pub mod proposal; pub mod reply; pub mod status; pub mod threshold; +pub mod veto; pub mod voting; diff --git a/packages/dao-voting/src/pre_propose.rs b/packages/dao-voting/src/pre_propose.rs index 52bd3e7a5..482bf9260 100644 --- a/packages/dao-voting/src/pre_propose.rs +++ b/packages/dao-voting/src/pre_propose.rs @@ -60,7 +60,7 @@ impl PreProposeInfo { #[cfg(test)] mod tests { - use cosmwasm_std::{to_binary, WasmMsg}; + use cosmwasm_std::{to_json_binary, WasmMsg}; use super::*; @@ -114,8 +114,9 @@ mod tests { let info = PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: 42, - msg: to_binary("foo").unwrap(), + msg: to_json_binary("foo").unwrap(), admin: None, + funds: vec![], label: "pre-propose-9000".to_string(), }, }; @@ -135,7 +136,7 @@ mod tests { WasmMsg::Instantiate { admin: None, code_id: 42, - msg: to_binary("foo").unwrap(), + msg: to_json_binary("foo").unwrap(), funds: vec![], label: "pre-propose-9000".to_string() }, diff --git a/packages/dao-voting/src/status.rs b/packages/dao-voting/src/status.rs index 3b75af978..b2881faaa 100644 --- a/packages/dao-voting/src/status.rs +++ b/packages/dao-voting/src/status.rs @@ -1,4 +1,5 @@ use cosmwasm_schema::cw_serde; +use cw_utils::Expiration; #[cw_serde] #[derive(Copy)] @@ -16,6 +17,11 @@ pub enum Status { Closed, /// The proposal's execution failed. ExecutionFailed, + /// The proposal is timelocked. Only the configured vetoer + /// can execute or veto until the timelock expires. + VetoTimelock { expiration: Expiration }, + /// The proposal has been vetoed. + Vetoed, } impl std::fmt::Display for Status { @@ -27,6 +33,10 @@ impl std::fmt::Display for Status { Status::Executed => write!(f, "executed"), Status::Closed => write!(f, "closed"), Status::ExecutionFailed => write!(f, "execution_failed"), + Status::VetoTimelock { expiration } => { + write!(f, "veto_timelock_until_{:?}", expiration) + } + Status::Vetoed => write!(f, "vetoed"), } } } diff --git a/packages/dao-voting/src/threshold.rs b/packages/dao-voting/src/threshold.rs index f1447b83e..bbb2668cb 100644 --- a/packages/dao-voting/src/threshold.rs +++ b/packages/dao-voting/src/threshold.rs @@ -3,6 +3,58 @@ use cosmwasm_std::{Decimal, Uint128}; use thiserror::Error; +/// 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. +#[cw_serde] +pub enum ActiveThreshold { + /// The absolute number of tokens that must be staked for the + /// module to be active. + AbsoluteCount { count: Uint128 }, + /// The percentage of tokens that must be staked for the module to + /// be active. Computed as `staked / total_supply`. + Percentage { percent: Decimal }, +} + +#[cw_serde] +pub struct ActiveThresholdResponse { + pub active_threshold: Option, +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum ActiveThresholdError { + #[error("Absolute count threshold cannot be greater than the total token supply")] + InvalidAbsoluteCount {}, + + #[error("Active threshold percentage must be greater than 0 and not greater than 1")] + InvalidActivePercentage {}, + + #[error("Active threshold count must be greater than zero")] + ZeroActiveCount {}, +} + +pub fn assert_valid_absolute_count_threshold( + count: Uint128, + supply: Uint128, +) -> Result<(), ActiveThresholdError> { + if count.is_zero() { + return Err(ActiveThresholdError::ZeroActiveCount {}); + } + if count > supply { + return Err(ActiveThresholdError::InvalidAbsoluteCount {}); + } + Ok(()) +} + +pub fn assert_valid_percentage_threshold(percent: Decimal) -> Result<(), ActiveThresholdError> { + if percent.is_zero() || percent > Decimal::one() { + return Err(ActiveThresholdError::InvalidActivePercentage {}); + } + Ok(()) +} + #[derive(Error, Debug, PartialEq, Eq)] pub enum ThresholdError { #[error("Required threshold cannot be zero")] @@ -18,7 +70,7 @@ pub enum ThresholdError { /// If a user specifies a 60% passing threshold, and there are 10 /// voters they likely expect that proposal to pass when there are 6 /// yes votes. This implies that the condition for passing should be -/// `yes_votes >= total_votes * threshold`. +/// `vote_weights >= total_votes * threshold`. /// /// With this in mind, how should a user specify that they would like /// proposals to pass if the majority of voters choose yes? Selecting diff --git a/packages/dao-voting/src/veto.rs b/packages/dao-voting/src/veto.rs new file mode 100644 index 000000000..fe5b77946 --- /dev/null +++ b/packages/dao-voting/src/veto.rs @@ -0,0 +1,91 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Deps, MessageInfo, StdError}; +use cw_utils::Duration; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum VetoError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Proposal is {status} and thus is unable to be vetoed.")] + InvalidProposalStatus { status: String }, + + #[error("Early execution for timelocked proposals is not enabled. Proposal can not be executed before the timelock delay has expired.")] + NoEarlyExecute {}, + + #[error("Veto is not enabled for this contract.")] + NoVetoConfiguration {}, + + #[error("Vetoing before a proposal passes is not enabled.")] + NoVetoBeforePassed {}, + + #[error("The proposal is timelocked and cannot be executed.")] + Timelocked {}, + + #[error("The veto timelock duration has expired.")] + TimelockExpired {}, + + #[error("The veto timelock duration must have the same units as the max_voting_period of the proposal (height or time).")] + TimelockDurationUnitMismatch {}, + + #[error("Only vetoer can veto a proposal.")] + Unauthorized {}, +} + +#[cw_serde] +pub struct VetoConfig { + /// The time duration to lock a proposal for after its expiration to allow + /// the vetoer to veto. + pub timelock_duration: Duration, + /// The address able to veto proposals. + pub vetoer: String, + /// Whether or not the vetoer can execute a proposal early before the + /// timelock duration has expired + pub early_execute: bool, + /// Whether or not the vetoer can veto a proposal before it passes. + pub veto_before_passed: bool, +} + +impl VetoConfig { + pub fn validate(&self, deps: &Deps, max_voting_period: &Duration) -> Result<(), VetoError> { + // Validate vetoer address. + deps.api.addr_validate(&self.vetoer)?; + + // Validate duration units match voting period. + match (self.timelock_duration, max_voting_period) { + (Duration::Time(_), Duration::Time(_)) => (), + (Duration::Height(_), Duration::Height(_)) => (), + _ => return Err(VetoError::TimelockDurationUnitMismatch {}), + }; + + Ok(()) + } + + /// Whether early execute is enabled + pub fn check_early_execute_enabled(&self) -> Result<(), VetoError> { + if self.early_execute { + Ok(()) + } else { + Err(VetoError::NoEarlyExecute {}) + } + } + + /// Checks whether the message sender is the vetoer. + pub fn check_is_vetoer(&self, info: &MessageInfo) -> Result<(), VetoError> { + if self.vetoer == info.sender { + Ok(()) + } else { + Err(VetoError::Unauthorized {}) + } + } + + /// Checks whether veto_before_passed is enabled, errors if not + pub fn check_veto_before_passed_enabled(&self) -> Result<(), VetoError> { + if self.veto_before_passed { + Ok(()) + } else { + Err(VetoError::NoVetoBeforePassed {}) + } + } +} diff --git a/packages/dao-voting/src/voting.rs b/packages/dao-voting/src/voting.rs index e8d0afd18..32f1e1e46 100644 --- a/packages/dao-voting/src/voting.rs +++ b/packages/dao-voting/src/voting.rs @@ -87,7 +87,7 @@ pub fn compare_vote_count( } pub fn does_vote_count_pass( - yes_votes: Uint128, + vote_weights: Uint128, options: Uint128, percent: PercentageThreshold, ) -> bool { @@ -96,9 +96,9 @@ pub fn does_vote_count_pass( return false; } match percent { - PercentageThreshold::Majority {} => yes_votes.full_mul(2u64) > options.into(), + PercentageThreshold::Majority {} => vote_weights.full_mul(2u64) > options.into(), PercentageThreshold::Percent(percent) => { - compare_vote_count(yes_votes, VoteCmp::Geq, options, percent) + compare_vote_count(vote_weights, VoteCmp::Geq, options, percent) } } } diff --git a/scripts/publish.sh b/scripts/publish.sh index ba42f086e..9b42ab909 100644 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -55,6 +55,10 @@ cd packages/cw721-controllers cargo publish cd "$START_DIR" +cd packages/dao-cw721-extensions +cargo hack publish --no-dev-deps --allow-dirty +cd "$START_DIR" + cd packages/dao-interface cargo publish cd "$START_DIR" @@ -67,36 +71,40 @@ cd packages/dao-voting cargo publish cd "$START_DIR" -cd packages/dao-vote-hooks +cd packages/dao-hooks cargo publish cd "$START_DIR" sleep 120 -cd packages/dao-proposal-hooks -cargo publish -cd "$START_DIR" - cd packages/dao-pre-propose-base cargo publish cd "$START_DIR" -# Test contracts -cd test-contracts/dao-proposal-sudo +Test contracts +cd contracts/test/dao-proposal-sudo cargo publish cd "$START_DIR" -cd test-contracts/dao-voting-cw20-balance +cd contracts/test/dao-voting-cw20-balance cargo publish cd "$START_DIR" -cd test-contracts/dao-proposal-hook-counter +cd contracts/test/dao-proposal-hook-counter cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" sleep 120 # Contracts +cd contracts/external/cw-tokenfactory-issuer +cargo hack publish --no-dev-deps --allow-dirty +cd "$START_DIR" + +cd contracts/test/dao-test-custom-factory +cargo hack publish --no-dev-deps --allow-dirty +cd "$START_DIR" + cd contracts/external/cw-token-swap cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" @@ -109,6 +117,10 @@ cd contracts/external/cw-payroll-factory cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" +cd contracts/external/cw721-roles +cargo hack publish --no-dev-deps --allow-dirty +cd "$START_DIR" + cd contracts/pre-propose/dao-pre-propose-single cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" @@ -163,11 +175,15 @@ cd contracts/voting/dao-voting-cw20-staked cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" +cd contracts/voting/dao-voting-cw721-roles +cargo hack publish --no-dev-deps --allow-dirty +cd "$START_DIR" + cd contracts/voting/dao-voting-cw721-staked cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" -cd contracts/voting/dao-voting-native-staked +cd contracts/voting/dao-voting-token-staked cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" @@ -190,6 +206,7 @@ cd contracts/external/dao-migrator cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" + cd packages/dao-testing cargo publish cd "$START_DIR" diff --git a/test-contracts/dao-proposal-hook-counter/.gitignore b/test-contracts/dao-proposal-hook-counter/.gitignore deleted file mode 100644 index dfdaaa6bc..000000000 --- a/test-contracts/dao-proposal-hook-counter/.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/test-contracts/dao-proposal-sudo/.gitignore b/test-contracts/dao-proposal-sudo/.gitignore deleted file mode 100644 index dfdaaa6bc..000000000 --- a/test-contracts/dao-proposal-sudo/.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/test-contracts/dao-voting-cw20-balance/.gitignore b/test-contracts/dao-voting-cw20-balance/.gitignore deleted file mode 100644 index dfdaaa6bc..000000000 --- a/test-contracts/dao-voting-cw20-balance/.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