diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 47afc4655..854840036 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -9,27 +9,26 @@ env: jobs: build: - runs-on: ubuntu-latest steps: - - 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 + - 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. + token: ${{ secrets.CODECOV_TOKEN }} + # 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 42af0ab81..66119a9f8 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-2023-02-02 + toolchain: nightly target: wasm32-unknown-unknown override: true diff --git a/Cargo.lock b/Cargo.lock index e5bf0491f..48a48bbbe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ "hex", "itertools 0.12.1", "log", - "prost 0.12.6", + "prost 0.12.3", "schemars", "serde", "serde_json", @@ -43,7 +43,7 @@ dependencies = [ "abstract-cw4-group", "abstract-cw4-stake", "cosmwasm-std", - "cw-orch", + "cw-orch 0.22.2", ] [[package]] @@ -69,7 +69,7 @@ dependencies = [ "abstract-cw2", "cosmwasm-schema", "cosmwasm-std", - "cw-orch", + "cw-orch 0.22.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "schemars", @@ -88,7 +88,7 @@ dependencies = [ "abstract-cw2", "cosmwasm-schema", "cosmwasm-std", - "cw-orch", + "cw-orch 0.22.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "schemars", @@ -118,7 +118,7 @@ checksum = "00d5e4b8084c3a2b3e42502e6c4fe3ed985dc72e86eb612bcc527f4a0443fa42" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-orch", + "cw-orch 0.22.2", "cw-utils 1.0.3", "schemars", "serde", @@ -134,7 +134,7 @@ dependencies = [ "abstract-cw20", "cosmwasm-schema", "cosmwasm-std", - "cw-orch", + "cw-orch 0.22.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "schemars", @@ -154,7 +154,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", - "cw-orch", + "cw-orch 0.22.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "schemars", @@ -188,7 +188,7 @@ dependencies = [ "abstract-cw3", "cosmwasm-schema", "cosmwasm-std", - "cw-orch", + "cw-orch 0.22.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "schemars", @@ -209,7 +209,7 @@ dependencies = [ "abstract-cw4", "cosmwasm-schema", "cosmwasm-std", - "cw-orch", + "cw-orch 0.22.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "schemars", @@ -241,7 +241,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", - "cw-orch", + "cw-orch 0.22.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "schemars", @@ -261,7 +261,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", - "cw-orch", + "cw-orch 0.22.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "schemars", @@ -304,6 +304,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -316,6 +331,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -335,7 +361,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -346,7 +372,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -415,6 +441,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + [[package]] name = "base16ct" version = "0.1.1" @@ -439,6 +471,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -476,7 +514,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.71", + "syn 2.0.72", "which", ] @@ -486,7 +524,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b30ed1d6f8437a487a266c8293aeb95b61a23261273e3e02912cdb8b68bf798b" dependencies = [ - "bs58", + "bs58 0.4.0", "hmac", "k256 0.11.6", "once_cell", @@ -498,6 +536,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bip32" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa13fae8b6255872fd86f7faf4b41168661d7d78609f7bfe6771b85c6739a15b" +dependencies = [ + "bs58 0.5.1", + "hmac", + "k256 0.13.3", + "rand_core 0.6.4", + "ripemd", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "bitcoin" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" +dependencies = [ + "bech32 0.9.1", + "bitcoin-private", + "bitcoin_hashes", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -544,17 +626,18 @@ dependencies = [ "cw-admin-factory", "cw-utils 1.0.3", "cw20 1.1.2", - "cw20-stake 2.4.2", - "dao-dao-core", - "dao-interface", - "dao-pre-propose-single", - "dao-proposal-single", - "dao-voting 2.4.2", + "cw20-stake 2.5.0", + "dao-dao-core 2.5.0", + "dao-interface 2.5.0", + "dao-pre-propose-single 2.5.0", + "dao-proposal-single 2.5.0", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "env_logger", "serde", "serde_json", "serde_yaml", + "tokio", ] [[package]] @@ -566,6 +649,42 @@ dependencies = [ "sha2 0.9.9", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.8", +] + +[[package]] +name = "btsg-ft-factory" +version = "2.5.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-orch 0.24.1", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-dao-core 2.5.0", + "dao-interface 2.5.0", + "dao-proposal-single 2.5.0", + "dao-testing", + "dao-voting 2.5.0", + "dao-voting-token-staked", + "osmosis-std-derive", + "prost 0.12.3", + "prost-derive 0.12.3", + "prost-types 0.12.3", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -580,18 +699,18 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.1.6" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "504bdec147f2cc13c8b57ed9401fd8a147cc66b67ad5cb241394244f2c947549" [[package]] name = "cexpr" @@ -614,7 +733,12 @@ version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", ] [[package]] @@ -745,14 +869,26 @@ dependencies = [ "tonic 0.9.2", ] +[[package]] +name = "cosmos-sdk-proto" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" +dependencies = [ + "prost 0.12.3", + "prost-types 0.12.3", + "tendermint-proto 0.34.1", + "tonic 0.10.2", +] + [[package]] name = "cosmos-sdk-proto" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82e23f6ab56d5f031cde05b8b82a5fefd3a1a223595c79e32317a97189e612bc" dependencies = [ - "prost 0.12.6", - "prost-types 0.12.6", + "prost 0.12.3", + "prost-types 0.12.3", "tendermint-proto 0.35.0", ] @@ -762,7 +898,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3903590099dcf1ea580d9353034c9ba1dbf55d1389a5bd2ade98535c3445d1f9" dependencies = [ - "bip32", + "bip32 0.4.0", "cosmos-sdk-proto 0.14.0", "ecdsa 0.14.8", "eyre", @@ -783,7 +919,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fa07096219b1817432b8f1e47c22e928c64bbfd231fc08f0a98f0e7ddd602b7" dependencies = [ - "bip32", + "bip32 0.4.0", "cosmos-sdk-proto 0.15.0", "ecdsa 0.14.8", "eyre", @@ -798,34 +934,55 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cosmrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" +dependencies = [ + "bip32 0.5.2", + "cosmos-sdk-proto 0.20.0", + "ecdsa 0.16.9", + "eyre", + "k256 0.13.3", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature 2.2.0", + "subtle-encoding", + "tendermint 0.34.1", + "tendermint-rpc 0.34.1", + "thiserror", + "tokio", +] + [[package]] name = "cosmwasm-crypto" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd50718a2b6830ce9eb5d465de5a018a12e71729d66b70807ce97e6dd14f931d" +checksum = "0f862b355f7e47711e0acfe6af92cb3fd8fd5936b66a9eaa338b51edabd1e77d" dependencies = [ "digest 0.10.7", - "ecdsa 0.16.9", "ed25519-zebra", - "k256 0.13.1", + "k256 0.13.3", "rand_core 0.6.4", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "242e98e7a231c122e08f300d9db3262d1007b51758a8732cd6210b3e9faa4f3a" +checksum = "cd85de6467cd1073688c86b39833679ae6db18cf4771471edd9809f15f1679f1" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7879036156092ad1c22fe0d7316efc5a5eceec2bc3906462a2560215f2a2f929" +checksum = "5b4cd28147a66eba73720b47636a58097a979ad8c8bfdb4ed437ebcbfe362576" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -836,9 +993,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb57855fbfc83327f8445ae0d413b1a05ac0d68c396ab4d122b2abd7bb82cb6" +checksum = "9acd45c63d41bc9b16bc6dc7f6bd604a8c2ad29ce96c8f3c96d7fc8ef384392e" dependencies = [ "proc-macro2", "quote", @@ -847,9 +1004,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c1556156fdf892a55cced6115968b961eaaadd6f724a2c2cb7d1e168e32dd3" +checksum = "2685c2182624b2e9e17f7596192de49a3f86b7a0c9a5f6b25c1df5e24592e836" dependencies = [ "base64 0.21.7", "bech32 0.9.1", @@ -886,6 +1043,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-bigint" version = "0.4.9" @@ -926,7 +1092,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" dependencies = [ - "sct", + "sct 0.6.1", ] [[package]] @@ -942,6 +1108,46 @@ dependencies = [ "zeroize", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + [[package]] name = "cw-address-like" version = "1.0.4" @@ -953,17 +1159,26 @@ dependencies = [ [[package]] name = "cw-admin-factory" -version = "2.4.2" +version = "2.5.0" dependencies = [ + "bech32 0.9.1", "cosmwasm-schema", "cosmwasm-std", + "cw-admin-factory", "cw-multi-test", + "cw-orch 0.24.1", "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", + "cw4 1.1.2", + "dao-dao-core 2.5.0", + "dao-interface 2.5.0", + "dao-proposal-single 2.5.0", + "dao-testing", + "dao-voting 2.5.0", + "dao-voting-cw4 2.5.0", + "osmosis-test-tube", "thiserror", ] @@ -1079,7 +1294,19 @@ dependencies = [ [[package]] name = "cw-denom" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decaa8be7ffa8090dc62d8bb8ee97cd3f41f815a41ba08de1d40cacef6c3cb4b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw20 1.1.2", + "thiserror", +] + +[[package]] +name = "cw-denom" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1091,20 +1318,21 @@ dependencies = [ [[package]] name = "cw-fund-distributor" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-paginate-storage 2.4.2", + "cw-orch 0.24.1", + "cw-paginate-storage 2.5.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.4.2", - "dao-dao-core", - "dao-interface", + "cw20-stake 2.5.0", + "dao-dao-core 2.5.0", + "dao-interface 2.5.0", "dao-voting-cw20-staked", "thiserror", ] @@ -1112,6 +1340,18 @@ dependencies = [ [[package]] name = "cw-hooks" version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "097ee97b99ecc90372eac3bcaf698d940a15f806f2ba1e1e901c729f6523e16e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "thiserror", +] + +[[package]] +name = "cw-hooks" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1121,17 +1361,18 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.18.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e2c2f2c0877b839c5cad85e67811074e854a50c1ff3085eb8290b1c27809c" +checksum = "cc392a5cb7e778e3f90adbf7faa43c4db7f35b6623224b08886d796718edb875" dependencies = [ "anyhow", + "bech32 0.9.1", "cosmwasm-std", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "derivative", - "itertools 0.11.0", - "prost 0.12.6", + "itertools 0.12.1", + "prost 0.12.3", "schemars", "serde", "sha2 0.10.8", @@ -1148,15 +1389,40 @@ dependencies = [ "cosmwasm-std", "cw-orch-contract-derive", "cw-orch-core", - "cw-orch-fns-derive", - "cw-orch-mock", - "cw-orch-traits", + "cw-orch-fns-derive 0.19.1", + "cw-orch-mock 0.22.4", + "cw-orch-traits 0.22.0", + "cw-utils 1.0.3", + "hex", + "log", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-orch" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c81cb500eb2f9be31a0f90c7ce66572ee4a790ffbae1c6b42ff2e3f9faf3479" +dependencies = [ + "anyhow", + "cosmrs 0.15.0", + "cosmwasm-std", + "cw-orch-contract-derive", + "cw-orch-core", + "cw-orch-daemon", + "cw-orch-fns-derive 0.22.0", + "cw-orch-mock 0.23.2", + "cw-orch-networks", + "cw-orch-traits 0.23.3", "cw-utils 1.0.3", "hex", "log", "schemars", "serde", "thiserror", + "tokio", ] [[package]] @@ -1167,14 +1433,14 @@ checksum = "5bc8ba75692fc7bd30e91c78fad2dc208a738e4e6ea26b232f9352c320e35543" dependencies = [ "convert_case", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] name = "cw-orch-core" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81af5ba67fdc434c6e90739ae89b91bff4dc2b87dc85f8a41aa822329c951bf8" +checksum = "5abd640f67f655411542a3c148769270c7a9e63d4097c2dc4a1f6edf23b7a9b4" dependencies = [ "abstract-cw-multi-test", "anyhow", @@ -1190,6 +1456,49 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw-orch-daemon" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e6d5033f1bf9d32a6d1b11dd8e9b9ad5cb673c3621def8fe63fa1310eda520" +dependencies = [ + "anyhow", + "async-recursion", + "base16", + "base64 0.22.1", + "bitcoin", + "chrono", + "cosmrs 0.15.0", + "cosmwasm-std", + "cw-orch-core", + "cw-orch-networks", + "cw-orch-traits 0.23.3", + "dirs", + "ed25519-dalek 2.1.1", + "eyre", + "file-lock", + "flate2", + "hex", + "hkd32", + "lazy_static", + "log", + "once_cell", + "prost 0.12.3", + "prost-types 0.12.3", + "rand_core 0.6.4", + "reqwest", + "ring 0.17.8", + "ripemd", + "schemars", + "serde", + "serde_json", + "sha2 0.10.8", + "thiserror", + "tokio", + "tonic 0.10.2", + "uid", +] + [[package]] name = "cw-orch-fns-derive" version = "0.19.1" @@ -1202,6 +1511,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cw-orch-fns-derive" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e21b23116a0702f540d7fa3f16e8276682d860b589fed56259220ad59d768e" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "cw-orch-mock" version = "0.22.4" @@ -1218,29 +1539,65 @@ dependencies = [ ] [[package]] -name = "cw-orch-traits" -version = "0.22.0" +name = "cw-orch-mock" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5959ce29e9d8a52594b47933a0a2736ea94dd9bf5e29b220cbdbe2b097f07c3a" +checksum = "57beb30d841bde79df51c9402741ef926ca8ef7ecd3570aa180074f767ac04d3" dependencies = [ + "abstract-cw-multi-test", + "cosmwasm-std", "cw-orch-core", - "prost 0.12.6", - "prost-types 0.12.6", + "cw-utils 1.0.3", + "log", + "serde", + "sha2 0.10.8", ] [[package]] -name = "cw-ownable" -version = "0.5.1" +name = "cw-orch-networks" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093dfb4520c48b5848274dd88ea99e280a04bc08729603341c7fb0d758c74321" +checksum = "f6b0e1af56869ced61c25f509a0a40cddd028779d838441b70f4578a8d7c0253" dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-address-like", - "cw-ownable-derive", - "cw-storage-plus 1.2.0", - "cw-utils 1.0.3", - "thiserror", + "cw-orch-core", + "serde", +] + +[[package]] +name = "cw-orch-traits" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5959ce29e9d8a52594b47933a0a2736ea94dd9bf5e29b220cbdbe2b097f07c3a" +dependencies = [ + "cw-orch-core", + "prost 0.12.3", + "prost-types 0.12.3", +] + +[[package]] +name = "cw-orch-traits" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e6b81dc282724c9c6334a499f4867e575458e69fe5b99034d4f962860f3357" +dependencies = [ + "cw-orch-core", + "prost 0.12.3", + "prost-types 0.12.3", +] + +[[package]] +name = "cw-ownable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093dfb4520c48b5848274dd88ea99e280a04bc08729603341c7fb0d758c74321" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-address-like", + "cw-ownable-derive", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "thiserror", ] [[package]] @@ -1269,6 +1626,17 @@ dependencies = [ [[package]] name = "cw-paginate-storage" version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9740c02c59072ad00de64cc60aae6ca492103dcefc0f7d3cc6f77d768f2ec70" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "serde", +] + +[[package]] +name = "cw-paginate-storage" +version = "2.5.0" dependencies = [ "cosmwasm-std", "cw-multi-test", @@ -1278,12 +1646,13 @@ dependencies = [ [[package]] name = "cw-payroll-factory" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-denom", + "cw-denom 2.5.0", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -1322,7 +1691,7 @@ dependencies = [ [[package]] name = "cw-stake-tracker" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1375,11 +1744,12 @@ dependencies = [ [[package]] name = "cw-token-swap" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", @@ -1390,19 +1760,21 @@ dependencies = [ [[package]] name = "cw-tokenfactory-issuer" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "cw-storage-plus 1.2.0", "cw-tokenfactory-types", "cw2 1.1.2", - "dao-interface", + "dao-interface 2.5.0", "osmosis-std", "osmosis-test-tube", - "prost 0.13.1", + "prost 0.12.3", + "prost-derive 0.12.3", "schemars", "serde", "serde_json", @@ -1411,15 +1783,16 @@ dependencies = [ [[package]] name = "cw-tokenfactory-types" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "dao-interface", + "dao-interface 2.5.0", "osmosis-std", "osmosis-std-derive", - "prost 0.13.1", - "prost-types 0.13.1", + "prost 0.12.3", + "prost-derive 0.12.3", + "prost-types 0.12.3", "schemars", "serde", "serde-cw-value", @@ -1481,13 +1854,14 @@ dependencies = [ [[package]] name = "cw-vesting" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-denom", + "cw-denom 2.5.0", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "cw-stake-tracker", "cw-storage-plus 1.2.0", @@ -1504,7 +1878,7 @@ dependencies = [ [[package]] name = "cw-wormhole" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1690,16 +2064,17 @@ dependencies = [ [[package]] name = "cw20-stake" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", - "cw-hooks", + "cw-hooks 2.5.0", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", - "cw-paginate-storage 2.4.2", + "cw-paginate-storage 2.5.0", "cw-storage-plus 1.2.0", "cw-utils 0.13.4", "cw-utils 1.0.3", @@ -1707,20 +2082,21 @@ dependencies = [ "cw20 1.1.2", "cw20-base 1.1.2", "cw20-stake 0.2.6", - "dao-hooks", - "dao-voting 2.4.2", + "dao-hooks 2.5.0", + "dao-voting 2.5.0", "thiserror", ] [[package]] name = "cw20-stake-external-rewards" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -1728,26 +2104,27 @@ dependencies = [ "cw20 0.13.4", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", - "dao-hooks", + "cw20-stake 2.5.0", + "dao-hooks 2.5.0", "stake-cw20-external-rewards", "thiserror", ] [[package]] name = "cw20-stake-reward-distributor" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "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.4.2", + "cw20-stake 2.5.0", "stake-cw20-reward-distributor", "thiserror", ] @@ -1940,7 +2317,7 @@ dependencies = [ [[package]] name = "cw721-controllers" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -1951,12 +2328,13 @@ dependencies = [ [[package]] name = "cw721-roles" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -1971,34 +2349,104 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-cw-orch" +version = "2.5.0" +dependencies = [ + "btsg-ft-factory", + "cosmwasm-std", + "cw-admin-factory", + "cw-fund-distributor", + "cw-orch 0.24.1", + "cw-payroll-factory", + "cw-token-swap", + "cw-tokenfactory-issuer", + "cw-vesting", + "cw20-stake 2.5.0", + "cw20-stake-external-rewards", + "cw20-stake-reward-distributor", + "cw721-base 0.18.0", + "cw721-roles", + "dao-dao-core 2.5.0", + "dao-gauge-adapter", + "dao-interface 2.5.0", + "dao-migrator", + "dao-pre-propose-approval-single 2.5.0", + "dao-pre-propose-approver", + "dao-pre-propose-multiple 2.5.0", + "dao-pre-propose-single 2.5.0", + "dao-proposal-condorcet", + "dao-proposal-hook-counter", + "dao-proposal-multiple 2.5.0", + "dao-proposal-single 2.5.0", + "dao-proposal-sudo", + "dao-rewards-distributor", + "dao-test-custom-factory", + "dao-voting-cw20-balance", + "dao-voting-cw20-staked", + "dao-voting-cw4 2.5.0", + "dao-voting-cw721-roles", + "dao-voting-cw721-staked", + "dao-voting-token-staked", + "gauge-adapter", + "gauge-orchestrator", + "serde", +] + [[package]] name = "dao-cw721-extensions" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", + "cw-orch 0.24.1", "cw4 1.1.2", ] [[package]] name = "dao-dao-core" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd16c5f6f2760c66546e1e2f3781106dd796c8920847e78e5984922767cbc68" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-core", - "cw-multi-test", "cw-paginate-storage 2.4.2", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", "cw20 1.1.2", + "cw721 0.18.0", + "dao-dao-macros 2.4.2", + "dao-interface 2.4.1", + "thiserror", +] + +[[package]] +name = "dao-dao-core" +version = "2.5.0" +dependencies = [ + "abstract-cw-plus-interface", + "abstract-cw20", + "abstract-cw20-base", + "cosmwasm-schema", + "cosmwasm-std", + "cw-core", + "cw-multi-test", + "cw-orch 0.24.1", + "cw-paginate-storage 2.5.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-cw-orch", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", "dao-proposal-sudo", "dao-voting-cw20-balance", "thiserror", @@ -2007,35 +2455,93 @@ dependencies = [ [[package]] name = "dao-dao-macros" version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c3f39b7aaf9d913d0de8c8742c151011da00662acdbe95c33a5f7bad1b835a" +dependencies = [ + "cosmwasm-schema", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dao-dao-macros" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-hooks", - "dao-interface", - "dao-voting 2.4.2", + "cw-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-voting 2.5.0", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "dao-gauge-adapter" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-orch 0.24.1", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "gauge-orchestrator", + "thiserror", +] + [[package]] name = "dao-hooks" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c85abbadefe491d571f709464a8cfd2fb78b63b0cb6e6ef49104df249e28acc" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-hooks 2.4.2", + "cw4 1.1.2", + "dao-pre-propose-base 2.4.1", + "dao-voting 2.4.1", +] + +[[package]] +name = "dao-hooks" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-hooks", + "cw-hooks 2.5.0", "cw4 1.1.2", - "dao-pre-propose-base", - "dao-voting 2.4.2", + "dao-pre-propose-base 2.5.0", + "dao-voting 2.5.0", ] [[package]] name = "dao-interface" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4895da96b53c77592f6728fd21dfed4b9aff653fac8d1ee5dceb96353c7045" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw721 0.18.0", + "osmosis-std", +] + +[[package]] +name = "dao-interface" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-orch 0.24.1", "cw-utils 1.0.3", "cw2 1.1.2", "cw20 1.1.2", @@ -2045,7 +2551,7 @@ dependencies = [ [[package]] name = "dao-migrator" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", @@ -2053,6 +2559,7 @@ dependencies = [ "cw-core", "cw-core-interface 0.1.0 (git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0)", "cw-multi-test", + "cw-orch 0.24.1", "cw-proposal-single", "cw-storage-plus 1.2.0", "cw-utils 0.13.4", @@ -2062,55 +2569,79 @@ dependencies = [ "cw20 1.1.2", "cw20-base 1.1.2", "cw20-stake 0.2.6", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "cw20-staked-balance-voting", "cw4 0.13.4", "cw4-voting", - "dao-dao-core", - "dao-interface", - "dao-proposal-single", + "dao-dao-core 2.5.0", + "dao-interface 2.5.0", + "dao-proposal-single 2.5.0", "dao-testing", "dao-voting 0.1.0", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", - "dao-voting-cw4", + "dao-voting-cw4 2.5.0", "thiserror", ] [[package]] name = "dao-pre-propose-approval-single" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4fe4e5b50b3081272557069dbecaf1e0984d1f2932d2e61418712460f8fd313" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-denom", - "cw-multi-test", "cw-paginate-storage 2.4.2", "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "dao-interface 2.4.1", + "dao-pre-propose-base 2.4.1", + "dao-voting 2.4.1", + "thiserror", +] + +[[package]] +name = "dao-pre-propose-approval-single" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.5.0", + "cw-multi-test", + "cw-paginate-storage 2.5.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 1.1.2", "cw4-group 1.1.2", - "dao-dao-core", - "dao-hooks", - "dao-interface", - "dao-pre-propose-base", - "dao-proposal-single", + "dao-dao-core 2.4.1", + "dao-dao-core 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.4.1", + "dao-interface 2.5.0", + "dao-pre-propose-approval-single 2.4.1", + "dao-pre-propose-base 2.5.0", + "dao-proposal-single 2.4.1", + "dao-proposal-single 2.5.0", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.4.1", + "dao-voting 2.5.0", "dao-voting-cw20-staked", - "dao-voting-cw4", + "dao-voting-cw4 2.4.1", + "dao-voting-cw4 2.5.0", "thiserror", ] [[package]] name = "dao-pre-propose-approver" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-denom", + "cw-denom 2.5.0", "cw-multi-test", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -2118,255 +2649,374 @@ dependencies = [ "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-single", + "dao-dao-core 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-pre-propose-approval-single 2.5.0", + "dao-pre-propose-base 2.5.0", + "dao-proposal-single 2.5.0", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", - "dao-voting-cw4", + "dao-voting-cw4 2.5.0", ] [[package]] name = "dao-pre-propose-base" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd285523d7dea35a0dd76f0a5f20c190935922a7b58fe0ec753eb407e68d718b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.4.1", + "cw-hooks 2.4.2", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-interface 2.4.1", + "dao-voting 2.4.1", + "serde", + "thiserror", +] + +[[package]] +name = "dao-pre-propose-base" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-denom", - "cw-hooks", + "cw-denom 2.4.1", + "cw-denom 2.5.0", + "cw-hooks 2.5.0", "cw-multi-test", + "cw-orch 0.24.1", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", - "dao-interface", - "dao-voting 2.4.2", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.4.1", + "dao-voting 2.4.1", + "dao-voting 2.5.0", + "semver", "serde", "thiserror", ] [[package]] name = "dao-pre-propose-multiple" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6158aeddd2e080c730f1a2ac3814351cbcd38d61c38209dcf1e203d0e554a5" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 1.1.2", + "dao-pre-propose-base 2.4.1", + "dao-voting 2.4.1", +] + +[[package]] +name = "dao-pre-propose-multiple" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-denom", + "cw-denom 2.5.0", "cw-multi-test", "cw-utils 1.0.3", "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", + "cw4 1.1.2", "cw4-group 1.1.2", - "dao-dao-core", - "dao-hooks", - "dao-interface", - "dao-pre-propose-base", - "dao-proposal-multiple", + "dao-dao-core 2.4.1", + "dao-dao-core 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.4.1", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.5.0", + "dao-pre-propose-multiple 2.4.1", + "dao-proposal-multiple 2.4.1", + "dao-proposal-multiple 2.5.0", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.4.1", + "dao-voting 2.5.0", "dao-voting-cw20-staked", - "dao-voting-cw4", + "dao-voting-cw4 2.4.1", + "dao-voting-cw4 2.5.0", ] [[package]] name = "dao-pre-propose-single" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30f96fb1898f94ea4fe5771f8ab5c2bc6237782e06bb599f070713d11d38c19e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 1.1.2", + "dao-pre-propose-base 2.4.1", + "dao-voting 2.4.1", +] + +[[package]] +name = "dao-pre-propose-single" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-denom", - "cw-hooks", + "cw-denom 2.5.0", + "cw-hooks 2.5.0", "cw-multi-test", "cw-utils 1.0.3", "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", + "cw4 1.1.2", "cw4-group 1.1.2", - "dao-dao-core", - "dao-hooks", - "dao-interface", - "dao-pre-propose-base", - "dao-proposal-single", + "dao-dao-core 2.4.1", + "dao-dao-core 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.4.1", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.5.0", + "dao-pre-propose-single 2.4.1", + "dao-proposal-single 2.4.1", + "dao-proposal-single 2.5.0", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.4.1", + "dao-voting 2.5.0", "dao-voting-cw20-staked", - "dao-voting-cw4", + "dao-voting-cw4 2.4.1", + "dao-voting-cw4 2.5.0", ] [[package]] name = "dao-proposal-condorcet" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "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-dao-core 2.5.0", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", "dao-testing", - "dao-voting 2.4.2", - "dao-voting-cw4", + "dao-voting 2.5.0", + "dao-voting-cw4 2.5.0", "thiserror", ] [[package]] name = "dao-proposal-hook-counter" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-hooks", + "cw-hooks 2.5.0", "cw-multi-test", + "cw-orch 0.24.1", "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-single", - "dao-voting 2.4.2", + "dao-dao-core 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-proposal-single 2.5.0", + "dao-voting 2.5.0", "dao-voting-cw20-balance", "thiserror", ] [[package]] name = "dao-proposal-multiple" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fac47816150063ef09b555f3466f8be99612b76860a20fba7c85bd1854beba" dependencies = [ - "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-denom", - "cw-hooks", - "cw-multi-test", + "cw-hooks 2.4.2", "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.4.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-testing", + "dao-dao-macros 2.4.2", + "dao-hooks 2.4.1", + "dao-interface 2.4.1", + "dao-pre-propose-base 2.4.1", + "dao-pre-propose-multiple 2.4.1", "dao-voting 0.1.0", - "dao-voting 2.4.2", - "dao-voting-cw20-balance", - "dao-voting-cw20-staked", - "dao-voting-cw4", - "dao-voting-cw721-staked", - "dao-voting-token-staked", - "rand", + "dao-voting 2.4.1", "thiserror", ] [[package]] -name = "dao-proposal-single" -version = "2.4.2" +name = "dao-proposal-multiple" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-core", - "cw-denom", - "cw-hooks", + "cw-denom 2.5.0", + "cw-hooks 2.5.0", "cw-multi-test", - "cw-proposal-single", + "cw-orch 0.24.1", "cw-storage-plus 1.2.0", - "cw-utils 0.13.4", "cw-utils 1.0.3", "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.0", "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-dao-macros 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.5.0", + "dao-pre-propose-multiple 2.5.0", "dao-testing", "dao-voting 0.1.0", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-balance", "dao-voting-cw20-staked", - "dao-voting-cw4", + "dao-voting-cw4 2.5.0", "dao-voting-cw721-staked", "dao-voting-token-staked", + "rand", "thiserror", ] [[package]] -name = "dao-proposal-sudo" -version = "2.4.2" +name = "dao-proposal-single" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce91710cfcff1af520cd0e885eee6972aeefbefc1c9da18349e66ab959269bb" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-hooks 2.4.2", + "cw-proposal-single", "cw-storage-plus 1.2.0", + "cw-utils 0.13.4", + "cw-utils 1.0.3", "cw2 1.1.2", - "dao-dao-macros", - "dao-interface", + "cw20 1.1.2", + "dao-dao-macros 2.4.2", + "dao-hooks 2.4.1", + "dao-interface 2.4.1", + "dao-pre-propose-base 2.4.1", + "dao-voting 0.1.0", + "dao-voting 2.4.1", "thiserror", ] [[package]] -name = "dao-rewards-distributor" -version = "2.4.2" +name = "dao-proposal-single" +version = "2.5.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-core", + "cw-denom 2.5.0", + "cw-hooks 2.5.0", + "cw-multi-test", + "cw-orch 0.24.1", + "cw-proposal-single", + "cw-storage-plus 1.2.0", + "cw-utils 0.13.4", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.5.0", + "cw4 1.1.2", + "cw4-group 1.1.2", + "cw721-base 0.18.0", + "dao-dao-core 2.5.0", + "dao-dao-macros 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.5.0", + "dao-pre-propose-single 2.5.0", + "dao-testing", + "dao-voting 0.1.0", + "dao-voting 2.5.0", + "dao-voting-cw20-balance", + "dao-voting-cw20-staked", + "dao-voting-cw4 2.5.0", + "dao-voting-cw721-staked", + "dao-voting-token-staked", + "thiserror", +] + +[[package]] +name = "dao-proposal-sudo" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-orch 0.24.1", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-rewards-distributor" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "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.4.2", + "cw20-stake 2.5.0", "cw4 1.1.2", "cw4-group 1.1.2", "cw721-base 0.18.0", - "dao-hooks", - "dao-interface", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", - "dao-voting-cw4", + "dao-voting-cw4 2.5.0", "dao-voting-cw721-staked", "dao-voting-token-staked", + "semver", "thiserror", ] [[package]] name = "dao-test-custom-factory" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "cw-storage-plus 1.2.0", "cw-tokenfactory-issuer", @@ -2374,20 +3024,21 @@ dependencies = [ "cw2 1.1.2", "cw721 0.18.0", "cw721-base 0.18.0", - "dao-dao-macros", - "dao-interface", - "dao-voting 2.4.2", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "dao-voting 2.5.0", "thiserror", ] [[package]] name = "dao-testing" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", + "cw-admin-factory", "cw-core", - "cw-hooks", + "cw-hooks 2.5.0", "cw-multi-test", "cw-proposal-single", "cw-tokenfactory-issuer", @@ -2396,25 +3047,26 @@ dependencies = [ "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.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-dao-core 2.5.0", + "dao-interface 2.5.0", + "dao-pre-propose-multiple 2.5.0", + "dao-pre-propose-single 2.5.0", "dao-proposal-condorcet", - "dao-proposal-single", + "dao-proposal-single 2.5.0", "dao-test-custom-factory", "dao-voting 0.1.0", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-balance", "dao-voting-cw20-staked", - "dao-voting-cw4", + "dao-voting-cw4 2.5.0", "dao-voting-cw721-roles", "dao-voting-cw721-staked", + "dao-voting-onft-staked", "dao-voting-token-staked", "osmosis-std", "osmosis-test-tube", @@ -2438,80 +3090,119 @@ dependencies = [ [[package]] name = "dao-voting" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945898e8e168eada7ed06fa713d679e541673ee0dd8c70aee8d1f224ccd031a0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.4.1", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "dao-dao-macros 2.4.2", + "dao-interface 2.4.1", + "thiserror", +] + +[[package]] +name = "dao-voting" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-denom", + "cw-denom 2.5.0", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw20 1.1.2", - "dao-dao-macros", - "dao-interface", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", "thiserror", ] [[package]] name = "dao-voting-cw20-balance" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "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", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", "thiserror", ] [[package]] name = "dao-voting-cw20-staked" -version = "2.4.2" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "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.4.2", - "dao-dao-macros", - "dao-interface", - "dao-voting 2.4.2", + "cw20-stake 2.5.0", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "dao-voting 2.5.0", "thiserror", ] [[package]] name = "dao-voting-cw4" -version = "2.4.2" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba59e19abd4d51d6c3a37a84fb0c8cfe90e2f2ab551a610ec6749fcd09fc9e86" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "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 2.4.2", + "dao-interface 2.4.1", + "thiserror", +] + +[[package]] +name = "dao-voting-cw4" +version = "2.5.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "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", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", "thiserror", ] [[package]] name = "dao-voting-cw721-roles" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", @@ -2522,65 +3213,97 @@ dependencies = [ "cw721-controllers", "cw721-roles", "dao-cw721-extensions", - "dao-dao-macros", - "dao-interface", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", "dao-testing", "thiserror", ] [[package]] name = "dao-voting-cw721-staked" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", - "cw-hooks", + "cw-hooks 2.5.0", "cw-multi-test", + "cw-orch 0.24.1", "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-dao-macros 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", "dao-proposal-hook-counter", - "dao-proposal-single", + "dao-proposal-single 2.5.0", "dao-test-custom-factory", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "osmosis-std", "osmosis-test-tube", "serde", "thiserror", ] +[[package]] +name = "dao-voting-onft-staked" +version = "2.5.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-hooks 2.5.0", + "cw-multi-test", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw721-controllers", + "dao-dao-macros 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-proposal-hook-counter", + "dao-proposal-single 2.5.0", + "dao-test-custom-factory", + "dao-testing", + "dao-voting 2.5.0", + "omniflix-std", + "osmosis-test-tube", + "prost 0.12.3", + "prost-derive 0.12.3", + "serde", + "thiserror", +] + [[package]] name = "dao-voting-token-staked" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-controllers 1.1.2", - "cw-hooks", + "cw-hooks 2.5.0", "cw-multi-test", + "cw-orch 0.24.1", "cw-ownable", "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-dao-macros 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", "dao-proposal-hook-counter", - "dao-proposal-single", + "dao-proposal-single 2.5.0", "dao-test-custom-factory", "dao-testing", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "osmosis-std", "osmosis-test-tube", "serde", @@ -2666,6 +3389,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dyn-clone" version = "1.0.17" @@ -2707,25 +3436,63 @@ dependencies = [ "signature 1.6.4", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "serde", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + [[package]] name = "ed25519-dalek" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ - "curve25519-dalek", - "ed25519", + "curve25519-dalek 3.2.0", + "ed25519 1.5.3", "sha2 0.9.9", "zeroize", ] +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519 2.2.3", + "serde", + "sha2 0.10.8", + "subtle", + "zeroize", +] + [[package]] name = "ed25519-zebra" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 3.2.0", "hashbrown 0.12.3", "hex", "rand_core 0.6.4", @@ -2779,6 +3546,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -2827,6 +3603,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "ff" version = "0.12.1" @@ -2847,6 +3629,32 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "file-lock" +version = "2.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "040b48f80a749da50292d0f47a1e2d5bf1d772f52836c07f64bfccc62ba6e664" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "flate2" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flex-error" version = "0.4.4" @@ -2863,6 +3671,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2934,7 +3757,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -2969,7 +3792,7 @@ dependencies = [ [[package]] name = "gauge-adapter" -version = "2.4.2" +version = "2.5.0" dependencies = [ "abstract-cw-plus-interface", "abstract-cw20", @@ -2977,38 +3800,39 @@ dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", - "cw-denom", + "cw-denom 2.5.0", "cw-multi-test", - "cw-orch", - "cw-orch-core", + "cw-orch 0.24.1", "cw-storage-plus 1.2.0", "cw-utils 1.0.3", "cw2 1.1.2", "cw20 1.1.2", "cw20-base 1.1.2", + "scripts", "semver", "thiserror", ] [[package]] name = "gauge-orchestrator" -version = "2.4.2" +version = "2.5.0" dependencies = [ "anyhow", "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw-orch 0.24.1", "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-hooks", - "dao-interface", - "dao-proposal-single", - "dao-voting 2.4.2", - "dao-voting-cw4", + "dao-dao-core 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-proposal-single 2.5.0", + "dao-voting 2.5.0", + "dao-voting-cw4 2.5.0", "schemars", "serde", "thiserror", @@ -3084,7 +3908,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.6", + "indexmap 2.3.0", "slab", "tokio", "tokio-util", @@ -3142,6 +3966,27 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hkd32" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e013a4f0b8772418eee1fc462e74017aba13c364a7b61bd3df1ddcbfe47b065" +dependencies = [ + "hmac", + "once_cell", + "pbkdf2", + "rand_core 0.6.4", + "sha2 0.10.8", + "subtle-encoding", + "zeroize", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3235,10 +4080,10 @@ dependencies = [ "headers", "http", "hyper", - "hyper-rustls", - "rustls-native-certs", + "hyper-rustls 0.22.1", + "rustls-native-certs 0.5.0", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", "tower-service", "webpki", ] @@ -3253,14 +4098,28 @@ dependencies = [ "futures-util", "hyper", "log", - "rustls", - "rustls-native-certs", + "rustls 0.19.1", + "rustls-native-certs 0.5.0", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", "webpki", "webpki-roots", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-timeout" version = "0.4.1" @@ -3273,6 +4132,42 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -3314,9 +4209,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -3336,16 +4231,16 @@ dependencies = [ "cw-vesting", "cw20 1.1.2", "cw20-base 1.1.2", - "cw20-stake 2.4.2", + "cw20-stake 2.5.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-dao-core 2.5.0", + "dao-interface 2.5.0", + "dao-pre-propose-single 2.5.0", + "dao-proposal-single 2.5.0", "dao-test-custom-factory", - "dao-voting 2.4.2", + "dao-voting 2.5.0", "dao-voting-cw20-staked", "dao-voting-cw721-staked", "env_logger", @@ -3354,8 +4249,15 @@ dependencies = [ "serde", "serde_json", "test-context", + "tokio", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is-terminal" version = "0.4.12" @@ -3394,15 +4296,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.11" @@ -3444,9 +4337,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if", "ecdsa 0.16.9", @@ -3485,9 +4378,9 @@ checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -3515,6 +4408,16 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -3565,6 +4468,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nom" version = "7.1.3" @@ -3594,7 +4514,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3627,13 +4547,42 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] +[[package]] +name = "omniflix-std" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a662bd7782ef6ad1af3747a2b73d37f8e6a230bb7b1624d96c05b3567501600" +dependencies = [ + "chrono", + "cosmwasm-std", + "omniflix-std-derive", + "prost 0.12.3", + "prost-types 0.12.3", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "omniflix-std-derive" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bbd85582e3ef1a23fa7b12e0415ea604260c114e72faf40d829c2c40f1c745e" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "prost-types 0.11.9", + "quote", + "syn 1.0.109", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -3646,12 +4595,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -3715,6 +4702,29 @@ dependencies = [ "thiserror", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "paste" version = "1.0.15" @@ -3748,8 +4758,18 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07c0b841ea54f523f7aa556956fbd293bcbe06f2e67d2eb732b7278aaf1d166a" dependencies = [ - "peg-macros", - "peg-runtime", + "peg-macros 0.7.0", + "peg-runtime 0.7.0", +] + +[[package]] +name = "peg" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" +dependencies = [ + "peg-macros 0.8.4", + "peg-runtime 0.8.3", ] [[package]] @@ -3758,7 +4778,18 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aa52829b8decbef693af90202711348ab001456803ba2a98eb4ec8fb70844c" dependencies = [ - "peg-runtime", + "peg-runtime 0.7.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" +dependencies = [ + "peg-runtime 0.8.3", "proc-macro2", "quote", ] @@ -3769,6 +4800,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +[[package]] +name = "peg-runtime" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3806,7 +4843,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3837,7 +4874,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3872,11 +4909,30 @@ dependencies = [ "spki 0.7.3", ] +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] [[package]] name = "prettyplease" @@ -3885,7 +4941,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3921,22 +4977,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes", - "prost-derive 0.12.6", -] - -[[package]] -name = "prost" -version = "0.13.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" dependencies = [ "bytes", - "prost-derive 0.13.1", + "prost-derive 0.12.3", ] [[package]] @@ -3954,28 +5000,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.71", -] - -[[package]] -name = "prost-derive" -version = "0.13.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -3989,20 +5022,11 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost 0.12.6", -] - -[[package]] -name = "prost-types" -version = "0.13.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee5168b05f49d4b0ca581206eb14a7b22fafd963efe729ac48eb03266e25cc2" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" dependencies = [ - "prost 0.13.1", + "prost 0.12.3", ] [[package]] @@ -4050,6 +5074,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "redox_users" version = "0.4.5" @@ -4063,9 +5096,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -4090,6 +5123,50 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls 0.24.2", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rfc6979" version = "0.3.1" @@ -4120,12 +5197,27 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + [[package]] name = "ripemd" version = "0.1.3" @@ -4179,6 +5271,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.34" @@ -4200,11 +5301,23 @@ checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64 0.13.1", "log", - "ring", - "sct", + "ring 0.16.20", + "sct 0.6.1", "webpki", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-webpki", + "sct 0.7.1", +] + [[package]] name = "rustls-native-certs" version = "0.5.0" @@ -4212,11 +5325,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" dependencies = [ "openssl-probe", - "rustls", + "rustls 0.19.1", "schannel", "security-framework", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -4260,25 +5404,78 @@ dependencies = [ ] [[package]] -name = "schemars_derive" -version = "0.8.21" +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.72", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scripts" +version = "2.5.0" +dependencies = [ + "abstract-cw-plus-interface", + "abstract-cw20", + "abstract-cw20-base", + "btsg-ft-factory", + "cosmwasm-std", + "cw-admin-factory", + "cw-denom 2.5.0", + "cw-orch 0.24.1", + "cw-orch-core", + "cw-payroll-factory", + "cw-token-swap", + "cw-tokenfactory-issuer", + "cw-utils 1.0.3", + "cw-vesting", + "cw20 1.1.2", + "cw4 1.1.2", + "cw4-group 1.1.2", + "cw721-roles", + "dao-cw-orch", + "dao-gauge-adapter", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-migrator", + "dao-proposal-single 2.5.0", + "dao-proposal-sudo", + "dao-voting 2.5.0", + "dao-voting-cw4 2.5.0", + "dotenv", + "gauge-adapter", + "gauge-orchestrator", + "pretty_env_logger", +] + +[[package]] +name = "sct" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.71", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] name = "sct" -version = "0.6.1" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.8", + "untrusted 0.9.0", ] [[package]] @@ -4309,6 +5506,25 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.11.1" @@ -4340,9 +5556,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" dependencies = [ "serde_derive", ] @@ -4376,13 +5592,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.205" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -4393,16 +5609,17 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -4415,7 +5632,19 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", ] [[package]] @@ -4424,7 +5653,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.3.0", "itoa", "ryu", "serde", @@ -4482,6 +5711,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "1.6.4" @@ -4511,6 +5749,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "socket2" version = "0.5.7" @@ -4527,6 +5771,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.6.0" @@ -4626,6 +5876,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + [[package]] name = "syn" version = "1.0.109" @@ -4639,9 +5895,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.71" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -4654,6 +5910,40 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "tendermint" version = "0.23.9" @@ -4662,8 +5952,8 @@ checksum = "467f82178deeebcd357e1273a0c0b77b9a8a0313ef7c07074baebe99d87851f4" dependencies = [ "async-trait", "bytes", - "ed25519", - "ed25519-dalek", + "ed25519 1.5.3", + "ed25519-dalek 1.0.1", "flex-error", "futures", "k256 0.11.6", @@ -4693,8 +5983,8 @@ checksum = "baa1d2d0ec1b531ba7d196f0dbee5e78ed2a82bfba928e88dff64aeec0b26073" dependencies = [ "async-trait", "bytes", - "ed25519", - "ed25519-dalek", + "ed25519 1.5.3", + "ed25519-dalek 1.0.1", "flex-error", "futures", "k256 0.11.6", @@ -4716,6 +6006,37 @@ dependencies = [ "zeroize", ] +[[package]] +name = "tendermint" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15ab8f0a25d0d2ad49ac615da054d6a76aa6603ff95f7d18bafdd34450a1a04b" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519 2.2.3", + "ed25519-consensus", + "flex-error", + "futures", + "k256 0.13.3", + "num-traits", + "once_cell", + "prost 0.12.3", + "prost-types 0.12.3", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature 2.2.0", + "subtle", + "subtle-encoding", + "tendermint-proto 0.34.1", + "time", + "zeroize", +] + [[package]] name = "tendermint-config" version = "0.23.9" @@ -4744,6 +6065,20 @@ dependencies = [ "url", ] +[[package]] +name = "tendermint-config" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint 0.34.1", + "toml", + "url", +] + [[package]] name = "tendermint-proto" version = "0.23.9" @@ -4798,6 +6133,24 @@ dependencies = [ "time", ] +[[package]] +name = "tendermint-proto" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b797dd3d2beaaee91d2f065e7bdf239dc8d80bba4a183a288bc1279dd5a69a1e" +dependencies = [ + "bytes", + "flex-error", + "num-derive 0.3.3", + "num-traits", + "prost 0.12.3", + "prost-types 0.12.3", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + [[package]] name = "tendermint-proto" version = "0.35.0" @@ -4808,8 +6161,8 @@ dependencies = [ "flex-error", "num-derive 0.4.2", "num-traits", - "prost 0.12.6", - "prost-types 0.12.6", + "prost 0.12.3", + "prost-types 0.12.3", "serde", "serde_bytes", "subtle-encoding", @@ -4830,8 +6183,8 @@ dependencies = [ "http", "hyper", "hyper-proxy", - "hyper-rustls", - "peg", + "hyper-rustls 0.22.1", + "peg 0.7.0", "pin-project", "serde", "serde_bytes", @@ -4845,7 +6198,7 @@ dependencies = [ "tokio", "tracing", "url", - "uuid", + "uuid 0.8.2", "walkdir", ] @@ -4863,8 +6216,8 @@ dependencies = [ "http", "hyper", "hyper-proxy", - "hyper-rustls", - "peg", + "hyper-rustls 0.22.1", + "peg 0.7.0", "pin-project", "serde", "serde_bytes", @@ -4879,7 +6232,40 @@ dependencies = [ "tokio", "tracing", "url", - "uuid", + "uuid 0.8.2", + "walkdir", +] + +[[package]] +name = "tendermint-rpc" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71afae8bb5f6b14ed48d4e1316a643b6c2c3cbad114f510be77b4ed20b7b3e42" +dependencies = [ + "async-trait", + "bytes", + "flex-error", + "futures", + "getrandom", + "peg 0.8.4", + "pin-project", + "rand", + "reqwest", + "semver", + "serde", + "serde_bytes", + "serde_json", + "subtle", + "subtle-encoding", + "tendermint 0.34.1", + "tendermint-config 0.34.1", + "tendermint-proto 0.34.1", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "uuid 1.10.0", "walkdir", ] @@ -4910,7 +6296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d506c7664333e246f564949bee4ed39062aa0f11918e6f5a95f553cdad65c274" dependencies = [ "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -4946,7 +6332,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -4992,7 +6378,9 @@ dependencies = [ "libc", "mio", "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.48.0", @@ -5016,7 +6404,17 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] @@ -5025,11 +6423,21 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ - "rustls", + "rustls 0.19.1", "tokio", "webpki", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.15" @@ -5123,6 +6531,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.7", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost 0.12.3", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "rustls-pemfile", + "tokio", + "tokio-rustls 0.24.1", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.4.13" @@ -5174,7 +6613,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] [[package]] @@ -5214,6 +6653,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "uid" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "281a11e4180ee68f8c06a7bf82d1be3c496e0debcadcc4576cc0b56132201edf" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -5253,6 +6698,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.2" @@ -5270,11 +6721,23 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vote-hooks" @@ -5334,10 +6797,22 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.92" @@ -5356,7 +6831,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5383,8 +6858,8 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -5426,11 +6901,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5439,6 +6914,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -5457,6 +6941,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -5578,6 +7071,16 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wynd-utils" version = "0.4.1" @@ -5599,6 +7102,27 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "zeroize" version = "1.8.1" @@ -5616,5 +7140,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.71", + "syn 2.0.72", ] diff --git a/Cargo.toml b/Cargo.toml index 98fd3c45f..82b7c027b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -exclude = ["ci/configs/", "wasmvm/libwasmvm"] +exclude = ["ci/configs/", "wasmvm/libwasmvm", "scripts"] members = [ "contracts/dao-dao-core", "contracts/distribution/*", @@ -11,6 +11,7 @@ members = [ "contracts/test/*", "contracts/voting/*", "packages/*", + "scripts", "ci/*", ] resolver = "2" @@ -19,7 +20,7 @@ resolver = "2" edition = "2021" license = "BSD-3-Clause" repository = "https://github.com/DA0-DA0/dao-contracts" -version = "2.4.2" +version = "2.5.0" [profile.release] codegen-units = 1 @@ -37,13 +38,14 @@ overflow-checks = true [workspace.dependencies] anyhow = { version = "1.0" } assert_matches = "1.5" +bech32 = "0.9.1" cosm-orc = { version = "4.0" } cosm-tome = "0.2" cosmos-sdk-proto = "0.19" cosmwasm-schema = { version = "1.5.4" } -cosmwasm-std = { version = "1.5.4", features = ["ibc3"] } +cosmwasm-std = { version = "1.5.4", features = ["ibc3", "cosmwasm_1_1"] } cw-controllers = "1.1" -cw-multi-test = "0.18" +cw-multi-test = { version = "0.20.1", features = ["cosmwasm_1_1"] } cw-storage-plus = { version = "1.1" } cw-utils = "1.0" cw2 = "1.1" @@ -56,16 +58,18 @@ cw721 = "0.18" cw721-base = "0.18" env_logger = "0.10" once_cell = "1.18" +omniflix-std = "0.1.8" osmosis-std = "0.20.1" osmosis-std-derive = "0.20.1" osmosis-test-tube = "20.1.1" proc-macro2 = "1.0" -prost = { version = "0.13.1", features = ["prost-derive"] } -prost-types = { version = "0.13.1", default-features = false } +prost = { version = "=0.12.3", features = ["prost-derive"] } +prost-types = { version = "=0.12.3", default-features = false } +prost-derive = "=0.12.3" quote = "1.0" rand = "0.8" schemars = "0.8" -semver = "1.0.23" +semver = "1.0.20" serde = { version = "1.0", default-features = false, features = ["derive"] } serde-cw-value = "0.7" serde_json = "1.0" @@ -78,55 +82,59 @@ syn = { version = "1.0", features = ["derive"] } test-context = "0.1" thiserror = { version = "1.0" } wynd-utils = "0.4" -cw-orch = "0.22.2" -cw-orch-core = "1.0.0-rc" - +cw-orch = "0.24.1" +tokio = "=1.38.1" # One commit ahead of version 0.3.0. Allows initialization with an # optional owner. cw-ownable = "0.5" -cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.4.2" } -cw-denom = { path = "./packages/cw-denom", version = "2.4.2" } -cw-fund-distributor = { path = "./contracts/distribution/cw-fund-distributor", version = "2.4.2" } -cw-hooks = { path = "./packages/cw-hooks", version = "2.4.2" } -cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.4.2" } -cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory", version = "2.4.2" } -cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "2.4.2" } -cw-tokenfactory-issuer = { path = "./contracts/external/cw-tokenfactory-issuer", version = "2.4.2", default-features = false } -cw-tokenfactory-types = { path = "./packages/cw-tokenfactory-types", version = "2.4.2", default-features = false } -cw-vesting = { path = "./contracts/external/cw-vesting", version = "2.4.2" } -cw-wormhole = { path = "./packages/cw-wormhole", version = "2.4.2" } -cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.4.2" } -cw721-controllers = { path = "./packages/cw721-controllers", version = "2.4.2" } -cw721-roles = { path = "./contracts/external/cw721-roles", version = "2.4.2" } -dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "2.4.2" } -dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.4.2" } -dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.4.2" } -dao-hooks = { path = "./packages/dao-hooks", version = "2.4.2" } -dao-interface = { path = "./packages/dao-interface", version = "2.4.2" } -dao-pre-propose-approval-single = { path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "2.4.2" } -dao-pre-propose-approver = { path = "./contracts/pre-propose/dao-pre-propose-approver", version = "2.4.2" } -dao-pre-propose-base = { path = "./packages/dao-pre-propose-base", version = "2.4.2" } -dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-multiple", version = "2.4.2" } -dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.4.2" } -dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.4.2" } -dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.4.2" } -dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.4.2" } -dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.4.2" } -dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.4.2" } -dao-rewards-distributor = { path = "./contracts/distribution/dao-rewards-distributor", version = "2.4.2" } -dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "2.4.2" } -dao-testing = { path = "./packages/dao-testing", version = "2.4.2" } -dao-voting = { path = "./packages/dao-voting", version = "2.4.2" } -dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.4.2" } -dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.4.2" } -dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.4.2" } -dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "2.4.2" } -dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.4.2" } -dao-voting-token-staked = { path = "./contracts/voting/dao-voting-token-staked", version = "2.4.2" } -gauge-orchestrator = { path = "./contracts/gauges/gauge", version = "2.4.2" } -gauge-adapter = { path = "./contracts/gauges/gauge-adapter", version = "2.4.2" } +btsg-ft-factory = { path = "./contracts/external/btsg-ft-factory", version = "2.5.0" } +cw-admin-factory = { path = "./contracts/external/cw-admin-factory", version = "2.5.0" } +cw-denom = { path = "./packages/cw-denom", version = "2.5.0" } +cw-fund-distributor = { path = "./contracts/distribution/cw-fund-distributor", version = "2.5.0" } +cw-hooks = { path = "./packages/cw-hooks", version = "2.5.0" } +cw-paginate-storage = { path = "./packages/cw-paginate-storage", version = "2.5.0" } +cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory", version = "2.5.0" } +cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "2.5.0" } +cw-token-swap = { path = "./contracts/external/cw-token-swap", version = "2.5.0" } +cw-tokenfactory-issuer = { path = "./contracts/external/cw-tokenfactory-issuer", version = "2.5.0", default-features = false } +cw-tokenfactory-types = { path = "./packages/cw-tokenfactory-types", version = "2.5.0", default-features = false } +cw-vesting = { path = "./contracts/external/cw-vesting", version = "2.5.0" } +cw-wormhole = { path = "./packages/cw-wormhole", version = "2.5.0" } +cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.5.0" } +cw721-controllers = { path = "./packages/cw721-controllers", version = "2.5.0" } +cw721-roles = { path = "./contracts/external/cw721-roles", version = "2.5.0" } +dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "2.5.0" } +dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.5.0" } +dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.5.0" } +dao-gauge-adapter = { path = "./contracts/test/dao-gauge-adapter", version = "2.5.0" } +dao-hooks = { path = "./packages/dao-hooks", version = "2.5.0" } +dao-interface = { path = "./packages/dao-interface", version = "2.5.0" } +dao-migrator = { path = "./contracts/external/dao-migrator", version = "2.5.0" } +dao-pre-propose-approval-single = { path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "2.5.0" } +dao-pre-propose-approver = { path = "./contracts/pre-propose/dao-pre-propose-approver", version = "2.5.0" } +dao-pre-propose-base = { path = "./packages/dao-pre-propose-base", version = "2.5.0" } +dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-multiple", version = "2.5.0" } +dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.5.0" } +dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.5.0" } +dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.5.0" } +dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.5.0" } +dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.5.0" } +dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.5.0" } +dao-rewards-distributor = { path = "./contracts/distribution/dao-rewards-distributor", version = "2.5.0" } +dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "2.5.0" } +dao-testing = { path = "./packages/dao-testing", version = "2.5.0" } +dao-voting = { path = "./packages/dao-voting", version = "2.5.0" } +dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.5.0" } +dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.5.0" } +dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.5.0" } +dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "2.5.0" } +dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.5.0" } +dao-voting-onft-staked = { path = "./contracts/voting/dao-voting-onft-staked", version = "2.5.0" } +dao-voting-token-staked = { path = "./contracts/voting/dao-voting-token-staked", version = "2.5.0" } +gauge-adapter = { path = "./contracts/gauges/gauge-adapter", version = "2.5.0" } +gauge-orchestrator = { path = "./contracts/gauges/gauge", version = "2.5.0" } # v1 dependencies. used for state migrations. cw-core-v1 = { package = "cw-core", version = "0.1.0" } @@ -139,3 +147,16 @@ cw20-staked-balance-voting-v1 = { package = "cw20-staked-balance-voting", versio cw4-voting-v1 = { package = "cw4-voting", version = "0.1.0" } stake-cw20-v03 = { package = "stake-cw20", version = "0.2.6" } voting-v1 = { package = "dao-voting", version = "0.1.0" } + +# v2.4.1 dependencies. used for state migrations. +cw-denom-v241 = { package = "cw-denom", version = "=2.4.1" } +dao-dao-core-v241 = { package = "dao-dao-core", version = "=2.4.1" } +dao-interface-v241 = { package = "dao-interface", version = "=2.4.1" } +dao-pre-propose-base-v241 = { package = "dao-pre-propose-base", version = "=2.4.1" } +dao-pre-propose-approval-single-v241 = { package = "dao-pre-propose-approval-single", version = "=2.4.1" } +dao-pre-propose-multiple-v241 = { package = "dao-pre-propose-multiple", version = "=2.4.1" } +dao-pre-propose-single-v241 = { package = "dao-pre-propose-single", version = "=2.4.1" } +dao-proposal-multiple-v241 = { package = "dao-proposal-multiple", version = "=2.4.1" } +dao-proposal-single-v241 = { package = "dao-proposal-single", version = "=2.4.1" } +dao-voting-cw4-v241 = { package = "dao-voting-cw4", version = "=2.4.1" } +dao-voting-v241 = { package = "dao-voting", version = "=2.4.1" } \ No newline at end of file diff --git a/ci/bootstrap-env/Cargo.toml b/ci/bootstrap-env/Cargo.toml index 5ea498f2b..1f20b472a 100644 --- a/ci/bootstrap-env/Cargo.toml +++ b/ci/bootstrap-env/Cargo.toml @@ -6,6 +6,7 @@ repository = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] cosm-orc = { workspace = true } +tokio.workspace = true cw20 = { workspace = true } cw-utils = { workspace = true } cosmwasm-std = { workspace = true, features = ["ibc3"] } diff --git a/ci/bootstrap-env/src/main.rs b/ci/bootstrap-env/src/main.rs index 83d6b82c9..be218793f 100644 --- a/ci/bootstrap-env/src/main.rs +++ b/ci/bootstrap-env/src/main.rs @@ -4,6 +4,7 @@ use cosm_orc::{config::cfg::Config, orchestrator::cosm_orc::CosmOrc}; use cosmwasm_std::{to_json_binary, Decimal, Empty, Uint128}; use cw20::Cw20Coin; use dao_interface::state::{Admin, ModuleInstantiateInfo}; +use dao_voting::pre_propose::PreProposeSubmissionPolicy; use dao_voting::{ deposit::{DepositRefundPolicy, DepositToken, UncheckedDepositInfo, VotingModuleTokenType}, pre_propose::PreProposeInfo, @@ -99,7 +100,11 @@ fn main() -> Result<()> { amount: Uint128::new(1000000000), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: Empty::default(), }) .unwrap(), diff --git a/ci/integration-tests/Cargo.toml b/ci/integration-tests/Cargo.toml index 6291ec502..0c268f1ba 100644 --- a/ci/integration-tests/Cargo.toml +++ b/ci/integration-tests/Cargo.toml @@ -11,6 +11,7 @@ edition = { workspace = true } # targeting wasm. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] cosm-orc = { workspace = true } +tokio.workspace = true cw20 = { workspace = true } cw20-base = { workspace = true } cw721-base = { workspace = true } diff --git a/ci/integration-tests/src/helpers/helper.rs b/ci/integration-tests/src/helpers/helper.rs index e3bb76558..13b627d4d 100644 --- a/ci/integration-tests/src/helpers/helper.rs +++ b/ci/integration-tests/src/helpers/helper.rs @@ -8,7 +8,7 @@ use dao_interface::query::DumpStateResponse; use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_voting::{ deposit::{DepositRefundPolicy, DepositToken, UncheckedDepositInfo, VotingModuleTokenType}, - pre_propose::{PreProposeInfo, ProposalCreationPolicy}, + pre_propose::{PreProposeInfo, PreProposeSubmissionPolicy, ProposalCreationPolicy}, threshold::PercentageThreshold, threshold::Threshold, voting::Vote, @@ -84,7 +84,11 @@ pub fn create_dao( amount: DEPOSIT_AMOUNT, refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: Empty::default(), }) .unwrap(), diff --git a/contracts/dao-dao-core/Cargo.toml b/contracts/dao-dao-core/Cargo.toml index ac8e2060d..7e51b54e2 100644 --- a/contracts/dao-dao-core/Cargo.toml +++ b/contracts/dao-dao-core/Cargo.toml @@ -31,8 +31,13 @@ cw-paginate-storage = { workspace = true } cw-core-v1 = { workspace = true, features = ["library"] } [dev-dependencies] -cw-multi-test = { workspace = true, features = ["stargate"] } +abstract-cw-plus-interface = "2.0.1" +abstract-cw20 = "2.0.0" +abstract-cw20-base = "2.0.0" +cw-multi-test = { workspace = true } +cw-orch = { workspace = true } cw20-base = { workspace = true } cw721-base = { workspace = true } +dao-cw-orch = { path = "../../packages/cw-orch" } dao-proposal-sudo = { workspace = true } dao-voting-cw20-balance = { workspace = true } diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index 3ada83c21..23e45fd01 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -918,6 +918,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" + }, "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": [ @@ -964,6 +968,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -1039,6 +1068,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -1463,6 +1522,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -1547,6 +1660,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -2486,37 +2614,43 @@ }, "cw20_balances": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Cw20BalanceResponse", - "description": "Returned by the `Cw20Balances` query.", - "type": "object", - "required": [ - "addr", - "balance" - ], - "properties": { - "addr": { - "description": "The address of the token.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "balance": { - "description": "The contract's balance.", - "allOf": [ - { - "$ref": "#/definitions/Uint128" - } - ] - } + "title": "Array_of_Cw20BalanceResponse", + "type": "array", + "items": { + "$ref": "#/definitions/Cw20BalanceResponse" }, - "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" }, + "Cw20BalanceResponse": { + "description": "Returned by the `Cw20Balances` query.", + "type": "object", + "required": [ + "addr", + "balance" + ], + "properties": { + "addr": { + "description": "The address of the token.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "balance": { + "description": "The contract's balance.", + "allOf": [ + { + "$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" @@ -2895,10 +3029,20 @@ }, "list_items": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Array_of_String", + "title": "Array_of_Tuple_of_String_and_String", "type": "array", "items": { - "type": "string" + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 } }, "list_sub_daos": { diff --git a/contracts/dao-dao-core/src/tests.rs b/contracts/dao-dao-core/src/tests.rs index 81dd040cb..52a350ec9 100644 --- a/contracts/dao-dao-core/src/tests.rs +++ b/contracts/dao-dao-core/src/tests.rs @@ -1,3 +1,12 @@ +use crate::{ + contract::{derive_proposal_module_prefix, migrate, CONTRACT_NAME, CONTRACT_VERSION}, + state::PROPOSAL_MODULES, + ContractError, +}; +use abstract_cw20::msg::Cw20ExecuteMsgFns; +use abstract_cw_plus_interface::cw20_base::Cw20Base; +use v1::DaoDaoCoreV1; + use cosmwasm_schema::cw_serde; use cosmwasm_std::{ from_json, @@ -5,99 +14,64 @@ use cosmwasm_std::{ to_json_binary, Addr, CosmosMsg, Empty, Storage, Uint128, WasmMsg, }; use cw2::{set_contract_version, ContractVersion}; -use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use cw_orch::prelude::*; + use cw_storage_plus::{Item, Map}; use cw_utils::{Duration, Expiration}; +use dao_cw_orch::Cw721Base; +use dao_cw_orch::{DaoDaoCore, DaoProposalSudo, DaoVotingCw20Balance}; +use dao_interface::CoreExecuteMsgFns; +use dao_interface::CoreQueryMsgFns; use dao_interface::{ - msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, QueryMsg}, + msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg}, query::{ - AdminNominationResponse, Cw20BalanceResponse, DaoURIResponse, DumpStateResponse, - GetItemResponse, PauseInfoResponse, ProposalModuleCountResponse, SubDao, + AdminNominationResponse, Cw20BalanceResponse, DumpStateResponse, GetItemResponse, + PauseInfoResponse, ProposalModuleCountResponse, SubDao, }, state::{Admin, Config, ModuleInstantiateInfo, ProposalModule, ProposalModuleStatus}, voting::{InfoResponse, VotingPowerAtHeightResponse}, }; +use dao_proposal_sudo::msg::ExecuteMsgFns as _; +use dao_voting_cw20_balance::msg::QueryMsgFns; -use crate::{ - contract::{derive_proposal_module_prefix, migrate, CONTRACT_NAME, CONTRACT_VERSION}, - state::PROPOSAL_MODULES, - ContractError, -}; - -const CREATOR_ADDR: &str = "creator"; - -fn cw20_contract() -> Box> { - let contract = ContractWrapper::new( - cw20_base::contract::execute, - cw20_base::contract::instantiate, - cw20_base::contract::query, - ); - Box::new(contract) -} - -fn cw721_contract() -> Box> { - let contract = ContractWrapper::new( - cw721_base::entry::execute, - cw721_base::entry::instantiate, - cw721_base::entry::query, - ); - Box::new(contract) -} - -fn sudo_proposal_contract() -> Box> { - let contract = ContractWrapper::new( - dao_proposal_sudo::contract::execute, - dao_proposal_sudo::contract::instantiate, - dao_proposal_sudo::contract::query, - ); - Box::new(contract) +pub fn assert_contains(e: impl std::fmt::Debug, el: impl ToString) { + assert!(format!("{:?}", e).contains(&el.to_string())) } -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) -} +pub mod v1 { + use cw_orch::{interface, prelude::*}; -fn cw_core_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) -} + use cw_core_v1::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -fn v1_cw_core_contract() -> Box> { - use cw_core_v1::contract; - let contract = ContractWrapper::new(contract::execute, contract::instantiate, contract::query) - .with_reply(contract::reply) - .with_migrate(contract::migrate); - Box::new(contract) -} + #[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] + pub struct DaoDaoCoreV1; -fn instantiate_gov(app: &mut App, code_id: u64, msg: InstantiateMsg) -> Addr { - app.instantiate_contract( - code_id, - Addr::unchecked(CREATOR_ADDR), - &msg, - &[], - "cw-governance", - None, - ) - .unwrap() + impl Uploadable for DaoDaoCoreV1 { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_dao_core") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + use cw_core_v1::contract; + Box::new( + ContractWrapper::new(contract::execute, contract::instantiate, contract::query) + .with_reply(contract::reply) + .with_migrate(contract::migrate), + ) + } + } } fn test_instantiate_with_n_gov_modules(n: usize) { - let mut app = App::default(); - let cw20_id = app.store_code(cw20_contract()); - let gov_id = app.store_code(cw_core_contract()); + let mock = MockBech32::new("mock"); + let cw20 = Cw20Base::new("cw20", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + cw20.upload().unwrap(); + let cw20_id = cw20.code_id().unwrap(); + gov.upload().unwrap(); let cw20_instantiate = cw20_base::msg::InstantiateMsg { name: "DAO".to_string(), @@ -133,12 +107,9 @@ fn test_instantiate_with_n_gov_modules(n: usize) { .collect(), initial_items: None, }; - let gov_addr = instantiate_gov(&mut app, gov_id, instantiate); + gov.instantiate(&instantiate, None, None).unwrap(); - let state: DumpStateResponse = app - .wrap() - .query_wasm_smart(gov_addr, &QueryMsg::DumpState {}) - .unwrap(); + let state = gov.dump_state().unwrap(); assert_eq!( state.config, @@ -173,11 +144,16 @@ fn test_valid_instantiate() { } #[test] -#[should_panic(expected = "Error parsing into type cw20_base::msg::InstantiateMsg: Invalid type")] +#[should_panic( + expected = "Error parsing into type abstract_cw20_base::msg::InstantiateMsg: Invalid type" +)] fn test_instantiate_with_submessage_failure() { - let mut app = App::default(); - let cw20_id = app.store_code(cw20_contract()); - let gov_id = app.store_code(cw_core_contract()); + let mock = MockBech32::new("mock"); + let cw20 = Cw20Base::new("cw20", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + cw20.upload().unwrap(); + let cw20_id = cw20.code_id().unwrap(); + gov.upload().unwrap(); let cw20_instantiate = cw20_base::msg::InstantiateMsg { name: "DAO".to_string(), @@ -233,17 +209,21 @@ makes wickedness." proposal_modules_instantiate_info: governance_modules, initial_items: None, }; - instantiate_gov(&mut app, gov_id, instantiate); + + gov.instantiate(&instantiate, None, None).unwrap(); } #[test] -fn test_update_config() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let gov_id = app.store_code(cw_core_contract()); +fn test_update_config() -> cw_orch::anyhow::Result<()> { + let mock = MockBech32::new("mock"); + let gov_mod = DaoProposalSudo::new("proposal", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + gov_mod.upload()?; + let govmod_id = gov_mod.code_id()?; + gov.upload()?; let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let gov_instantiate = InstantiateMsg { @@ -256,14 +236,14 @@ fn test_update_config() { automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { code_id: govmod_id, - msg: to_json_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate)?, admin: Some(Admin::CoreModule {}), funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, - msg: to_json_binary(&govmod_instantiate).unwrap(), + msg: to_json_binary(&govmod_instantiate)?, admin: Some(Admin::CoreModule {}), funds: vec![], label: "voting module".to_string(), @@ -271,29 +251,11 @@ fn test_update_config() { initial_items: None, }; - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); - - let modules: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + gov.instantiate(&gov_instantiate, None, None)?; + let modules = gov.proposal_modules(None, None)?; assert_eq!(modules.len(), 1); + gov_mod.set_address(&modules[0].clone().address); let expected_config = Config { name: "Root DAO".to_string(), @@ -304,45 +266,31 @@ fn test_update_config() { dao_uri: Some("https://daostar.one/EIP".to_string()), }; - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - modules[0].clone().address, - &dao_proposal_sudo::msg::ExecuteMsg::Execute { - msgs: vec![WasmMsg::Execute { - contract_addr: gov_addr.to_string(), - funds: vec![], - msg: to_json_binary(&ExecuteMsg::UpdateConfig { - config: expected_config.clone(), - }) - .unwrap(), - } - .into()], - }, - &[], - ) - .unwrap(); - - let config: Config = app - .wrap() - .query_wasm_smart(gov_addr.clone(), &QueryMsg::Config {}) - .unwrap(); + gov_mod.proposal_execute(vec![WasmMsg::Execute { + contract_addr: gov.address()?.to_string(), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::UpdateConfig { + config: expected_config.clone(), + })?, + } + .into()])?; - assert_eq!(expected_config, config); + assert_eq!(expected_config, gov.config()?); - let dao_uri: DaoURIResponse = app - .wrap() - .query_wasm_smart(gov_addr, &QueryMsg::DaoURI {}) - .unwrap(); - assert_eq!(dao_uri.dao_uri, expected_config.dao_uri); + assert_eq!(gov.dao_uri()?.dao_uri, expected_config.dao_uri); + Ok(()) } fn test_swap_governance(swaps: Vec<(u32, u32)>) { - let mut app = App::default(); - let propmod_id = app.store_code(sudo_proposal_contract()); - let core_id = app.store_code(cw_core_contract()); + let mock = MockBech32::new("mock"); + let gov_mod = DaoProposalSudo::new("proposal", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + gov_mod.upload().unwrap(); + let propmod_id = gov_mod.code_id().unwrap(); + gov.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let gov_instantiate = InstantiateMsg { @@ -370,31 +318,12 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { initial_items: None, }; - let gov_addr = app - .instantiate_contract( - core_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); - - let modules: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + gov.instantiate(&gov_instantiate, None, None).unwrap(); + let modules = gov.proposal_modules(None, None).unwrap(); assert_eq!(modules.len(), 1); + let module_count = gov.proposal_module_count().unwrap(); - let module_count = query_proposal_module_count(&app, &gov_addr); assert_eq!( module_count, ProposalModuleCountResponse { @@ -410,19 +339,12 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { .unwrap_or((0, 0)); for (add, remove) in swaps { - let start_modules: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let start_modules = gov.proposal_modules(None, None).unwrap(); - let start_modules_active: Vec = get_active_modules(&app, gov_addr.clone()); + let start_modules_active: Vec = get_active_modules(&gov); + get_active_modules(&gov); + gov_mod.set_address(&start_modules_active[0].address.clone()); let to_add: Vec<_> = (0..add) .map(|n| ModuleInstantiateInfo { code_id: propmod_id, @@ -440,23 +362,17 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { .map(|a| a.address.to_string()) .collect(); - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - start_modules_active[0].address.clone(), - &dao_proposal_sudo::msg::ExecuteMsg::Execute { - msgs: vec![WasmMsg::Execute { - contract_addr: gov_addr.to_string(), - funds: vec![], - msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) - .unwrap(), - } - .into()], - }, - &[], - ) - .unwrap(); + gov_mod + .proposal_execute(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) + .unwrap(), + } + .into()]) + .unwrap(); - let finish_modules_active = get_active_modules(&app, gov_addr.clone()); + let finish_modules_active = get_active_modules(&gov); assert_eq!( finish_modules_active.len() as u32, @@ -471,11 +387,7 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { assert!(!finish_modules_active.contains(&module)) } - let state: DumpStateResponse = app - .wrap() - .query_wasm_smart(gov_addr.clone(), &QueryMsg::DumpState {}) - .unwrap(); - + let state: DumpStateResponse = gov.dump_state().unwrap(); assert_eq!( state.active_proposal_module_count, finish_modules_active.len() as u32 @@ -487,7 +399,7 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { ) } - let module_count = query_proposal_module_count(&app, &gov_addr); + let module_count = gov.proposal_module_count().unwrap(); assert_eq!( module_count, ProposalModuleCountResponse { @@ -516,12 +428,15 @@ fn test_swap_governance_bad() { #[test] fn test_removed_modules_can_not_execute() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let gov_id = app.store_code(cw_core_contract()); + let mock = MockBech32::new("mock"); + let gov_mod = DaoProposalSudo::new("proposal", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + gov_mod.upload().unwrap(); + let govmod_id = gov_mod.code_id().unwrap(); + gov.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let gov_instantiate = InstantiateMsg { @@ -549,31 +464,14 @@ fn test_removed_modules_can_not_execute() { initial_items: None, }; - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); + gov.instantiate(&gov_instantiate, None, None).unwrap(); - let modules: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let modules = gov.proposal_modules(None, None).unwrap(); assert_eq!(modules.len(), 1); let start_module = modules.into_iter().next().unwrap(); + gov_mod.set_address(&start_module.address); let to_add = vec![ModuleInstantiateInfo { code_id: govmod_id, @@ -586,23 +484,16 @@ fn test_removed_modules_can_not_execute() { let to_disable = vec![start_module.address.to_string()]; // Swap ourselves out. - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - start_module.address.clone(), - &dao_proposal_sudo::msg::ExecuteMsg::Execute { - msgs: vec![WasmMsg::Execute { - contract_addr: gov_addr.to_string(), - funds: vec![], - msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) - .unwrap(), - } - .into()], - }, - &[], - ) - .unwrap(); + gov_mod + .proposal_execute(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }).unwrap(), + } + .into()]) + .unwrap(); - let finish_modules_active: Vec = get_active_modules(&app, gov_addr.clone()); + let finish_modules_active = get_active_modules(&gov); let new_proposal_module = finish_modules_active.into_iter().next().unwrap(); @@ -617,74 +508,54 @@ fn test_removed_modules_can_not_execute() { }]; let to_disable = vec![new_proposal_module.address.to_string()]; - let err: ContractError = app - .execute_contract( - Addr::unchecked(CREATOR_ADDR), - start_module.address, - &dao_proposal_sudo::msg::ExecuteMsg::Execute { - msgs: vec![WasmMsg::Execute { - contract_addr: gov_addr.to_string(), - funds: vec![], - msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { - to_add: to_add.clone(), - to_disable: to_disable.clone(), - }) - .unwrap(), - } - .into()], - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!( + let err = gov_mod + .proposal_execute(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { + to_add: to_add.clone(), + to_disable: to_disable.clone(), + }) + .unwrap(), + } + .into()]) + .unwrap_err(); + + assert_contains( err, ContractError::ModuleDisabledCannotExecute { - address: _gov_address - } - )); + address: Addr::unchecked(""), + }, + ); // Check that the enabled query works. - let enabled_modules: Vec = app - .wrap() - .query_wasm_smart( - &gov_addr, - &QueryMsg::ActiveProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let enabled_modules = gov.active_proposal_modules(None, None).unwrap(); assert_eq!(enabled_modules, vec![new_proposal_module.clone()]); // The new proposal module should be able to perform actions. - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - new_proposal_module.address, - &dao_proposal_sudo::msg::ExecuteMsg::Execute { - msgs: vec![WasmMsg::Execute { - contract_addr: gov_addr.to_string(), - funds: vec![], - msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }) - .unwrap(), - } - .into()], - }, - &[], - ) - .unwrap(); + gov_mod.set_address(&new_proposal_module.address); + gov_mod + .proposal_execute(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { to_add, to_disable }).unwrap(), + } + .into()]) + .unwrap(); } #[test] fn test_module_already_disabled() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let gov_id = app.store_code(cw_core_contract()); + let mock = MockBech32::new("mock"); + let gov_mod = DaoProposalSudo::new("proposal", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + gov_mod.upload().unwrap(); + let govmod_id = gov_mod.code_id().unwrap(); + gov.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let gov_instantiate = InstantiateMsg { @@ -712,81 +583,56 @@ fn test_module_already_disabled() { initial_items: None, }; - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); - - let modules: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); - + gov.instantiate(&gov_instantiate, None, None).unwrap(); + let modules = gov.proposal_modules(None, None).unwrap(); assert_eq!(modules.len(), 1); let start_module = modules.into_iter().next().unwrap(); + gov_mod.set_address(&start_module.address); let to_disable = vec![ start_module.address.to_string(), start_module.address.to_string(), ]; - let err: ContractError = app - .execute_contract( - Addr::unchecked(CREATOR_ADDR), - start_module.address.clone(), - &dao_proposal_sudo::msg::ExecuteMsg::Execute { - msgs: vec![WasmMsg::Execute { - contract_addr: gov_addr.to_string(), + let err = gov_mod + .proposal_execute(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { + to_add: vec![ModuleInstantiateInfo { + code_id: govmod_id, + msg: to_json_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), funds: vec![], - msg: to_json_binary(&ExecuteMsg::UpdateProposalModules { - to_add: vec![ModuleInstantiateInfo { - code_id: govmod_id, - msg: to_json_binary(&govmod_instantiate).unwrap(), - admin: Some(Admin::CoreModule {}), - funds: vec![], - label: "governance module".to_string(), - }], - to_disable, - }) - .unwrap(), - } - .into()], - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); + label: "governance module".to_string(), + }], + to_disable, + }) + .unwrap(), + } + .into()]) + .unwrap_err(); - assert_eq!( + assert_contains( err, ContractError::ModuleAlreadyDisabled { - address: start_module.address - } - ) + address: start_module.address, + }, + ); } #[test] fn test_swap_voting_module() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let gov_id = app.store_code(cw_core_contract()); + let mock = MockBech32::new("mock"); + let gov_mod = DaoProposalSudo::new("proposal", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + gov_mod.upload().unwrap(); + let govmod_id = gov_mod.code_id().unwrap(); + gov.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let gov_instantiate = InstantiateMsg { @@ -814,85 +660,51 @@ fn test_swap_voting_module() { initial_items: None, }; - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); - - let voting_addr: Addr = app - .wrap() - .query_wasm_smart(gov_addr.clone(), &QueryMsg::VotingModule {}) - .unwrap(); - - let modules: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); - + gov.instantiate(&gov_instantiate, None, None).unwrap(); + let modules = gov.proposal_modules(None, None).unwrap(); assert_eq!(modules.len(), 1); + gov_mod.set_address(&modules[0].address); - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - modules[0].address.clone(), - &dao_proposal_sudo::msg::ExecuteMsg::Execute { - msgs: vec![WasmMsg::Execute { - contract_addr: gov_addr.to_string(), - funds: vec![], - msg: to_json_binary(&ExecuteMsg::UpdateVotingModule { - module: ModuleInstantiateInfo { - code_id: govmod_id, - msg: to_json_binary(&govmod_instantiate).unwrap(), - admin: Some(Admin::CoreModule {}), - funds: vec![], - label: "voting module".to_string(), - }, - }) - .unwrap(), - } - .into()], - }, - &[], - ) - .unwrap(); + let voting_addr = gov.voting_module().unwrap(); - let new_voting_addr: Addr = app - .wrap() - .query_wasm_smart(gov_addr, &QueryMsg::VotingModule {}) + gov_mod + .proposal_execute(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + funds: vec![], + msg: to_json_binary(&ExecuteMsg::UpdateVotingModule { + module: ModuleInstantiateInfo { + code_id: govmod_id, + msg: to_json_binary(&govmod_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + }) + .unwrap(), + } + .into()]) .unwrap(); - assert_ne!(new_voting_addr, voting_addr); + assert_ne!(gov.voting_module().unwrap(), voting_addr); } -fn test_unauthorized(app: &mut App, gov_addr: Addr, msg: ExecuteMsg) { - let err: ContractError = app - .execute_contract(Addr::unchecked(CREATOR_ADDR), gov_addr, &msg, &[]) - .unwrap_err() - .downcast() - .unwrap(); +fn test_unauthorized(gov: &DaoDaoCore, msg: ExecuteMsg) { + let err = gov.execute(&msg, None).unwrap_err(); - assert_eq!(err, ContractError::Unauthorized {}); + assert_contains(err, ContractError::Unauthorized {}); } #[test] fn test_permissions() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let gov_id = app.store_code(cw_core_contract()); + let mock = MockBech32::new("mock"); + let gov_mod = DaoProposalSudo::new("proposal", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + gov_mod.upload().unwrap(); + let govmod_id = gov_mod.code_id().unwrap(); + gov.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let gov_instantiate = InstantiateMsg { @@ -920,20 +732,10 @@ fn test_permissions() { automatically_add_cw721s: true, }; - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); + gov.instantiate(&gov_instantiate, None, None).unwrap(); test_unauthorized( - &mut app, - gov_addr.clone(), + &gov, ExecuteMsg::UpdateVotingModule { module: ModuleInstantiateInfo { code_id: govmod_id, @@ -946,8 +748,7 @@ fn test_permissions() { ); test_unauthorized( - &mut app, - gov_addr.clone(), + &gov, ExecuteMsg::UpdateProposalModules { to_add: vec![], to_disable: vec![], @@ -955,8 +756,7 @@ fn test_permissions() { ); test_unauthorized( - &mut app, - gov_addr, + &gov, ExecuteMsg::UpdateConfig { config: Config { dao_uri: None, @@ -970,48 +770,62 @@ fn test_permissions() { ); } -fn do_standard_instantiate(auto_add: bool, admin: Option) -> (Addr, App) { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let voting_id = app.store_code(cw20_balances_voting()); - let gov_id = app.store_code(cw_core_contract()); - let cw20_id = app.store_code(cw20_contract()); +fn do_standard_instantiate( + auto_add: bool, + admin: bool, +) -> ( + DaoDaoCore, + DaoProposalSudo, + MockBech32, + Option, +) { + let mock = MockBech32::new("mock"); + let gov_mod = DaoProposalSudo::new("proposal", mock.clone()); + let voting = DaoVotingCw20Balance::new("dao-voting", mock.clone()); + let mut gov = DaoDaoCore::new("dao-core", mock.clone()); + let cw20 = Cw20Base::new("cw20", mock.clone()); + + gov_mod.upload().unwrap(); + voting.upload().unwrap(); + gov.upload().unwrap(); + cw20.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { token_info: dao_voting_cw20_balance::msg::TokenInfo::New { - code_id: cw20_id, + code_id: cw20.code_id().unwrap(), label: "DAO DAO voting".to_string(), name: "DAO DAO".to_string(), symbol: "DAO".to_string(), decimals: 6, initial_balances: vec![cw20::Cw20Coin { - address: CREATOR_ADDR.to_string(), + address: mock.sender_addr().to_string(), amount: Uint128::from(2u64), }], marketing: None, }, }; + let admin = admin.then(|| mock.addr_make("admin")); let gov_instantiate = InstantiateMsg { dao_uri: None, - admin, + admin: admin.as_ref().map(|a| a.to_string()), name: "DAO DAO".to_string(), description: "A DAO that builds DAOs.".to_string(), image_url: None, automatically_add_cw20s: auto_add, automatically_add_cw721s: auto_add, voting_module_instantiate_info: ModuleInstantiateInfo { - code_id: voting_id, + code_id: voting.code_id().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, + code_id: gov_mod.code_id().unwrap(), msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), funds: vec![], @@ -1020,653 +834,374 @@ fn do_standard_instantiate(auto_add: bool, admin: Option) -> (Addr, App) initial_items: None, }; - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); + gov.instantiate(&gov_instantiate, None, None).unwrap(); + + let proposal_modules = gov.proposal_modules(None, None).unwrap(); + assert_eq!(proposal_modules.len(), 1); + let proposal_module = proposal_modules.into_iter().next().unwrap(); + gov_mod.set_address(&proposal_module.address); + + if admin.is_none() { + gov = gov.call_as(&gov.address().unwrap()); + } - (gov_addr, app) + (gov, gov_mod, mock, admin) } #[test] fn test_admin_permissions() { - let (core_addr, mut app) = do_standard_instantiate(true, None); - - let start_height = app.block_info().height; - let proposal_modules: Vec = app - .wrap() - .query_wasm_smart( - core_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let (core, proposal, mock, _) = do_standard_instantiate(true, false); - assert_eq!(proposal_modules.len(), 1); - let proposal_module = proposal_modules.into_iter().next().unwrap(); + let random = mock.addr_make("random"); + let start_height = mock.block_info().unwrap().height; // Random address can't call ExecuteAdminMsgs - let res = app.execute_contract( - Addr::unchecked("random"), - core_addr.clone(), - &ExecuteMsg::ExecuteAdminMsgs { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ); - res.unwrap_err(); + core.call_as(&random) + .execute_admin_msgs(vec![WasmMsg::Execute { + contract_addr: core.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap_err(); // Proposal module can't call ExecuteAdminMsgs - let res = app.execute_contract( - proposal_module.address.clone(), - core_addr.clone(), - &ExecuteMsg::ExecuteAdminMsgs { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ); - res.unwrap_err(); + core.call_as(&proposal.address().unwrap()) + .execute_admin_msgs(vec![WasmMsg::Execute { + contract_addr: core.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap_err(); // Update Admin can't be called by non-admins - let res = app.execute_contract( - Addr::unchecked("rando"), - core_addr.clone(), - &ExecuteMsg::NominateAdmin { - admin: Some("rando".to_string()), - }, - &[], - ); - res.unwrap_err(); + core.call_as(&random) + .nominate_admin(Some(random.to_string())) + .unwrap_err(); // Nominate admin can be called by core contract as no admin was // specified so the admin defaulted to the core contract. - let res = app.execute_contract( - proposal_module.address.clone(), - core_addr.clone(), - &ExecuteMsg::ExecuteProposalHook { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::NominateAdmin { - admin: Some("meow".to_string()), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ); - res.unwrap(); + + core.call_as(&proposal.address().unwrap()) + .execute_proposal_hook(vec![WasmMsg::Execute { + contract_addr: core.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap(); // Instantiate new DAO with an admin - let (core_with_admin_addr, mut app) = - do_standard_instantiate(true, Some(Addr::unchecked("admin").to_string())); + let (core_with_admin, proposal_with_admin_address, mock, admin) = + do_standard_instantiate(true, true); + let admin = admin.unwrap(); // Non admins still can't call ExecuteAdminMsgs - let res = app.execute_contract( - proposal_module.address, - core_with_admin_addr.clone(), - &ExecuteMsg::ExecuteAdminMsgs { - msgs: vec![WasmMsg::Execute { - contract_addr: core_with_admin_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ); - res.unwrap_err(); + core_with_admin + .call_as(&proposal_with_admin_address.address().unwrap()) + .execute_admin_msgs(vec![WasmMsg::Execute { + contract_addr: core_with_admin.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap_err(); // Admin cannot directly pause the DAO - let res = app.execute_contract( - Addr::unchecked("admin"), - core_with_admin_addr.clone(), - &ExecuteMsg::Pause { - duration: Duration::Height(10), - }, - &[], - ); - assert!(res.is_err()); + core_with_admin + .call_as(&admin) + .pause(Duration::Height(10)) + .unwrap_err(); // Random person cannot pause the DAO - let res = app.execute_contract( - Addr::unchecked("random"), - core_with_admin_addr.clone(), - &ExecuteMsg::Pause { - duration: Duration::Height(10), - }, - &[], - ); - assert!(res.is_err()); + core_with_admin + .call_as(&random) + .pause(Duration::Height(10)) + .unwrap_err(); // Admin can call ExecuteAdminMsgs, here an admin pauses the DAO - let res = app.execute_contract( - Addr::unchecked("admin"), - core_with_admin_addr.clone(), - &ExecuteMsg::ExecuteAdminMsgs { - msgs: vec![WasmMsg::Execute { - contract_addr: core_with_admin_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ); - assert!(res.is_ok()); + let _res = core_with_admin + .call_as(&admin) + .execute_admin_msgs(vec![WasmMsg::Execute { + contract_addr: core_with_admin.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap(); // Ensure we are paused for 10 blocks - let paused: PauseInfoResponse = app - .wrap() - .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::PauseInfo {}) - .unwrap(); assert_eq!( - paused, + core_with_admin.pause_info().unwrap(), PauseInfoResponse::Paused { expiration: Expiration::AtHeight(start_height + 10) } ); // DAO unpauses after 10 blocks - app.update_block(|block| block.height += 11); + mock.wait_blocks(11).unwrap(); // Check we are unpaused - let paused: PauseInfoResponse = app - .wrap() - .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::PauseInfo {}) - .unwrap(); - assert_eq!(paused, PauseInfoResponse::Unpaused {}); + assert_eq!( + core_with_admin.pause_info().unwrap(), + PauseInfoResponse::Unpaused {} + ); // Admin pauses DAO again - let res = app.execute_contract( - Addr::unchecked("admin"), - core_with_admin_addr.clone(), - &ExecuteMsg::ExecuteAdminMsgs { - msgs: vec![WasmMsg::Execute { - contract_addr: core_with_admin_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ); - assert!(res.is_ok()); + let _res = core_with_admin + .call_as(&admin) + .execute_admin_msgs(vec![WasmMsg::Execute { + contract_addr: core_with_admin.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap(); // DAO with admin cannot unpause itself - let res = app.execute_contract( - core_with_admin_addr.clone(), - core_with_admin_addr.clone(), - &ExecuteMsg::Unpause {}, - &[], - ); - assert!(res.is_err()); + let _res = core_with_admin + .call_as(&core_with_admin.address().unwrap()) + .unpause() + .unwrap_err(); // Random person cannot unpause the DAO - let res = app.execute_contract( - Addr::unchecked("random"), - core_with_admin_addr.clone(), - &ExecuteMsg::Unpause {}, - &[], - ); - assert!(res.is_err()); + let _res = core_with_admin.call_as(&random).unpause().unwrap_err(); // Admin can unpause the DAO directly - let res = app.execute_contract( - Addr::unchecked("admin"), - core_with_admin_addr.clone(), - &ExecuteMsg::Unpause {}, - &[], - ); - assert!(res.is_ok()); + let _res = core_with_admin.call_as(&admin).unpause().unwrap(); // Check we are unpaused - let paused: PauseInfoResponse = app - .wrap() - .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::PauseInfo {}) - .unwrap(); - assert_eq!(paused, PauseInfoResponse::Unpaused {}); - // Admin can nominate a new admin. - let res = app.execute_contract( - Addr::unchecked("admin"), - core_with_admin_addr.clone(), - &ExecuteMsg::NominateAdmin { - admin: Some("meow".to_string()), - }, - &[], + assert_eq!( + core_with_admin.pause_info().unwrap(), + PauseInfoResponse::Unpaused {} ); - res.unwrap(); - let nomination: AdminNominationResponse = app - .wrap() - .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::AdminNomination {}) + // Admin can nominate a new admin. + let new_admin = mock.addr_make("meow"); + core_with_admin + .call_as(&admin) + .nominate_admin(Some(new_admin.to_string())) .unwrap(); + assert_eq!( - nomination, + core_with_admin.admin_nomination().unwrap(), AdminNominationResponse { - nomination: Some(Addr::unchecked("meow")) + nomination: Some(new_admin.clone()) } ); // Check that admin has not yet been updated - let res: Addr = app - .wrap() - .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::Admin {}) - .unwrap(); - assert_eq!(res, Addr::unchecked("admin")); + assert_eq!(core_with_admin.admin().unwrap(), admin); // Only the nominated address may accept the nomination. - let err: ContractError = app - .execute_contract( - Addr::unchecked("random"), - core_with_admin_addr.clone(), - &ExecuteMsg::AcceptAdminNomination {}, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::Unauthorized {}); + let err = core_with_admin + .call_as(&random) + .accept_admin_nomination() + .unwrap_err(); + + assert_contains(err, ContractError::Unauthorized {}); // Accept the nomination. - app.execute_contract( - Addr::unchecked("meow"), - core_with_admin_addr.clone(), - &ExecuteMsg::AcceptAdminNomination {}, - &[], - ) - .unwrap(); + core_with_admin + .call_as(&new_admin) + .accept_admin_nomination() + .unwrap(); // Check that admin has been updated - let res: Addr = app - .wrap() - .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::Admin {}) - .unwrap(); - assert_eq!(res, Addr::unchecked("meow")); + assert_eq!(core_with_admin.admin().unwrap(), new_admin); // Check that the pending admin has been cleared. - let nomination: AdminNominationResponse = app - .wrap() - .query_wasm_smart(core_with_admin_addr, &QueryMsg::AdminNomination {}) - .unwrap(); - assert_eq!(nomination, AdminNominationResponse { nomination: None }); + assert_eq!( + core_with_admin.admin_nomination().unwrap(), + AdminNominationResponse { nomination: None } + ); } #[test] fn test_admin_nomination() { - let (core_addr, mut app) = do_standard_instantiate(true, Some("admin".to_string())); + let (core, _, mock, admin) = do_standard_instantiate(true, true); + let admin = admin.unwrap(); // Check that there is no pending nominations. - let nomination: AdminNominationResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {}) - .unwrap(); - assert_eq!(nomination, AdminNominationResponse { nomination: None }); + assert_eq!( + core.admin_nomination().unwrap(), + AdminNominationResponse { nomination: None } + ); // Nominate a new admin. - app.execute_contract( - Addr::unchecked("admin"), - core_addr.clone(), - &ExecuteMsg::NominateAdmin { - admin: Some("ekez".to_string()), - }, - &[], - ) - .unwrap(); + let ekez = mock.addr_make("ekez"); + core.call_as(&admin) + .nominate_admin(Some(ekez.to_string())) + .unwrap(); // Check that the nomination is in place. - let nomination: AdminNominationResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {}) - .unwrap(); assert_eq!( - nomination, + core.admin_nomination().unwrap(), AdminNominationResponse { - nomination: Some(Addr::unchecked("ekez")) + nomination: Some(ekez.clone()) } ); // Non-admin can not withdraw. - let err: ContractError = app - .execute_contract( - Addr::unchecked("ekez"), - core_addr.clone(), - &ExecuteMsg::WithdrawAdminNomination {}, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::Unauthorized {}); + let err = core.call_as(&ekez).withdraw_admin_nomination().unwrap_err(); + assert_contains(err, ContractError::Unauthorized {}); // Admin can withdraw. - app.execute_contract( - Addr::unchecked("admin"), - core_addr.clone(), - &ExecuteMsg::WithdrawAdminNomination {}, - &[], - ) - .unwrap(); + core.call_as(&admin).withdraw_admin_nomination().unwrap(); // Check that the nomination is withdrawn. - let nomination: AdminNominationResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {}) - .unwrap(); - assert_eq!(nomination, AdminNominationResponse { nomination: None }); + assert_eq!( + core.admin_nomination().unwrap(), + AdminNominationResponse { nomination: None } + ); // Can not withdraw if no nomination is pending. - let err: ContractError = app - .execute_contract( - Addr::unchecked("admin"), - core_addr.clone(), - &ExecuteMsg::WithdrawAdminNomination {}, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::NoAdminNomination {}); + let err = core + .call_as(&admin) + .withdraw_admin_nomination() + .unwrap_err(); + + assert_contains(err, ContractError::NoAdminNomination {}); // Can not claim nomination b/c it has been withdrawn. - let err: ContractError = app - .execute_contract( - Addr::unchecked("ekez"), - core_addr.clone(), - &ExecuteMsg::AcceptAdminNomination {}, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::NoAdminNomination {}); + let err = core.call_as(&admin).accept_admin_nomination().unwrap_err(); + + assert_contains(err, ContractError::NoAdminNomination {}); // Nominate a new admin. - app.execute_contract( - Addr::unchecked("admin"), - core_addr.clone(), - &ExecuteMsg::NominateAdmin { - admin: Some("meow".to_string()), - }, - &[], - ) - .unwrap(); + let meow = mock.addr_make("meow"); + core.call_as(&admin) + .nominate_admin(Some(meow.to_string())) + .unwrap(); // A new nomination can not be created if there is already a // pending nomination. - let err: ContractError = app - .execute_contract( - Addr::unchecked("admin"), - core_addr.clone(), - &ExecuteMsg::NominateAdmin { - admin: Some("arthur".to_string()), - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::PendingNomination {}); + let err = core + .call_as(&admin) + .nominate_admin(Some(ekez.to_string())) + .unwrap_err(); + assert_contains(err, ContractError::PendingNomination {}); // Only nominated admin may accept. - let err: ContractError = app - .execute_contract( - Addr::unchecked("ekez"), - core_addr.clone(), - &ExecuteMsg::AcceptAdminNomination {}, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::Unauthorized {}); + let err = core.call_as(&ekez).accept_admin_nomination().unwrap_err(); + assert_contains(err, ContractError::Unauthorized {}); - app.execute_contract( - Addr::unchecked("meow"), - core_addr.clone(), - &ExecuteMsg::AcceptAdminNomination {}, - &[], - ) - .unwrap(); + core.call_as(&meow).accept_admin_nomination().unwrap(); // Check that meow is the new admin. - let admin: Addr = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::Admin {}) - .unwrap(); - assert_eq!(admin, Addr::unchecked("meow".to_string())); + assert_eq!(core.admin().unwrap(), meow); - let start_height = app.block_info().height; + let start_height = mock.block_info().unwrap().height; // Check that the new admin can do admin things and the old can not. - let err: ContractError = app - .execute_contract( - Addr::unchecked("admin"), - core_addr.clone(), - &ExecuteMsg::ExecuteAdminMsgs { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ) - .unwrap_err() - .downcast() + let err = core + .call_as(&admin) + .execute_admin_msgs(vec![WasmMsg::Execute { + contract_addr: core.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap_err(); + assert_contains(err, ContractError::Unauthorized {}); + + core.call_as(&meow) + .execute_admin_msgs(vec![WasmMsg::Execute { + contract_addr: core.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) .unwrap(); - assert_eq!(err, ContractError::Unauthorized {}); - - let res = app.execute_contract( - Addr::unchecked("meow"), - core_addr.clone(), - &ExecuteMsg::ExecuteAdminMsgs { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ); - res.unwrap(); - let paused: PauseInfoResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) - .unwrap(); assert_eq!( - paused, + core.pause_info().unwrap(), PauseInfoResponse::Paused { expiration: Expiration::AtHeight(start_height + 10) } ); // DAO unpauses after 10 blocks - app.update_block(|block| block.height += 11); + mock.wait_blocks(11).unwrap(); // Remove the admin. - app.execute_contract( - Addr::unchecked("meow"), - core_addr.clone(), - &ExecuteMsg::NominateAdmin { admin: None }, - &[], - ) - .unwrap(); + core.call_as(&meow).nominate_admin(None).unwrap(); // Check that this has not caused an admin to be nominated. - let nomination: AdminNominationResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {}) - .unwrap(); - assert_eq!(nomination, AdminNominationResponse { nomination: None }); + assert_eq!( + core.admin_nomination().unwrap(), + AdminNominationResponse { nomination: None } + ); // Check that admin has been updated. As there was no admin // nominated the admin should revert back to the contract address. - let res: Addr = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::Admin {}) - .unwrap(); - assert_eq!(res, core_addr); + assert_eq!(core.admin().unwrap(), core.address().unwrap()); } #[test] fn test_passthrough_voting_queries() { - let (gov_addr, app) = do_standard_instantiate(true, None); - - let creator_voting_power: VotingPowerAtHeightResponse = app - .wrap() - .query_wasm_smart( - gov_addr, - &QueryMsg::VotingPowerAtHeight { - address: CREATOR_ADDR.to_string(), - height: None, - }, - ) - .unwrap(); + let (gov, _, mock, _) = do_standard_instantiate(true, false); assert_eq!( - creator_voting_power, + gov.voting_power_at_height(mock.sender_addr().to_string(), None) + .unwrap(), VotingPowerAtHeightResponse { power: Uint128::from(2u64), - height: app.block_info().height, + height: mock.block_info().unwrap().height, } ); } -fn set_item(app: &mut App, gov_addr: Addr, key: String, value: String) { - app.execute_contract( - gov_addr.clone(), - gov_addr, - &ExecuteMsg::SetItem { key, value }, - &[], - ) - .unwrap(); -} - -fn remove_item(app: &mut App, gov_addr: Addr, key: String) { - app.execute_contract( - gov_addr.clone(), - gov_addr, - &ExecuteMsg::RemoveItem { key }, - &[], - ) - .unwrap(); -} - -fn get_item(app: &mut App, gov_addr: Addr, key: String) -> GetItemResponse { - app.wrap() - .query_wasm_smart(gov_addr, &QueryMsg::GetItem { key }) - .unwrap() -} - -fn list_items( - app: &mut App, - gov_addr: Addr, - start_at: Option, - limit: Option, -) -> Vec<(String, String)> { - app.wrap() - .query_wasm_smart( - gov_addr, - &QueryMsg::ListItems { - start_after: start_at, - limit, - }, - ) - .unwrap() -} - #[test] fn test_item_permissions() { - let (gov_addr, mut app) = do_standard_instantiate(true, None); - - let err: ContractError = app - .execute_contract( - Addr::unchecked("ekez"), - gov_addr.clone(), - &ExecuteMsg::SetItem { - key: "k".to_string(), - value: "v".to_string(), - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::Unauthorized {}); - - let err: ContractError = app - .execute_contract( - Addr::unchecked("ekez"), - gov_addr, - &ExecuteMsg::RemoveItem { - key: "k".to_string(), - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::Unauthorized {}); + let (gov, _, mock, _) = do_standard_instantiate(true, false); + + let ekez = mock.addr_make("ekez"); + let err = gov + .call_as(&ekez) + .set_item("k".to_string(), "v".to_string()) + .unwrap_err(); + assert_contains(err, ContractError::Unauthorized {}); + + let err = gov.call_as(&ekez).remove_item("k".to_string()).unwrap_err(); + assert_contains(err, ContractError::Unauthorized {}); } #[test] fn test_add_remove_get() { - let (gov_addr, mut app) = do_standard_instantiate(true, None); + let (gov, _, _mock, _) = do_standard_instantiate(true, false); - let a = get_item(&mut app, gov_addr.clone(), "aaaaa".to_string()); + let a = gov.get_item("aaaaa".to_string()).unwrap(); assert_eq!(a, GetItemResponse { item: None }); - set_item( - &mut app, - gov_addr.clone(), - "aaaaakey".to_string(), - "aaaaaaddr".to_string(), - ); - let a = get_item(&mut app, gov_addr.clone(), "aaaaakey".to_string()); + gov.set_item("aaaaakey".to_string(), "aaaaaaddr".to_string()) + .unwrap(); + let a = gov.get_item("aaaaakey".to_string()).unwrap(); assert_eq!( a, GetItemResponse { @@ -1674,38 +1209,42 @@ fn test_add_remove_get() { } ); - remove_item(&mut app, gov_addr.clone(), "aaaaakey".to_string()); - let a = get_item(&mut app, gov_addr, "aaaaakey".to_string()); + gov.remove_item("aaaaakey".to_string()).unwrap(); + let a = gov.get_item("aaaaakey".to_string()).unwrap(); assert_eq!(a, GetItemResponse { item: None }); } #[test] #[should_panic(expected = "Key is missing from storage")] fn test_remove_missing_key() { - let (gov_addr, mut app) = do_standard_instantiate(true, None); - remove_item(&mut app, gov_addr, "b".to_string()) + let (gov, _, _, _) = do_standard_instantiate(true, false); + gov.remove_item("b".to_string()).unwrap(); } #[test] fn test_list_items() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let voting_id = app.store_code(cw20_balances_voting()); - let gov_id = app.store_code(cw_core_contract()); - let cw20_id = app.store_code(cw20_contract()); - + let mock = MockBech32::new("mock"); + let govmod = DaoProposalSudo::new("proposal", mock.clone()); + let voting = DaoVotingCw20Balance::new("dao-voting", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + let cw20 = Cw20Base::new("cw20", mock.clone()); + + govmod.upload().unwrap(); + voting.upload().unwrap(); + gov.upload().unwrap(); + cw20.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { token_info: dao_voting_cw20_balance::msg::TokenInfo::New { - code_id: cw20_id, + code_id: cw20.code_id().unwrap(), label: "DAO DAO voting".to_string(), name: "DAO DAO".to_string(), symbol: "DAO".to_string(), decimals: 6, initial_balances: vec![cw20::Cw20Coin { - address: CREATOR_ADDR.to_string(), + address: mock.sender_addr().to_string(), amount: Uint128::from(2u64), }], marketing: None, @@ -1721,14 +1260,14 @@ fn test_list_items() { automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { - code_id: voting_id, + code_id: voting.code_id().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, + code_id: govmod.code_id().unwrap(), msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), funds: vec![], @@ -1737,64 +1276,41 @@ fn test_list_items() { initial_items: None, }; - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); + gov.instantiate(&gov_instantiate, None, None).unwrap(); + let gov = gov.call_as(&gov.address().unwrap()); - set_item( - &mut app, - gov_addr.clone(), - "fookey".to_string(), - "fooaddr".to_string(), - ); - set_item( - &mut app, - gov_addr.clone(), - "barkey".to_string(), - "baraddr".to_string(), - ); - set_item( - &mut app, - gov_addr.clone(), - "loremkey".to_string(), - "loremaddr".to_string(), - ); - set_item( - &mut app, - gov_addr.clone(), - "ipsumkey".to_string(), - "ipsumaddr".to_string(), - ); + gov.set_item("fookey".to_string(), "fooaddr".to_string()) + .unwrap(); + gov.set_item("barkey".to_string(), "baraddr".to_string()) + .unwrap(); + gov.set_item("loremkey".to_string(), "loremaddr".to_string()) + .unwrap(); + gov.set_item("ipsumkey".to_string(), "ipsumaddr".to_string()) + .unwrap(); // Foo returned as we are only getting one item and items are in // decending order. - let first_item = list_items(&mut app, gov_addr.clone(), None, Some(1)); + let first_item = gov.list_items(Some(1), None).unwrap(); assert_eq!(first_item.len(), 1); assert_eq!( first_item[0], ("loremkey".to_string(), "loremaddr".to_string()) ); - let no_items = list_items(&mut app, gov_addr.clone(), None, Some(0)); + let no_items = gov.list_items(Some(0), None).unwrap(); assert_eq!(no_items.len(), 0); // Items are retreived in decending order so asking for foo with // no limit ought to give us the barkey k/v. this will be the last item // note: the paginate map bound is exclusive, so fookey will be starting point - let last_item = list_items(&mut app, gov_addr.clone(), Some("foo".to_string()), None); + let last_item = gov.list_items(None, Some("foo".to_string())).unwrap(); + assert_eq!(last_item.len(), 1); assert_eq!(last_item[0], ("barkey".to_string(), "baraddr".to_string())); // Items are retreived in decending order so asking for ipsum with // 4 limit ought to give us the fookey and barkey k/vs. - let after_foo_list = list_items(&mut app, gov_addr, Some("ipsum".to_string()), Some(4)); + let after_foo_list = gov.list_items(Some(4), Some("ipsum".to_string())).unwrap(); assert_eq!(after_foo_list.len(), 2); assert_eq!( after_foo_list, @@ -1807,24 +1323,29 @@ fn test_list_items() { #[test] fn test_instantiate_with_items() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let voting_id = app.store_code(cw20_balances_voting()); - let gov_id = app.store_code(cw_core_contract()); - let cw20_id = app.store_code(cw20_contract()); + let mock = MockBech32::new("mock"); + let govmod = DaoProposalSudo::new("proposal", mock.clone()); + let voting = DaoVotingCw20Balance::new("dao-voting", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + let cw20 = Cw20Base::new("cw20", mock.clone()); + + govmod.upload().unwrap(); + voting.upload().unwrap(); + gov.upload().unwrap(); + cw20.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { token_info: dao_voting_cw20_balance::msg::TokenInfo::New { - code_id: cw20_id, + code_id: cw20.code_id().unwrap(), label: "DAO DAO voting".to_string(), name: "DAO DAO".to_string(), symbol: "DAO".to_string(), decimals: 6, initial_balances: vec![cw20::Cw20Coin { - address: CREATOR_ADDR.to_string(), + address: mock.sender_addr().to_string(), amount: Uint128::from(2u64), }], marketing: None, @@ -1855,14 +1376,14 @@ fn test_instantiate_with_items() { automatically_add_cw20s: true, automatically_add_cw721s: true, voting_module_instantiate_info: ModuleInstantiateInfo { - code_id: voting_id, + code_id: voting.code_id().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, + code_id: govmod.code_id().unwrap(), msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), funds: vec![], @@ -1872,45 +1393,26 @@ fn test_instantiate_with_items() { }; // Ensure duplicates are dissallowed. - let err: ContractError = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!( + let err = gov.instantiate(&gov_instantiate, None, None).unwrap_err(); + assert_contains( err, ContractError::DuplicateInitialItem { - item: "item0".to_string() - } + item: "item0".to_string(), + }, ); initial_items.pop(); gov_instantiate.initial_items = Some(initial_items); - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); + let _gov_addr = gov.instantiate(&gov_instantiate, None, None).unwrap(); // Ensure initial items were added. - let items = list_items(&mut app, gov_addr.clone(), None, None); + let items = gov.list_items(None, None).unwrap(); assert_eq!(items.len(), 2); // Descending order, so item1 is first. assert_eq!(items[1].0, "item0".to_string()); - let get_item0 = get_item(&mut app, gov_addr.clone(), "item0".to_string()); + let get_item0 = gov.get_item("item0".to_string()).unwrap(); + assert_eq!( get_item0, GetItemResponse { @@ -1919,20 +1421,18 @@ fn test_instantiate_with_items() { ); assert_eq!(items[0].0, "item1".to_string()); - let item1_value = get_item(&mut app, gov_addr, "item1".to_string()).item; + let item1_value = gov.get_item("item1".to_string()).unwrap().item; assert_eq!(item1_value, Some("item1_value".to_string())) } #[test] fn test_cw20_receive_auto_add() { - let (gov_addr, mut app) = do_standard_instantiate(true, None); - - let cw20_id = app.store_code(cw20_contract()); - let another_cw20 = app - .instantiate_contract( - cw20_id, - Addr::unchecked(CREATOR_ADDR), - &cw20_base::msg::InstantiateMsg { + let (gov, _proposal, mock, _) = do_standard_instantiate(true, false); + let another_cw20 = Cw20Base::new("another-cw20", mock.clone()); + another_cw20.upload().unwrap(); + another_cw20 + .instantiate( + &abstract_cw20_base::msg::InstantiateMsg { name: "DAO".to_string(), symbol: "DAO".to_string(), decimals: 6, @@ -1940,146 +1440,82 @@ fn test_cw20_receive_auto_add() { mint: None, marketing: None, }, - &[], - "another-token", + None, None, ) .unwrap(); - let voting_module: Addr = app - .wrap() - .query_wasm_smart(gov_addr.clone(), &QueryMsg::VotingModule {}) - .unwrap(); - let gov_token: Addr = app - .wrap() - .query_wasm_smart( - voting_module, - &dao_interface::voting::Query::TokenContract {}, - ) - .unwrap(); + let voting = DaoVotingCw20Balance::new("dao-voting", mock.clone()); + voting.set_address(&gov.voting_module().unwrap()); + + let gov_token = Cw20Base::new("cw20", mock.clone()); + gov_token.set_address(&voting.token_contract().unwrap()); // Check that the balances query works with no tokens. - let cw20_balances: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::Cw20Balances { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let cw20_balances = gov.cw_20_balances(None, None).unwrap(); assert_eq!(cw20_balances, vec![]); // Send a gov token to the governance contract. - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - gov_token.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: gov_addr.to_string(), - amount: Uint128::new(1), - msg: to_json_binary(&"").unwrap(), - }, - &[], - ) - .unwrap(); - - let cw20_list: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::Cw20TokenList { - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(cw20_list, vec![gov_token.clone()]); - - let cw20_balances: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::Cw20Balances { - start_after: None, - limit: None, - }, + gov_token + .send( + Uint128::new(1), + gov.address().unwrap().to_string(), + to_json_binary(&"").unwrap(), ) .unwrap(); + + let cw20_list = gov.cw_20_token_list(None, None).unwrap(); + assert_eq!( + cw20_list, + vec![gov_token.address().unwrap().to_string().clone()] + ); + assert_eq!( - cw20_balances, + gov.cw_20_balances(None, None).unwrap(), vec![Cw20BalanceResponse { - addr: gov_token.clone(), + addr: gov_token.address().unwrap(), balance: Uint128::new(1), }] ); // Test removing and adding some new ones. Invalid should fail. - let err: ContractError = app - .execute_contract( - Addr::unchecked(gov_addr.clone()), - gov_addr.clone(), - &ExecuteMsg::UpdateCw20List { - to_add: vec!["new".to_string()], - to_remove: vec![gov_token.to_string()], - }, - &[], + let err = gov + .update_cw_20_list( + vec![mock.addr_make("new").to_string()], + vec![gov_token.address().unwrap().to_string()], ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!(err, ContractError::Std(_))); + .unwrap_err(); + println!("{:?}", err); + assert_contains(&err, "key:"); + assert_contains(err, "not found"); // Test that non-DAO can not update the list. - let err: ContractError = app - .execute_contract( - Addr::unchecked("ekez"), - gov_addr.clone(), - &ExecuteMsg::UpdateCw20List { - to_add: vec![], - to_remove: vec![gov_token.to_string()], - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!(err, ContractError::Unauthorized {})); - - app.execute_contract( - Addr::unchecked(gov_addr.clone()), - gov_addr.clone(), - &ExecuteMsg::UpdateCw20List { - to_add: vec![another_cw20.to_string()], - to_remove: vec![gov_token.to_string()], - }, - &[], + let err = gov + .call_as(&mock.addr_make("ekez")) + .update_cw_20_list(vec![], vec![gov_token.address().unwrap().to_string()]) + .unwrap_err(); + + assert_contains(err, ContractError::Unauthorized {}); + + gov.update_cw_20_list( + vec![another_cw20.address().unwrap().to_string()], + vec![gov_token.address().unwrap().to_string()], ) .unwrap(); - let cw20_list: Vec = app - .wrap() - .query_wasm_smart( - gov_addr, - &QueryMsg::Cw20TokenList { - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(cw20_list, vec![another_cw20]); + let cw20_list = gov.cw_20_token_list(None, None).unwrap(); + assert_eq!(cw20_list, vec![another_cw20.address().unwrap().to_string()]); } #[test] fn test_cw20_receive_no_auto_add() { - let (gov_addr, mut app) = do_standard_instantiate(false, None); - - let cw20_id = app.store_code(cw20_contract()); - let another_cw20 = app - .instantiate_contract( - cw20_id, - Addr::unchecked(CREATOR_ADDR), - &cw20_base::msg::InstantiateMsg { + let (gov, _proposal, mock, _) = do_standard_instantiate(false, false); + + let another_cw20 = Cw20Base::new("another-cw20", mock.clone()); + another_cw20.upload().unwrap(); + another_cw20 + .instantiate( + &abstract_cw20_base::msg::InstantiateMsg { name: "DAO".to_string(), symbol: "DAO".to_string(), decimals: 6, @@ -2087,532 +1523,345 @@ fn test_cw20_receive_no_auto_add() { mint: None, marketing: None, }, - &[], - "another-token", + None, None, ) .unwrap(); - let voting_module: Addr = app - .wrap() - .query_wasm_smart(gov_addr.clone(), &QueryMsg::VotingModule {}) - .unwrap(); - let gov_token: Addr = app - .wrap() - .query_wasm_smart( - voting_module, - &dao_interface::voting::Query::TokenContract {}, - ) - .unwrap(); + let voting = DaoVotingCw20Balance::new("dao-voting", mock.clone()); + voting.set_address(&gov.voting_module().unwrap()); + + let gov_token = Cw20Base::new("cw20", mock.clone()); + gov_token.set_address(&voting.token_contract().unwrap()); // Send a gov token to the governance contract. Should not be // added becasue auto add is turned off. - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - gov_token.clone(), - &cw20::Cw20ExecuteMsg::Send { - contract: gov_addr.to_string(), - amount: Uint128::new(1), - msg: to_json_binary(&"").unwrap(), - }, - &[], - ) - .unwrap(); - - let cw20_list: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::Cw20TokenList { - start_after: None, - limit: None, - }, + gov_token + .send( + Uint128::new(1), + gov.address().unwrap().to_string(), + to_json_binary(&"").unwrap(), ) .unwrap(); - assert_eq!(cw20_list, Vec::::new()); - - app.execute_contract( - Addr::unchecked(gov_addr.clone()), - gov_addr.clone(), - &ExecuteMsg::UpdateCw20List { - to_add: vec![another_cw20.to_string(), gov_token.to_string()], - to_remove: vec!["ok to remove non existent".to_string()], - }, - &[], + + assert_eq!( + gov.cw_20_token_list(None, None).unwrap(), + Vec::::new() + ); + + gov.update_cw_20_list( + vec![ + another_cw20.address().unwrap().to_string(), + gov_token.address().unwrap().to_string(), + ], + vec![mock.addr_make("ok to remove non existent").to_string()], ) .unwrap(); - let cw20_list: Vec = app - .wrap() - .query_wasm_smart( - gov_addr, - &QueryMsg::Cw20TokenList { - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(cw20_list, vec![another_cw20, gov_token]); + assert_eq!( + gov.cw_20_token_list(None, None).unwrap(), + vec![ + gov_token.address().unwrap(), + another_cw20.address().unwrap(), + ] + ); } #[test] fn test_cw721_receive() { - let (gov_addr, mut app) = do_standard_instantiate(true, None); - - let cw721_id = app.store_code(cw721_contract()); + let (gov, _proposal, mock, _) = do_standard_instantiate(true, false); - let cw721_addr = app - .instantiate_contract( - cw721_id, - Addr::unchecked(CREATOR_ADDR), + let cw721 = Cw721Base::new("cw721", mock.clone()); + cw721.upload().unwrap(); + cw721 + .instantiate( &cw721_base::msg::InstantiateMsg { name: "ekez".to_string(), symbol: "ekez".to_string(), - minter: CREATOR_ADDR.to_string(), + minter: mock.sender_addr().to_string(), }, - &[], - "cw721", + None, None, ) .unwrap(); - let another_cw721 = app - .instantiate_contract( - cw721_id, - Addr::unchecked(CREATOR_ADDR), + let another_cw721 = Cw721Base::new("another_cw721", mock.clone()); + another_cw721.set_code_id(cw721.code_id().unwrap()); + another_cw721 + .instantiate( &cw721_base::msg::InstantiateMsg { name: "ekez".to_string(), symbol: "ekez".to_string(), - minter: CREATOR_ADDR.to_string(), + minter: mock.sender_addr().to_string(), }, - &[], - "cw721", + None, None, ) .unwrap(); - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - cw721_addr.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint { - token_id: "ekez".to_string(), - owner: CREATOR_ADDR.to_string(), - token_uri: None, - extension: None, - }, - &[], - ) - .unwrap(); - - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - cw721_addr.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::SendNft { - contract: gov_addr.to_string(), - token_id: "ekez".to_string(), - msg: to_json_binary("").unwrap(), - }, - &[], - ) - .unwrap(); - - let cw721_list: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::Cw721TokenList { - start_after: None, - limit: None, + cw721 + .execute( + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { + token_id: "ekez".to_string(), + owner: mock.sender_addr().to_string(), + token_uri: None, + extension: None, }, + None, ) .unwrap(); - assert_eq!(cw721_list, vec![cw721_addr.clone()]); - // Try to add an invalid cw721. - let err: ContractError = app - .execute_contract( - Addr::unchecked(gov_addr.clone()), - gov_addr.clone(), - &ExecuteMsg::UpdateCw721List { - to_add: vec!["new".to_string(), cw721_addr.to_string()], - to_remove: vec![cw721_addr.to_string()], + cw721 + .execute( + &cw721_base::msg::ExecuteMsg::, Empty>::SendNft { + contract: gov.address().unwrap().to_string(), + token_id: "ekez".to_string(), + msg: to_json_binary("").unwrap(), }, - &[], + None, ) - .unwrap_err() - .downcast() .unwrap(); - assert!(matches!(err, ContractError::Std(_))); - // Test that non-DAO can not update the list. - let err: ContractError = app - .execute_contract( - Addr::unchecked("ekez"), - gov_addr.clone(), - &ExecuteMsg::UpdateCw721List { - to_add: vec![], - to_remove: vec![cw721_addr.to_string()], - }, - &[], + assert_eq!( + gov.cw_721_token_list(None, None).unwrap(), + vec![cw721.address().unwrap().clone()] + ); + + // Try to add an invalid cw721. + let err = gov + .update_cw_721_list( + vec![ + mock.addr_make("new").to_string(), + cw721.address().unwrap().clone().to_string(), + ], + vec![cw721.address().unwrap().clone().to_string()], ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!(err, ContractError::Unauthorized {})); + .unwrap_err(); + + println!("{:?}", err); + assert_contains(&err, "key:"); + assert_contains(err, "not found"); + // assert!(matches!(err, ContractError::Std(_))); + + // Test that non-DAO can not update the list. + let err = gov + .call_as(&mock.addr_make("ekez")) + .update_cw_721_list(vec![], vec![cw721.address().unwrap().clone().to_string()]) + .unwrap_err(); + + assert_contains(err, ContractError::Unauthorized {}); // Add a real cw721. - app.execute_contract( - Addr::unchecked(gov_addr.clone()), - gov_addr.clone(), - &ExecuteMsg::UpdateCw721List { - to_add: vec![another_cw721.to_string(), cw721_addr.to_string()], - to_remove: vec![cw721_addr.to_string()], - }, - &[], + gov.update_cw_721_list( + vec![ + cw721.address().unwrap().to_string(), + another_cw721.address().unwrap().to_string(), + ], + vec![cw721.address().unwrap().to_string()], ) .unwrap(); - let cw20_list: Vec = app - .wrap() - .query_wasm_smart( - gov_addr, - &QueryMsg::Cw721TokenList { - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(cw20_list, vec![another_cw721]); + assert_eq!( + gov.cw_721_token_list(None, None).unwrap(), + vec![another_cw721.address().unwrap()] + ); } #[test] fn test_cw721_receive_no_auto_add() { - let (gov_addr, mut app) = do_standard_instantiate(false, None); - - let cw721_id = app.store_code(cw721_contract()); + let (gov, _proposal, mock, _) = do_standard_instantiate(false, false); - let cw721_addr = app - .instantiate_contract( - cw721_id, - Addr::unchecked(CREATOR_ADDR), + let cw721 = Cw721Base::new("cw721", mock.clone()); + cw721.upload().unwrap(); + cw721 + .instantiate( &cw721_base::msg::InstantiateMsg { name: "ekez".to_string(), symbol: "ekez".to_string(), - minter: CREATOR_ADDR.to_string(), + minter: mock.sender_addr().to_string(), }, - &[], - "cw721", + None, None, ) .unwrap(); - let another_cw721 = app - .instantiate_contract( - cw721_id, - Addr::unchecked(CREATOR_ADDR), + let another_cw721 = Cw721Base::new("another_cw721", mock.clone()); + another_cw721.set_code_id(cw721.code_id().unwrap()); + another_cw721 + .instantiate( &cw721_base::msg::InstantiateMsg { name: "ekez".to_string(), symbol: "ekez".to_string(), - minter: CREATOR_ADDR.to_string(), + minter: mock.sender_addr().to_string(), }, - &[], - "cw721", + None, None, ) .unwrap(); - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - cw721_addr.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint { - token_id: "ekez".to_string(), - owner: CREATOR_ADDR.to_string(), - token_uri: None, - extension: None, - }, - &[], - ) - .unwrap(); - - app.execute_contract( - Addr::unchecked(CREATOR_ADDR), - cw721_addr.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::SendNft { - contract: gov_addr.to_string(), - token_id: "ekez".to_string(), - msg: to_json_binary("").unwrap(), - }, - &[], - ) - .unwrap(); - - let cw721_list: Vec = app - .wrap() - .query_wasm_smart( - gov_addr.clone(), - &QueryMsg::Cw721TokenList { - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(cw721_list, Vec::::new()); + assert_eq!( + gov.cw_721_token_list(None, None).unwrap(), + Vec::::new() + ); // Duplicates OK. Just adds one. - app.execute_contract( - Addr::unchecked(gov_addr.clone()), - gov_addr.clone(), - &ExecuteMsg::UpdateCw721List { - to_add: vec![ - another_cw721.to_string(), - cw721_addr.to_string(), - cw721_addr.to_string(), - ], - to_remove: vec![], - }, - &[], + gov.update_cw_721_list( + vec![ + another_cw721.address().unwrap().to_string(), + cw721.address().unwrap().to_string(), + cw721.address().unwrap().to_string(), + ], + vec![], ) .unwrap(); - let cw20_list: Vec = app - .wrap() - .query_wasm_smart( - gov_addr, - &QueryMsg::Cw721TokenList { - start_after: None, - limit: None, - }, - ) - .unwrap(); - assert_eq!(cw20_list, vec![another_cw721, cw721_addr]); + assert_eq!( + gov.cw_721_token_list(None, None).unwrap(), + vec![another_cw721.address().unwrap(), cw721.address().unwrap()] + ); } #[test] fn test_pause() { - let (core_addr, mut app) = do_standard_instantiate(false, None); + let (gov, _proposal, mock, _) = do_standard_instantiate(false, false); - let start_height = app.block_info().height; - - let proposal_modules: Vec = app - .wrap() - .query_wasm_smart( - core_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let start_height = mock.block_info().unwrap().height; + let proposal_modules = gov.proposal_modules(None, None).unwrap(); assert_eq!(proposal_modules.len(), 1); let proposal_module = proposal_modules.into_iter().next().unwrap(); - let paused: PauseInfoResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) - .unwrap(); - assert_eq!(paused, PauseInfoResponse::Unpaused {}); - let all_state: DumpStateResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {}) - .unwrap(); - assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); + assert_eq!(gov.pause_info().unwrap(), PauseInfoResponse::Unpaused {}); + + assert_eq!( + gov.dump_state().unwrap().pause_info, + PauseInfoResponse::Unpaused {} + ); // DAO is not paused. Check that we can execute things. // // Tests intentionally use the core address to send these // messsages to simulate a worst case scenerio where the core // contract has a vulnerability. - app.execute_contract( - core_addr.clone(), - core_addr.clone(), - &ExecuteMsg::UpdateConfig { - config: Config { - dao_uri: None, - name: "The Empire Strikes Back".to_string(), - description: "haha lol we have pwned your DAO".to_string(), - image_url: None, - automatically_add_cw20s: true, - automatically_add_cw721s: true, - }, - }, - &[], - ) + gov.update_config(Config { + dao_uri: None, + name: "The Empire Strikes Back".to_string(), + description: "haha lol we have pwned your DAO".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + }) .unwrap(); // Oh no the DAO is under attack! Quick! Pause the DAO while we // figure out what to do! - let err: ContractError = app - .execute_contract( - proposal_module.address.clone(), - core_addr.clone(), - &ExecuteMsg::Pause { - duration: Duration::Height(10), - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); + let err = gov + .call_as(&proposal_module.address) + .pause(Duration::Height(10)) + .unwrap_err(); // Only the DAO may call this on itself. Proposal modules must use // the execute hook. - assert_eq!(err, ContractError::Unauthorized {}); - - app.execute_contract( - proposal_module.address.clone(), - core_addr.clone(), - &ExecuteMsg::ExecuteProposalHook { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ) - .unwrap(); - - let paused: PauseInfoResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) + assert_contains(err, ContractError::Unauthorized {}); + gov.call_as(&proposal_module.address) + .execute_proposal_hook(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) .unwrap(); + assert_eq!( - paused, + gov.pause_info().unwrap(), PauseInfoResponse::Paused { expiration: Expiration::AtHeight(start_height + 10) } ); - let all_state: DumpStateResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {}) - .unwrap(); assert_eq!( - all_state.pause_info, + gov.dump_state().unwrap().pause_info, PauseInfoResponse::Paused { expiration: Expiration::AtHeight(start_height + 10) } ); // This should actually be allowed to enable the admin to execute - let result = app.execute_contract( - core_addr.clone(), - core_addr.clone(), - &ExecuteMsg::UpdateConfig { - config: Config { - dao_uri: None, - name: "The Empire Strikes Back Again".to_string(), - description: "haha lol we have pwned your DAO again".to_string(), - image_url: None, - automatically_add_cw20s: true, - automatically_add_cw721s: true, - }, - }, - &[], - ); - assert!(result.is_ok()); - - let err: ContractError = app - .execute_contract( - proposal_module.address.clone(), - core_addr.clone(), - &ExecuteMsg::ExecuteProposalHook { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); + gov.update_config(Config { + dao_uri: None, + name: "The Empire Strikes Back Again".to_string(), + description: "haha lol we have pwned your DAO again".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + }) + .unwrap(); + + let err = gov + .call_as(&proposal_module.address) + .execute_proposal_hook(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap_err(); - assert!(matches!(err, ContractError::Paused { .. })); + assert_contains(err, ContractError::Paused {}); - app.update_block(|block| block.height += 9); + mock.wait_blocks(9).unwrap(); // Still not unpaused. - let err: ContractError = app - .execute_contract( - proposal_module.address.clone(), - core_addr.clone(), - &ExecuteMsg::ExecuteProposalHook { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - assert!(matches!(err, ContractError::Paused { .. })); + let err = gov + .call_as(&proposal_module.address) + .execute_proposal_hook(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) + .unwrap_err(); + + assert_contains(err, ContractError::Paused {}); - app.update_block(|block| block.height += 1); + mock.wait_blocks(1).unwrap(); - let paused: PauseInfoResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) - .unwrap(); - assert_eq!(paused, PauseInfoResponse::Unpaused {}); - let all_state: DumpStateResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {}) - .unwrap(); - assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); + assert_eq!(gov.pause_info().unwrap(), PauseInfoResponse::Unpaused {}); + assert_eq!( + gov.dump_state().unwrap().pause_info, + PauseInfoResponse::Unpaused {} + ); // Now its unpaused so we should be able to pause again. - app.execute_contract( - proposal_module.address, - core_addr.clone(), - &ExecuteMsg::ExecuteProposalHook { - msgs: vec![WasmMsg::Execute { - contract_addr: core_addr.to_string(), - msg: to_json_binary(&ExecuteMsg::Pause { - duration: Duration::Height(10), - }) - .unwrap(), - funds: vec![], - } - .into()], - }, - &[], - ) - .unwrap(); - - let paused: PauseInfoResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {}) + gov.call_as(&proposal_module.address) + .execute_proposal_hook(vec![WasmMsg::Execute { + contract_addr: gov.address().unwrap().to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()]) .unwrap(); + assert_eq!( - paused, + gov.pause_info().unwrap(), PauseInfoResponse::Paused { expiration: Expiration::AtHeight(start_height + 20) } ); - let all_state: DumpStateResponse = app - .wrap() - .query_wasm_smart(core_addr, &QueryMsg::DumpState {}) - .unwrap(); assert_eq!( - all_state.pause_info, + gov.dump_state().unwrap().pause_info, PauseInfoResponse::Paused { expiration: Expiration::AtHeight(start_height + 20) } @@ -2621,25 +1870,13 @@ fn test_pause() { #[test] fn test_dump_state_proposal_modules() { - let (core_addr, app) = do_standard_instantiate(false, None); - let proposal_modules: Vec = app - .wrap() - .query_wasm_smart( - core_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let (gov, _proposal, _mock, _) = do_standard_instantiate(false, false); + let proposal_modules = gov.proposal_modules(None, None).unwrap(); assert_eq!(proposal_modules.len(), 1); let proposal_module = proposal_modules.into_iter().next().unwrap(); - let all_state: DumpStateResponse = app - .wrap() - .query_wasm_smart(core_addr, &QueryMsg::DumpState {}) - .unwrap(); + let all_state: DumpStateResponse = gov.dump_state().unwrap(); assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {}); assert_eq!(all_state.proposal_modules.len(), 1); assert_eq!(all_state.proposal_modules[0], proposal_module); @@ -2650,24 +1887,29 @@ fn test_dump_state_proposal_modules() { // this just tests the idempotency of migrate. #[test] fn test_migrate_from_compatible() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let voting_id = app.store_code(cw20_balances_voting()); - let gov_id = app.store_code(cw_core_contract()); - let cw20_id = app.store_code(cw20_contract()); + let mock = MockBech32::new("mock"); + let govmod = DaoProposalSudo::new("proposal", mock.clone()); + let voting = DaoVotingCw20Balance::new("dao-voting", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + let cw20 = Cw20Base::new("cw20", mock.clone()); + + govmod.upload().unwrap(); + voting.upload().unwrap(); + gov.upload().unwrap(); + cw20.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { token_info: dao_voting_cw20_balance::msg::TokenInfo::New { - code_id: cw20_id, + code_id: cw20.code_id().unwrap(), label: "DAO DAO voting".to_string(), name: "DAO DAO".to_string(), symbol: "DAO".to_string(), decimals: 6, initial_balances: vec![cw20::Cw20Coin { - address: CREATOR_ADDR.to_string(), + address: mock.sender_addr().to_string(), amount: Uint128::from(2u64), }], marketing: None, @@ -2684,14 +1926,14 @@ fn test_migrate_from_compatible() { automatically_add_cw20s: false, automatically_add_cw721s: false, voting_module_instantiate_info: ModuleInstantiateInfo { - code_id: voting_id, + code_id: voting.code_id().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, + code_id: govmod.code_id().unwrap(), msg: to_json_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), funds: vec![], @@ -2700,37 +1942,16 @@ fn test_migrate_from_compatible() { initial_items: None, }; - let core_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - Some(CREATOR_ADDR.to_string()), - ) - .unwrap(); - - let state: DumpStateResponse = app - .wrap() - .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {}) + gov.instantiate(&gov_instantiate, Some(&mock.sender_addr()), None) .unwrap(); - app.execute( - Addr::unchecked(CREATOR_ADDR), - CosmosMsg::Wasm(WasmMsg::Migrate { - contract_addr: core_addr.to_string(), - new_code_id: gov_id, - msg: to_json_binary(&MigrateMsg::FromCompatible {}).unwrap(), - }), - ) - .unwrap(); + let state = gov.dump_state().unwrap(); - let new_state: DumpStateResponse = app - .wrap() - .query_wasm_smart(core_addr, &QueryMsg::DumpState {}) + gov.migrate(&MigrateMsg::FromCompatible {}, gov.code_id().unwrap()) .unwrap(); + let new_state = gov.dump_state().unwrap(); + assert_eq!(new_state, state); } @@ -2738,25 +1959,31 @@ fn test_migrate_from_compatible() { fn test_migrate_from_beta() { use cw_core_v1 as v1; - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let voting_id = app.store_code(cw20_balances_voting()); - let core_id = app.store_code(cw_core_contract()); - let v1_core_id = app.store_code(v1_cw_core_contract()); - let cw20_id = app.store_code(cw20_contract()); + let mock = MockBech32::new("mock"); + let govmod = DaoProposalSudo::new("proposal", mock.clone()); + let voting = DaoVotingCw20Balance::new("dao-voting", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + let v1_gov = DaoDaoCoreV1::new("dao-core-v1", mock.clone()); + let cw20 = Cw20Base::new("cw20", mock.clone()); + + govmod.upload().unwrap(); + voting.upload().unwrap(); + gov.upload().unwrap(); + v1_gov.upload().unwrap(); + cw20.upload().unwrap(); let proposal_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let voting_instantiate = dao_voting_cw20_balance::msg::InstantiateMsg { token_info: dao_voting_cw20_balance::msg::TokenInfo::New { - code_id: cw20_id, + code_id: cw20.code_id().unwrap(), label: "DAO DAO voting".to_string(), name: "DAO DAO".to_string(), symbol: "DAO".to_string(), decimals: 6, initial_balances: vec![cw20::Cw20Coin { - address: CREATOR_ADDR.to_string(), + address: mock.sender_addr().to_string(), amount: Uint128::from(2u64), }], marketing: None, @@ -2772,20 +1999,20 @@ fn test_migrate_from_beta() { automatically_add_cw20s: false, automatically_add_cw721s: false, voting_module_instantiate_info: v1::msg::ModuleInstantiateInfo { - code_id: voting_id, + code_id: voting.code_id().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, + code_id: govmod.code_id().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, + code_id: govmod.code_id().unwrap(), msg: to_json_binary(&proposal_instantiate).unwrap(), admin: v1::msg::Admin::CoreContract {}, label: "governance module 2".to_string(), @@ -2794,35 +2021,21 @@ fn test_migrate_from_beta() { initial_items: None, }; - let core_addr = app - .instantiate_contract( - v1_core_id, - Addr::unchecked(CREATOR_ADDR), - &v1_core_instantiate, - &[], - "cw-governance", - Some(CREATOR_ADDR.to_string()), - ) + v1_gov + .instantiate(&v1_core_instantiate, Some(&mock.sender_addr()), None) .unwrap(); - app.execute( - Addr::unchecked(CREATOR_ADDR), - CosmosMsg::Wasm(WasmMsg::Migrate { - contract_addr: core_addr.to_string(), - new_code_id: core_id, - msg: to_json_binary(&MigrateMsg::FromV1 { - dao_uri: None, - params: None, - }) - .unwrap(), - }), + gov.set_address(&v1_gov.address().unwrap()); + gov.migrate( + &MigrateMsg::FromV1 { + dao_uri: None, + params: None, + }, + gov.code_id().unwrap(), ) .unwrap(); - let new_state: DumpStateResponse = app - .wrap() - .query_wasm_smart(&core_addr, &QueryMsg::DumpState {}) - .unwrap(); + let new_state = gov.dump_state().unwrap(); let proposal_modules = new_state.proposal_modules; assert_eq!(2, proposal_modules.len()); @@ -2833,23 +2046,17 @@ fn test_migrate_from_beta() { } // Check that we may not migrate more than once. - let err: ContractError = app - .execute( - Addr::unchecked(CREATOR_ADDR), - CosmosMsg::Wasm(WasmMsg::Migrate { - contract_addr: core_addr.to_string(), - new_code_id: core_id, - msg: to_json_binary(&MigrateMsg::FromV1 { - dao_uri: None, - params: None, - }) - .unwrap(), - }), + let err = gov + .migrate( + &MigrateMsg::FromV1 { + dao_uri: None, + params: None, + }, + gov.code_id().unwrap(), ) - .unwrap_err() - .downcast() - .unwrap(); - assert_eq!(err, ContractError::AlreadyMigrated {}) + .unwrap_err(); + + assert_contains(err, ContractError::AlreadyMigrated {}) } #[test] @@ -2920,44 +2127,34 @@ fn test_migrate_mock() { #[test] fn test_execute_stargate_msg() { - let (core_addr, mut app) = do_standard_instantiate(true, None); - let proposal_modules: Vec = app - .wrap() - .query_wasm_smart( - core_addr.clone(), - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let (gov, _proposal, _mock, _) = do_standard_instantiate(true, false); + let proposal_modules = gov.proposal_modules(None, None).unwrap(); assert_eq!(proposal_modules.len(), 1); let proposal_module = proposal_modules.into_iter().next().unwrap(); - let res = app.execute_contract( - proposal_module.address, - core_addr, - &ExecuteMsg::ExecuteProposalHook { - msgs: vec![CosmosMsg::Stargate { - type_url: "foo_type".to_string(), - value: to_json_binary("foo_bin").unwrap(), - }], - }, - &[], - ); + let res = gov + .call_as(&proposal_module.address) + .execute_proposal_hook(vec![CosmosMsg::Stargate { + type_url: "foo_type".to_string(), + value: to_json_binary("foo_bin").unwrap(), + }]); + // TODO: Once cw-multi-test supports executing stargate/ibc messages we can change this test assert assert!(res.is_err()); } #[test] fn test_module_prefixes() { - let mut app = App::default(); - let govmod_id = app.store_code(sudo_proposal_contract()); - let gov_id = app.store_code(cw_core_contract()); + let mock = MockBech32::new("mock"); + let gov_mod = DaoProposalSudo::new("proposal", mock.clone()); + let gov = DaoDaoCore::new("dao-core", mock.clone()); + gov_mod.upload().unwrap(); + let govmod_id = gov_mod.code_id().unwrap(); + gov.upload().unwrap(); let govmod_instantiate = dao_proposal_sudo::msg::InstantiateMsg { - root: CREATOR_ADDR.to_string(), + root: mock.sender_addr().to_string(), }; let gov_instantiate = InstantiateMsg { @@ -3001,28 +2198,9 @@ fn test_module_prefixes() { initial_items: None, }; - let gov_addr = app - .instantiate_contract( - gov_id, - Addr::unchecked(CREATOR_ADDR), - &gov_instantiate, - &[], - "cw-governance", - None, - ) - .unwrap(); - - let modules: Vec = app - .wrap() - .query_wasm_smart( - gov_addr, - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); + gov.instantiate(&gov_instantiate, None, None).unwrap(); + let modules = gov.proposal_modules(None, None).unwrap(); assert_eq!(modules.len(), 3); let module_1 = &modules[0]; @@ -3032,26 +2210,17 @@ fn test_module_prefixes() { let module_2 = &modules[1]; assert_eq!(module_2.status, ProposalModuleStatus::Enabled {}); - assert_eq!(module_2.prefix, "B"); + assert_eq!(module_2.prefix, "C"); assert_eq!(&module_2.address, &modules[1].address); let module_3 = &modules[2]; assert_eq!(module_3.status, ProposalModuleStatus::Enabled {}); - assert_eq!(module_3.prefix, "C"); + assert_eq!(module_3.prefix, "B"); assert_eq!(&module_3.address, &modules[2].address); } -fn get_active_modules(app: &App, gov_addr: Addr) -> Vec { - let modules: Vec = app - .wrap() - .query_wasm_smart( - gov_addr, - &QueryMsg::ProposalModules { - start_after: None, - limit: None, - }, - ) - .unwrap(); +fn get_active_modules(gov: &DaoDaoCore) -> Vec { + let modules = gov.proposal_modules(None, None).unwrap(); modules .into_iter() @@ -3059,19 +2228,12 @@ fn get_active_modules(app: &App, gov_addr: Addr) -> Vec { .collect() } -fn query_proposal_module_count(app: &App, core_addr: &Addr) -> ProposalModuleCountResponse { - app.wrap() - .query_wasm_smart(core_addr, &QueryMsg::ProposalModuleCount {}) - .unwrap() -} - #[test] fn test_add_remove_subdaos() { - let (core_addr, mut app) = do_standard_instantiate(false, None); + let (gov, _proposal, mock, _) = do_standard_instantiate(false, false); test_unauthorized( - &mut app, - core_addr.clone(), + &gov.call_as(&mock.sender_addr()), ExecuteMsg::UpdateSubDaos { to_add: vec![], to_remove: vec![], @@ -3080,89 +2242,46 @@ fn test_add_remove_subdaos() { let to_add: Vec = vec![ SubDao { - addr: "subdao001".to_string(), + addr: mock.addr_make("subdao001").to_string(), charter: None, }, SubDao { - addr: "subdao002".to_string(), + addr: mock.addr_make("subdao002").to_string(), charter: Some("cool charter bro".to_string()), }, SubDao { - addr: "subdao005".to_string(), + addr: mock.addr_make("subdao005").to_string(), charter: None, }, SubDao { - addr: "subdao007".to_string(), + addr: mock.addr_make("subdao007").to_string(), charter: None, }, ]; let to_remove: Vec = vec![]; - app.execute_contract( - Addr::unchecked(core_addr.clone()), - core_addr.clone(), - &ExecuteMsg::UpdateSubDaos { to_add, to_remove }, - &[], - ) - .unwrap(); + gov.update_sub_daos(to_add, to_remove).unwrap(); - let res: Vec = app - .wrap() - .query_wasm_smart( - core_addr.clone(), - &QueryMsg::ListSubDaos { - start_after: None, - limit: None, - }, - ) - .unwrap(); + assert_eq!(gov.list_sub_daos(None, None).unwrap().len(), 4); - assert_eq!(res.len(), 4); + let to_remove: Vec = vec![mock.addr_make("subdao005").to_string()]; - let to_remove: Vec = vec!["subdao005".to_string()]; + gov.update_sub_daos(vec![], to_remove).unwrap(); - app.execute_contract( - Addr::unchecked(core_addr.clone()), - core_addr.clone(), - &ExecuteMsg::UpdateSubDaos { - to_add: vec![], - to_remove, - }, - &[], - ) - .unwrap(); - - let res: Vec = app - .wrap() - .query_wasm_smart( - core_addr, - &QueryMsg::ListSubDaos { - start_after: None, - limit: None, - }, - ) - .unwrap(); + let res = gov.list_sub_daos(None, None).unwrap(); assert_eq!(res.len(), 3); - - let test_res: SubDao = SubDao { - addr: "subdao002".to_string(), - charter: Some("cool charter bro".to_string()), - }; - - assert_eq!(res[1], test_res); - let full_result_set: Vec = vec![ SubDao { - addr: "subdao001".to_string(), + addr: mock.addr_make("subdao001").to_string(), charter: None, }, SubDao { - addr: "subdao002".to_string(), + addr: mock.addr_make("subdao002").to_string(), charter: Some("cool charter bro".to_string()), }, SubDao { - addr: "subdao007".to_string(), + addr: mock.addr_make("subdao007").to_string(), charter: None, }, ]; @@ -3182,13 +2301,9 @@ pub fn test_migrate_update_version() { #[test] fn test_query_info() { - let (core_addr, app) = do_standard_instantiate(true, None); - let res: InfoResponse = app - .wrap() - .query_wasm_smart(core_addr, &QueryMsg::Info {}) - .unwrap(); + let (gov, _, _, _) = do_standard_instantiate(true, false); assert_eq!( - res, + gov.info().unwrap(), InfoResponse { info: ContractVersion { contract: CONTRACT_NAME.to_string(), diff --git a/contracts/distribution/cw-fund-distributor/Cargo.toml b/contracts/distribution/cw-fund-distributor/Cargo.toml index f73aba4df..fdb1619f3 100644 --- a/contracts/distribution/cw-fund-distributor/Cargo.toml +++ b/contracts/distribution/cw-fund-distributor/Cargo.toml @@ -28,6 +28,7 @@ dao-voting-cw20-staked = { workspace = true } cw20-stake = { workspace = true, features = ["library"] } dao-interface = { workspace = true } cw-paginate-storage = { workspace = true } +cw-orch.workspace = true [dev-dependencies] dao-dao-core = { workspace = true, features = ["library"] } diff --git a/contracts/distribution/cw-fund-distributor/README.md b/contracts/distribution/cw-fund-distributor/README.md index 1df4f15bd..3896499c2 100644 --- a/contracts/distribution/cw-fund-distributor/README.md +++ b/contracts/distribution/cw-fund-distributor/README.md @@ -1,17 +1,21 @@ # cw-fund-distributor -This contract is meant to facilitate fund distribution +This contract is meant to facilitate fund distribution proportional to the amount of voting power members have at a given block height. Possible use cases may involve: + - Dissolving a DAO and distributing its treasury to members prior to shutting down - Distributing funds among DAO members - Funding subDAOs +> **WARNING:** THIS CONTRACT IS NOT AUDITED AND IS _EXPERIMENTAL_. USE AT YOUR +> OWN RISK. + ## Funding Period -Contract is instantiated with a `funding_period` - a time duration that should suffice +Contract is instantiated with a `funding_period` - a time duration that should suffice to move the funds to be distributed into the distributor contract. Funding the contract can only happen during this period. @@ -32,6 +36,6 @@ it is possible to redistribute the unclaimed funds. Only the `cw_admin` can call the method. The redistribution method finds all the claims that have been performed -and subtracts the amounts from the initially funded balance. The respective +and subtracts the amounts from the initially funded balance. The respective allocation ratios for each DAO member remain the same; any previous claims are cleared. diff --git a/contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json b/contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json index 02b368e07..63fc4b821 100644 --- a/contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json +++ b/contracts/distribution/cw-fund-distributor/schema/cw-fund-distributor.json @@ -1,6 +1,6 @@ { "contract_name": "cw-fund-distributor", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/distribution/cw-fund-distributor/src/msg.rs b/contracts/distribution/cw-fund-distributor/src/msg.rs index abb39389d..26c0984e4 100644 --- a/contracts/distribution/cw-fund-distributor/src/msg.rs +++ b/contracts/distribution/cw-fund-distributor/src/msg.rs @@ -13,11 +13,17 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { Receive(cw20::Cw20ReceiveMsg), + #[cw_orch(payable)] FundNative {}, - ClaimCW20 { tokens: Vec }, - ClaimNatives { denoms: Vec }, + ClaimCW20 { + tokens: Vec, + }, + ClaimNatives { + denoms: Vec, + }, ClaimAll {}, } diff --git a/contracts/distribution/dao-rewards-distributor/Cargo.toml b/contracts/distribution/dao-rewards-distributor/Cargo.toml index 0df6ef1d5..0a76bf320 100644 --- a/contracts/distribution/dao-rewards-distributor/Cargo.toml +++ b/contracts/distribution/dao-rewards-distributor/Cargo.toml @@ -1,6 +1,11 @@ [package] name = "dao-rewards-distributor" -authors = ["Ben2x4 ", "ekez ", "Jake Hartnell ", "bekauz "] +authors = [ + "Ben2x4 ", + "ekez ", + "Jake Hartnell ", + "bekauz ", +] description = "Distributes rewards based on DAO membership." edition = { workspace = true } license = { workspace = true } @@ -21,7 +26,7 @@ cosmwasm-schema = { workspace = true } cw2 = { workspace = true } cw4 = { workspace = true } cw20 = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } +cw20-base = { workspace = true, features = ["library"] } cw-controllers = { workspace = true } cw-ownable = { workspace = true } cw-storage-plus = { workspace = true } @@ -29,7 +34,9 @@ cw-utils = { workspace = true } dao-hooks = { workspace = true } dao-interface = { workspace = true } dao-voting = { workspace = true } +semver = { workspace = true } thiserror = { workspace = true } +cw-orch.workspace = true [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/distribution/dao-rewards-distributor/README.md b/contracts/distribution/dao-rewards-distributor/README.md index a2e355d14..cb78b5c62 100644 --- a/contracts/distribution/dao-rewards-distributor/README.md +++ b/contracts/distribution/dao-rewards-distributor/README.md @@ -1,31 +1,172 @@ # DAO Rewards Distributor -[![dao-rewards-distributor on crates.io](https://img.shields.io/crates/v/dao-rewards-distributor.svg?logo=rust)](https://crates.io/crates/dao-rewards-distributor) +[![dao-rewards-distributor on +crates.io](https://img.shields.io/crates/v/dao-rewards-distributor.svg?logo=rust)](https://crates.io/crates/dao-rewards-distributor) [![docs.rs](https://img.shields.io/docsrs/dao-rewards-distributor?logo=docsdotrs)](https://docs.rs/dao-rewards-distributor/latest/cw20_stake_external_rewards/) -The `dao-rewards-distributor` works in conjuction with DAO voting modules to provide rewards over time for DAO members. The contract supports both cw20 and native Cosmos SDK tokens. The following voting power modules are supported: +The `dao-rewards-distributor` works in conjuction with DAO voting modules to +provide rewards streamed over time for DAO members. The contract supports both +native and CW20 Cosmos SDK tokens. Any voting power module that supports the +standard DAO voting module interface is supported for deriving staking reward +allocations, as long it also supports voting power change hooks. This includes, +but is not limited to: + - `dao-voting-cw4`: for membership or group based DAOs - `dao-voting-cw20-staked`: for cw20 token based DAOs. - `dao-voting-cw721-staked`: for NFT based DAOs. - `dao-voting-token-staked`: for native and Token Factory token based DAOs. -NOTE: this contract is NOT AUDITED and is _experimental_. USE AT YOUR OWN RISK. - ## Instantiation and Setup -The contract is instantiated with a number of parameters: -- `owner`: The owner of the contract. Is able to fund the contract and update the reward duration. -- `vp_contract`: A DAO DAO voting power module contract address, used to determine membership in the DAO over time. -- `hook_caller`: An optional contract that is allowed to call voting power change hooks. Often, as in `dao-voting-token-staked` and `dao-voting-cw721-staked` the vp_contract calls hooks for power change events, but sometimes they are separate. For example, the `cw4-group` contract is separate from the `dao-voting-cw4` contract and since the `cw4-group` contract fires the membership change events, it's address would be used as the `hook_caller`. -- `reward_denom`: the denomination of the reward token, can be either a cw20 or native token. -- `reward_duration`: the time period over which rewards are to be paid out in blocks. +The contract is instantiated with a very minimal state. An optional `owner` can +be specified. If it is not, the owner is set to be the address instantiating the +contract. + +### Hooks -After instantiating the contract it is VITAL to setup the required hooks for it to work. This is because to pay out rewards accurately, this contract needs to know about staking or voting power changes in the DAO. +After instantiating the contract, it is VITAL to set up the required hooks for +it to work. This is because to pay out rewards accurately, this contract needs +to know about staking or voting power changes in the DAO as soon as they happen. + +This can be achieved using the `add_hook` method on contracts that support +voting power changes, such as: -This can be achieved using the `add_hook` method on contracts that support voting power changes, which are: - `cw4-group` - `dao-voting-cw721-staked` - `dao-voting-token-staked` - `cw20-stake` -Finally, the contract needs to be funded with a token matching the denom specified in the `reward_denom` field during instantiation. This can be achieved by calling the `fund` method on the `dao-rewards-distributor` smart contract, and sending along the appropriate funds. +### Creating a new distribution + +Only the `owner` can create new distributions. + +Creating a distribution requires the following configuration: + +- `denom`, which can be a native token or CW20 contract +- `emission_rate`, which determines how the rewards are distributed. there are 3 + options: + - `paused`: no rewards are distributed until the emission rate is updated + - `immediate`: funded rewards are distributed immediately to those with + voting power + - `linear`: `amount` of the denom is distributed to all applicable addresses + per `duration` of time, updating throughout based on changing voting power. + `duration` may be declared in either time (seconds) or blocks. if + `continuous` is true, it will backfill if there are funding gaps using + current voting power. some example configurations may be: + - `1000udenom` per `500 blocks` + - `10udenom` per `24 hours` + - `1udenom` per `1 second` +- `vp_contract` address, which will be used to determine the total and relative + address voting power for allocating the rewards on a pro-rata basis +- `hook_caller` address, which will be authorized to call back into this + contract with any voting power event changes. examples of such events may be: + - user staking tokens + - user unstaking tokens + - user cw-721 state change event + - cw-4 membership change event +- optional `withdraw_destination` address to be used when withdrawing (i.e. + unfunding the remainder of a previously funded distribution). this may be a + subDAO, for example. if not provided, the contract owner is used. + +You can fund a distribution at any point after it's been created, or during +creation if it's for a native token. CW20 tokens must be funded after creation. +Simply including native funds in the create message will suffice. For any token, +you can always top up the funds later, which extends the distribution period. + +### Funding a distribution + +Anyone can fund a distribution once it's been created. + +> **WARNING:** Do not transfer funds directly to the contract. You must use the +> `Fund` execution message in order for the contract to properly recognize and +> distribute the tokens. **Funds will be lost if you don't use the execution +> msg.** + +There are a few different emission rates. Below describes the funding behavior +while different emission rates are active. + +#### Linear + +Linear emissions can be continuous or not. + +When a linear emission is **continuous**, it will backfill rewards if there's a gap +between when it finishes distributing everything it's been funded with so far +and when it's funded next. This means that when another funding occurs after a +period of no more rewards being available, it will instantly distribute the +portion of the funds that corresponds with the time that passed in that gap. One +limitation is that it uses the current voting power to backfill. + +When a linear emission is **not continuous**, and a gap in reward funding occurs, it +will simply restart the distribution the next time it receives funding. This may +be less intuitive, but it doesn't suffer from the voting power limitation that +the continuous mode does. + +Upon funding, the start and end are computed based on the funds provided, the +configured emission rate, and whether or not it's set to the continuous mode. If +this is the first funding, or it's not continuous and we're restarting from the +current block, the start block is updated to the current block. The end block is +computed based on the start block and funding duration, calculated from the +emission rate and remaining funds, including any that already existed that have +not yet been distributed. + +Linear emissions can be extended indefinitely by continuously funding them. + +**Example:** if 100_000udenom were funded, and the configured emission rate is +1_000udenom per 100 blocks, we derive that there are 100_000/1_000 = 100 epochs +funded, each of which contain 100 blocks. We therefore funded 10_000 blocks of +rewards. + +#### Immediate + +When set to immediate, funding is immediately distributed based on the voting +power of the block funding occurs on. + +You may fund an immediate distribution as many times as you'd like to distribute +funds instantly to the current members of the DAO. + +#### Paused + +When set to paused, no rewards will be distributed. + +You may fund a paused distribution and accumulate rewards in the contract to be +distributed at a later date, since you can update the emission rate of a +distribution. + +Maybe you want to accumulate rewards in a paused state for a month, and then +distribute them instantly at the end of the month to the DAO. Or maybe you want +to pause an active linear emission, which will hold the funds in the contract +and not distribute any more than have already been distributed. + +### Updating emission rate and other distribution config + +Only the `owner` can update a distribution's config. + +Updating the emission rate preserves all previously distributed rewards and adds +it to a historical value (`historical_earned_puvp`), so updating does not +interfere with users who have not yet claimed their rewards. + +You can also update the `vp_contract`, `hook_caller`, and +`withdraw_destination`. + +> **WARNING:** You probably always want to update `vp_contract` and +> `hook_caller` together. Make sure you know what you're doing. And be sure to +> add/remove hooks on the old and new `hook_caller`s accordingly. + +### Withdrawing + +Only the `owner` can withdraw from a distribution. + +This is effectively the inverse of funding a distribution. If the current +distribution is inactive, meaning its emission rate is `paused`, `immediate`, or +`linear` with an expired distribution period (because the end block is in the +past), then there is nothing to withdraw. + +When rewards are being distributed, withdrawing ends the distribution early, +setting the end block to the current one, and clawing back the undistributed +funds to the specified `withdraw_destination`. Pending funds that have already +been distributed, even if not yet claimed, will remain in the contract to be +claimed. Withdrawing only applies to unallocated funds. + +### Claiming + +You can claim funds from a distribution that you have pending rewards for. diff --git a/contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json b/contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json index e7d00a62b..7a85ec819 100644 --- a/contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json +++ b/contracts/distribution/dao-rewards-distributor/schema/dao-rewards-distributor.json @@ -1,6 +1,6 @@ { "contract_name": "dao-rewards-distributor", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,7 @@ "type": "object", "properties": { "owner": { - "description": "The owner of the contract. Is able to fund the contract and update the reward duration.", + "description": "The owner of the contract. Is able to fund the contract and update the reward duration. If not provided, the instantiator is used.", "type": [ "string", "null" @@ -61,20 +61,68 @@ "additionalProperties": false }, { - "description": "Claims rewards for the sender.", + "description": "registers a new distribution", "type": "object", "required": [ - "claim" + "create" ], "properties": { - "claim": { + "create": { + "$ref": "#/definitions/CreateMsg" + } + }, + "additionalProperties": false + }, + { + "description": "updates the config for a distribution", + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { "type": "object", "required": [ - "denom" + "id" ], "properties": { - "denom": { - "type": "string" + "emission_rate": { + "description": "reward emission rate", + "anyOf": [ + { + "$ref": "#/definitions/EmissionRate" + }, + { + "type": "null" + } + ] + }, + "hook_caller": { + "description": "address that will update the reward split when the voting power distribution changes", + "type": [ + "string", + "null" + ] + }, + "id": { + "description": "distribution ID to update", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "vp_contract": { + "description": "address to query the voting power", + "type": [ + "string", + "null" + ] + }, + "withdraw_destination": { + "description": "destination address for reward clawbacks. defaults to owner", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false @@ -103,27 +151,28 @@ ], "properties": { "fund": { - "type": "object", - "additionalProperties": false + "$ref": "#/definitions/FundMsg" } }, "additionalProperties": false }, { - "description": "shuts down the rewards distributor. withdraws all future staking rewards back to the treasury. members can claim whatever they earned until this point.", + "description": "Claims rewards for the sender.", "type": "object", "required": [ - "shutdown" + "claim" ], "properties": { - "shutdown": { + "claim": { "type": "object", "required": [ - "denom" + "id" ], "properties": { - "denom": { - "type": "string" + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -132,38 +181,22 @@ "additionalProperties": false }, { - "description": "registers a new reward denom", + "description": "withdraws the undistributed rewards for a distribution. members can claim whatever they earned until this point. this is effectively an inverse to fund and does not affect any already-distributed rewards.", "type": "object", "required": [ - "register_reward_denom" + "withdraw" ], "properties": { - "register_reward_denom": { + "withdraw": { "type": "object", "required": [ - "denom", - "emission_rate", - "hook_caller", - "vp_contract" + "id" ], "properties": { - "denom": { - "$ref": "#/definitions/UncheckedDenom" - }, - "emission_rate": { - "$ref": "#/definitions/RewardEmissionRate" - }, - "hook_caller": { - "type": "string" - }, - "vp_contract": { - "type": "string" - }, - "withdraw_destination": { - "type": [ - "string", - "null" - ] + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -245,6 +278,49 @@ "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" }, + "CreateMsg": { + "type": "object", + "required": [ + "denom", + "emission_rate", + "hook_caller", + "vp_contract" + ], + "properties": { + "denom": { + "description": "denom to distribute", + "allOf": [ + { + "$ref": "#/definitions/UncheckedDenom" + } + ] + }, + "emission_rate": { + "description": "reward emission rate", + "allOf": [ + { + "$ref": "#/definitions/EmissionRate" + } + ] + }, + "hook_caller": { + "description": "address that will update the reward split when the voting power distribution changes", + "type": "string" + }, + "vp_contract": { + "description": "address to query the voting power", + "type": "string" + }, + "withdraw_destination": { + "description": "destination address for reward clawbacks. defaults to owner", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, "Cw20ReceiveMsg": { "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", "type": "object", @@ -300,6 +376,80 @@ } ] }, + "EmissionRate": { + "description": "defines how many tokens (amount) should be distributed per amount of time (duration). e.g. 5udenom per hour.", + "oneOf": [ + { + "description": "rewards are paused", + "type": "object", + "required": [ + "paused" + ], + "properties": { + "paused": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "rewards are distributed immediately", + "type": "object", + "required": [ + "immediate" + ], + "properties": { + "immediate": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "rewards are distributed at a constant rate", + "type": "object", + "required": [ + "linear" + ], + "properties": { + "linear": { + "type": "object", + "required": [ + "amount", + "continuous", + "duration" + ], + "properties": { + "amount": { + "description": "amount of tokens to distribute per amount of time", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "continuous": { + "description": "whether or not reward distribution is continuous: whether future funding after distribution finishes should be applied to the past, or rewards are paused once all funding has been distributed. all continuously backfilled rewards are distributed based on the current voting power.", + "type": "boolean" + }, + "duration": { + "description": "duration of time to distribute amount", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + } + }, + "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": [ @@ -347,6 +497,21 @@ } ] }, + "FundMsg": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "description": "distribution ID to fund", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "MemberChangedHookMsg": { "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a ExecuteMsg. This contains a list of all diffs on the given transaction.", "type": "object", @@ -450,23 +615,6 @@ } ] }, - "RewardEmissionRate": { - "description": "defines how many tokens (amount) should be distributed per amount of time (duration). e.g. 5udenom per hour.", - "type": "object", - "required": [ - "amount", - "duration" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "duration": { - "$ref": "#/definitions/Duration" - } - }, - "additionalProperties": false - }, "StakeChangedHookMsg": { "description": "An enum representing staking hooks.", "oneOf": [ @@ -587,13 +735,13 @@ "additionalProperties": false }, { - "description": "Returns the state of the registered reward distributions.", + "description": "Returns information about the ownership of this contract.", "type": "object", "required": [ - "rewards_state" + "ownership" ], "properties": { - "rewards_state": { + "ownership": { "type": "object", "additionalProperties": false } @@ -604,10 +752,10 @@ "description": "Returns the pending rewards for the given address.", "type": "object", "required": [ - "get_pending_rewards" + "pending_rewards" ], "properties": { - "get_pending_rewards": { + "pending_rewards": { "type": "object", "required": [ "address" @@ -615,6 +763,22 @@ "properties": { "address": { "type": "string" + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -623,33 +787,54 @@ "additionalProperties": false }, { - "description": "Returns information about the ownership of this contract.", + "description": "Returns the state of the given distribution.", "type": "object", "required": [ - "ownership" + "distribution" ], "properties": { - "ownership": { + "distribution": { "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, "additionalProperties": false } }, "additionalProperties": false }, { + "description": "Returns the state of all the distributions.", "type": "object", "required": [ - "denom_reward_state" + "distributions" ], "properties": { - "denom_reward_state": { + "distributions": { "type": "object", - "required": [ - "denom" - ], "properties": { - "denom": { - "type": "string" + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } }, "additionalProperties": false @@ -662,58 +847,56 @@ "migrate": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MigrateMsg", - "type": "string", - "enum": [] + "type": "object", + "additionalProperties": false }, "sudo": null, "responses": { - "denom_reward_state": { + "distribution": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "DenomRewardState", - "description": "the state of a denom's reward distribution", + "title": "DistributionState", + "description": "the state of a reward distribution", "type": "object", "required": [ + "active_epoch", "denom", - "emission_rate", - "ends_at", "funded_amount", + "historical_earned_puvp", "hook_caller", - "last_update", - "started_at", - "total_earned_puvp", + "id", "vp_contract", "withdraw_destination" ], "properties": { - "denom": { - "description": "validated denom (native or cw20)", + "active_epoch": { + "description": "current distribution epoch state", "allOf": [ { - "$ref": "#/definitions/Denom" + "$ref": "#/definitions/Epoch" } ] }, - "emission_rate": { - "description": "reward emission rate", + "denom": { + "description": "validated denom (native or cw20)", "allOf": [ { - "$ref": "#/definitions/RewardEmissionRate" + "$ref": "#/definitions/Denom" } ] }, - "ends_at": { - "description": "the time when all funded rewards are allocated to users and thus the distribution period ends.", + "funded_amount": { + "description": "total amount of rewards funded that will be distributed in the active epoch.", "allOf": [ { - "$ref": "#/definitions/Expiration" + "$ref": "#/definitions/Uint128" } ] }, - "funded_amount": { - "description": "total amount of rewards funded", + "historical_earned_puvp": { + "description": "historical rewards earned per unit voting power from past epochs due to changes in the emission rate. each time emission rate is changed, this value is increased by the `active_epoch`'s rewards earned puvp.", "allOf": [ { - "$ref": "#/definitions/Uint128" + "$ref": "#/definitions/Uint256" } ] }, @@ -725,29 +908,11 @@ } ] }, - "last_update": { - "description": "time when total_earned_puvp was last updated for this denom", - "allOf": [ - { - "$ref": "#/definitions/Expiration" - } - ] - }, - "started_at": { - "description": "the time when the current reward distribution period started. period finishes iff it reaches its end.", - "allOf": [ - { - "$ref": "#/definitions/Expiration" - } - ] - }, - "total_earned_puvp": { - "description": "total rewards earned per unit voting power from started_at to last_update", - "allOf": [ - { - "$ref": "#/definitions/Uint256" - } - ] + "id": { + "description": "distribution ID", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "vp_contract": { "description": "address to query the voting power", @@ -758,7 +923,7 @@ ] }, "withdraw_destination": { - "description": "optional destination address for reward clawbacks", + "description": "destination address for reward clawbacks", "allOf": [ { "$ref": "#/definitions/Addr" @@ -834,46 +999,73 @@ } ] }, - "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)", + "EmissionRate": { + "description": "defines how many tokens (amount) should be distributed per amount of time (duration). e.g. 5udenom per hour.", "oneOf": [ { - "description": "AtHeight will expire when `env.block.height` >= height", + "description": "rewards are paused", "type": "object", "required": [ - "at_height" + "paused" ], "properties": { - "at_height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "paused": { + "type": "object", + "additionalProperties": false } }, "additionalProperties": false }, { - "description": "AtTime will expire when `env.block.time` >= time", + "description": "rewards are distributed immediately", "type": "object", "required": [ - "at_time" + "immediate" ], "properties": { - "at_time": { - "$ref": "#/definitions/Timestamp" + "immediate": { + "type": "object", + "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Never will never expire. Used to express the empty variant", + "description": "rewards are distributed at a constant rate", "type": "object", "required": [ - "never" + "linear" ], "properties": { - "never": { + "linear": { "type": "object", + "required": [ + "amount", + "continuous", + "duration" + ], + "properties": { + "amount": { + "description": "amount of tokens to distribute per amount of time", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "continuous": { + "description": "whether or not reward distribution is continuous: whether future funding after distribution finishes should be applied to the past, or rewards are paused once all funding has been distributed. all continuously backfilled rewards are distributed based on the current voting power.", + "type": "boolean" + }, + "duration": { + "description": "duration of time to distribute amount", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + } + }, "additionalProperties": false } }, @@ -881,151 +1073,58 @@ } ] }, - "RewardEmissionRate": { - "description": "defines how many tokens (amount) should be distributed per amount of time (duration). e.g. 5udenom per hour.", + "Epoch": { "type": "object", "required": [ - "amount", - "duration" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "duration": { - "$ref": "#/definitions/Duration" - } - }, - "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" - }, - "Uint256": { - "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 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 out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", - "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" - } - } - }, - "get_pending_rewards": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PendingRewardsResponse", - "type": "object", - "required": [ - "address", - "pending_rewards" - ], - "properties": { - "address": { - "type": "string" - }, - "pending_rewards": { - "type": "object", - "additionalProperties": { - "$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" - } - } - }, - "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" + "emission_rate", + "ends_at", + "last_updated_total_earned_puvp", + "started_at", + "total_earned_puvp" ], "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" + "emission_rate": { + "description": "reward emission rate", + "allOf": [ + { + "$ref": "#/definitions/EmissionRate" + } + ] }, - "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 - } - } - }, - "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" + "ends_at": { + "description": "the time when all funded rewards are allocated to users and thus the distribution period ends.", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] }, - { - "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" + "last_updated_total_earned_puvp": { + "description": "time when total_earned_puvp was last updated", + "allOf": [ + { + "$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" + "started_at": { + "description": "the time when the current reward distribution period started. period finishes iff it reaches its end.", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] }, - { - "type": "null" + "total_earned_puvp": { + "description": "total rewards earned per unit voting power from started_at to last_updated_total_earned_puvp", + "allOf": [ + { + "$ref": "#/definitions/Uint256" + } + ] } - ] - } - }, - "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" + }, + "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)", @@ -1082,24 +1181,32 @@ } ] }, + "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" + }, + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 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 out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "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" } } }, - "rewards_state": { + "distributions": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "RewardsStateResponse", + "title": "DistributionsResponse", "type": "object", "required": [ - "rewards" + "distributions" ], "properties": { - "rewards": { + "distributions": { "type": "array", "items": { - "$ref": "#/definitions/DenomRewardState" + "$ref": "#/definitions/DistributionState" } } }, @@ -1137,51 +1244,49 @@ } ] }, - "DenomRewardState": { - "description": "the state of a denom's reward distribution", + "DistributionState": { + "description": "the state of a reward distribution", "type": "object", "required": [ + "active_epoch", "denom", - "emission_rate", - "ends_at", "funded_amount", + "historical_earned_puvp", "hook_caller", - "last_update", - "started_at", - "total_earned_puvp", + "id", "vp_contract", "withdraw_destination" ], "properties": { - "denom": { - "description": "validated denom (native or cw20)", + "active_epoch": { + "description": "current distribution epoch state", "allOf": [ { - "$ref": "#/definitions/Denom" + "$ref": "#/definitions/Epoch" } ] }, - "emission_rate": { - "description": "reward emission rate", + "denom": { + "description": "validated denom (native or cw20)", "allOf": [ { - "$ref": "#/definitions/RewardEmissionRate" + "$ref": "#/definitions/Denom" } ] }, - "ends_at": { - "description": "the time when all funded rewards are allocated to users and thus the distribution period ends.", + "funded_amount": { + "description": "total amount of rewards funded that will be distributed in the active epoch.", "allOf": [ { - "$ref": "#/definitions/Expiration" + "$ref": "#/definitions/Uint128" } ] }, - "funded_amount": { - "description": "total amount of rewards funded", + "historical_earned_puvp": { + "description": "historical rewards earned per unit voting power from past epochs due to changes in the emission rate. each time emission rate is changed, this value is increased by the `active_epoch`'s rewards earned puvp.", "allOf": [ { - "$ref": "#/definitions/Uint128" + "$ref": "#/definitions/Uint256" } ] }, @@ -1193,29 +1298,11 @@ } ] }, - "last_update": { - "description": "time when total_earned_puvp was last updated for this denom", - "allOf": [ - { - "$ref": "#/definitions/Expiration" - } - ] - }, - "started_at": { - "description": "the time when the current reward distribution period started. period finishes iff it reaches its end.", - "allOf": [ - { - "$ref": "#/definitions/Expiration" - } - ] - }, - "total_earned_puvp": { - "description": "total rewards earned per unit voting power from started_at to last_update", - "allOf": [ - { - "$ref": "#/definitions/Uint256" - } - ] + "id": { + "description": "distribution ID", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, "vp_contract": { "description": "address to query the voting power", @@ -1226,7 +1313,7 @@ ] }, "withdraw_destination": { - "description": "optional destination address for reward clawbacks", + "description": "destination address for reward clawbacks", "allOf": [ { "$ref": "#/definitions/Addr" @@ -1270,46 +1357,73 @@ } ] }, - "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)", + "EmissionRate": { + "description": "defines how many tokens (amount) should be distributed per amount of time (duration). e.g. 5udenom per hour.", "oneOf": [ { - "description": "AtHeight will expire when `env.block.height` >= height", + "description": "rewards are paused", "type": "object", "required": [ - "at_height" + "paused" ], "properties": { - "at_height": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "paused": { + "type": "object", + "additionalProperties": false } }, "additionalProperties": false }, { - "description": "AtTime will expire when `env.block.time` >= time", + "description": "rewards are distributed immediately", "type": "object", "required": [ - "at_time" + "immediate" ], "properties": { - "at_time": { - "$ref": "#/definitions/Timestamp" + "immediate": { + "type": "object", + "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Never will never expire. Used to express the empty variant", + "description": "rewards are distributed at a constant rate", "type": "object", "required": [ - "never" + "linear" ], "properties": { - "never": { + "linear": { "type": "object", + "required": [ + "amount", + "continuous", + "duration" + ], + "properties": { + "amount": { + "description": "amount of tokens to distribute per amount of time", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "continuous": { + "description": "whether or not reward distribution is continuous: whether future funding after distribution finishes should be applied to the past, or rewards are paused once all funding has been distributed. all continuously backfilled rewards are distributed based on the current voting power.", + "type": "boolean" + }, + "duration": { + "description": "duration of time to distribute amount", + "allOf": [ + { + "$ref": "#/definitions/Duration" + } + ] + } + }, "additionalProperties": false } }, @@ -1317,36 +1431,119 @@ } ] }, - "RewardEmissionRate": { - "description": "defines how many tokens (amount) should be distributed per amount of time (duration). e.g. 5udenom per hour.", + "Epoch": { "type": "object", "required": [ - "amount", - "duration" + "emission_rate", + "ends_at", + "last_updated_total_earned_puvp", + "started_at", + "total_earned_puvp" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "emission_rate": { + "description": "reward emission rate", + "allOf": [ + { + "$ref": "#/definitions/EmissionRate" + } + ] }, - "duration": { - "$ref": "#/definitions/Duration" - } - }, - "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" - }, - "Uint256": { + "ends_at": { + "description": "the time when all funded rewards are allocated to users and thus the distribution period ends.", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "last_updated_total_earned_puvp": { + "description": "time when total_earned_puvp was last updated", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "started_at": { + "description": "the time when the current reward distribution period started. period finishes iff it reaches its end.", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "total_earned_puvp": { + "description": "total rewards earned per unit voting power from started_at to last_updated_total_earned_puvp", + "allOf": [ + { + "$ref": "#/definitions/Uint256" + } + ] + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint256": { "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 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 out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", "type": "string" }, @@ -1355,6 +1552,235 @@ "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 + } + } + }, + "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" + } + } + }, + "pending_rewards": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PendingRewardsResponse", + "type": "object", + "required": [ + "pending_rewards" + ], + "properties": { + "pending_rewards": { + "type": "array", + "items": { + "$ref": "#/definitions/DistributionPendingRewards" + } + } + }, + "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" + }, + "Denom": { + "oneOf": [ + { + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + ] + }, + "DistributionPendingRewards": { + "type": "object", + "required": [ + "denom", + "id", + "pending_rewards" + ], + "properties": { + "denom": { + "description": "denomination of the pending rewards", + "allOf": [ + { + "$ref": "#/definitions/Denom" + } + ] + }, + "id": { + "description": "distribution ID", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "pending_rewards": { + "description": "amount of pending rewards in the denom being distributed", + "allOf": [ + { + "$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" + } + } } } } diff --git a/contracts/distribution/dao-rewards-distributor/src/contract.rs b/contracts/distribution/dao-rewards-distributor/src/contract.rs index 83d4d868b..5b7715770 100644 --- a/contracts/distribution/dao-rewards-distributor/src/contract.rs +++ b/contracts/distribution/dao-rewards-distributor/src/contract.rs @@ -1,45 +1,58 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - coins, ensure, from_json, to_json_binary, Addr, BankMsg, Binary, CosmosMsg, Decimal, Deps, - DepsMut, Env, MessageInfo, Order, Response, StdError, StdResult, Uint128, Uint256, WasmMsg, + ensure, from_json, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, + StdError, StdResult, Uint128, Uint256, }; use cw2::{get_contract_version, set_contract_version}; -use cw20::{Cw20ReceiveMsg, Denom, UncheckedDenom}; -use cw_utils::{one_coin, Duration, Expiration}; -use dao_interface::voting::{ - InfoResponse, Query as VotingQueryMsg, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, -}; -use std::collections::HashMap; -use std::convert::TryInto; +use cw20::{Cw20ReceiveMsg, Denom}; +use cw_storage_plus::Bound; +use cw_utils::{must_pay, nonpayable, Duration, Expiration}; +use dao_interface::voting::InfoResponse; +use semver::Version; + +use std::ops::Add; +use crate::helpers::{get_transfer_msg, validate_voting_power_contract}; use crate::hooks::{ execute_membership_changed, execute_nft_stake_changed, execute_stake_changed, - subscribe_denom_to_hook, + subscribe_distribution_to_hook, unsubscribe_distribution_from_hook, }; use crate::msg::{ - ExecuteMsg, InstantiateMsg, PendingRewardsResponse, QueryMsg, ReceiveMsg, RewardEmissionRate, - RewardsStateResponse, + CreateMsg, DistributionPendingRewards, DistributionsResponse, ExecuteMsg, FundMsg, + InstantiateMsg, MigrateMsg, PendingRewardsResponse, QueryMsg, ReceiveCw20Msg, +}; +use crate::rewards::{ + get_accrued_rewards_not_yet_accounted_for, get_active_total_earned_puvp, update_rewards, }; -use crate::state::{DenomRewardState, DENOM_REWARD_STATES, USER_REWARD_STATES}; +use crate::state::{DistributionState, EmissionRate, Epoch, COUNT, DISTRIBUTIONS, USER_REWARDS}; use crate::ContractError; -const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); -const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub(crate) const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub const DEFAULT_LIMIT: u32 = 10; +pub const MAX_LIMIT: u32 = 50; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, _env: Env, - _info: MessageInfo, + info: MessageInfo, msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - // Intialize the contract owner - cw_ownable::initialize_owner(deps.storage, deps.api, msg.owner.as_deref())?; + // Intialize the contract owner, defaulting to instantiator. + let owner = deps + .api + .addr_validate(&msg.owner.unwrap_or_else(|| info.sender.to_string()))?; + cw_ownable::initialize_owner(deps.storage, deps.api, Some(owner.as_str()))?; + + // initialize count + COUNT.save(deps.storage, &0)?; - Ok(Response::new().add_attribute("owner", msg.owner.unwrap_or_else(|| "None".to_string()))) + Ok(Response::new().add_attribute("owner", owner)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -53,479 +66,508 @@ pub fn execute( ExecuteMsg::StakeChangeHook(msg) => execute_stake_changed(deps, env, info, msg), ExecuteMsg::NftStakeChangeHook(msg) => execute_nft_stake_changed(deps, env, info, msg), ExecuteMsg::MemberChangedHook(msg) => execute_membership_changed(deps, env, info, msg), - ExecuteMsg::Claim { denom } => execute_claim(deps, env, info, denom), - ExecuteMsg::Fund {} => execute_fund_native(deps, env, info), - ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg), ExecuteMsg::UpdateOwnership(action) => execute_update_owner(deps, info, env, action), - ExecuteMsg::Shutdown { denom } => execute_shutdown(deps, info, env, denom), - ExecuteMsg::RegisterRewardDenom { - denom, + ExecuteMsg::Receive(msg) => execute_receive_cw20(deps, env, info, msg), + ExecuteMsg::Create(create_msg) => execute_create(deps, env, info, create_msg), + ExecuteMsg::Update { + id, emission_rate, vp_contract, hook_caller, withdraw_destination, - } => execute_register_reward_denom( + } => execute_update( deps, + env, info, - denom, + id, emission_rate, vp_contract, hook_caller, withdraw_destination, ), + ExecuteMsg::Fund(FundMsg { id }) => execute_fund_native(deps, env, info, id), + ExecuteMsg::Claim { id } => execute_claim(deps, env, info, id), + ExecuteMsg::Withdraw { id } => execute_withdraw(deps, info, env, id), } } -/// registers a new denom for rewards distribution. -/// only the owner can register a new denom. -/// a denom can only be registered once; update if you need to change something. -fn execute_register_reward_denom( +fn execute_receive_cw20( deps: DepsMut, + env: Env, info: MessageInfo, - denom: UncheckedDenom, - emission_rate: RewardEmissionRate, - vp_contract: String, - hook_caller: String, - withdraw_destination: Option, + wrapper: Cw20ReceiveMsg, +) -> Result { + nonpayable(&info)?; + + // verify msg + let msg: ReceiveCw20Msg = from_json(&wrapper.msg)?; + + match msg { + ReceiveCw20Msg::Fund(FundMsg { id }) => { + let distribution = DISTRIBUTIONS + .load(deps.storage, id) + .map_err(|_| ContractError::DistributionNotFound { id })?; + + match &distribution.denom { + Denom::Native(_) => return Err(ContractError::InvalidFunds {}), + Denom::Cw20(addr) => { + // ensure funding is coming from the cw20 we are currently + // distributing + if addr != info.sender { + return Err(ContractError::InvalidCw20 {}); + } + } + }; + + execute_fund(deps, env, distribution, wrapper.amount) + } + } +} + +/// creates a new rewards distribution. only the owner can do this. if funds +/// provided when creating a native token distribution, will start distributing +/// rewards immediately. +fn execute_create( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: CreateMsg, ) -> Result { - // only the owner can register a new denom + // only the owner can create a new distribution cw_ownable::assert_owner(deps.storage, &info.sender)?; - emission_rate.validate_emission_time_window()?; + // update count and use as the new distribution's ID + let id = COUNT.update(deps.storage, |count| -> StdResult { Ok(count + 1) })?; - let checked_denom = denom.into_checked(deps.as_ref())?; - let hook_caller = deps.api.addr_validate(&hook_caller)?; - let vp_contract = validate_voting_power_contract(&deps, vp_contract)?; + let checked_denom = msg.denom.into_checked(deps.as_ref())?; + let hook_caller = deps.api.addr_validate(&msg.hook_caller)?; + let vp_contract = validate_voting_power_contract(&deps, msg.vp_contract)?; - let withdraw_destination = match withdraw_destination { + let withdraw_destination = match msg.withdraw_destination { // if withdraw destination is specified, we validate it Some(addr) => deps.api.addr_validate(&addr)?, // otherwise default to the owner - None => info.sender, + None => info.sender.clone(), }; - // Initialize the reward state - let reward_state = DenomRewardState { + msg.emission_rate.validate()?; + + // Initialize the distribution state + let distribution = DistributionState { + id, denom: checked_denom, - started_at: Expiration::Never {}, - ends_at: Expiration::Never {}, - emission_rate, - total_earned_puvp: Uint256::zero(), - last_update: Expiration::Never {}, + active_epoch: Epoch { + started_at: Expiration::Never {}, + ends_at: Expiration::Never {}, + emission_rate: msg.emission_rate, + total_earned_puvp: Uint256::zero(), + last_updated_total_earned_puvp: Expiration::Never {}, + }, vp_contract, hook_caller: hook_caller.clone(), funded_amount: Uint128::zero(), withdraw_destination, + historical_earned_puvp: Uint256::zero(), }; - let str_denom = reward_state.to_str_denom(); - - // store the new reward denom state or error if it already exists - DENOM_REWARD_STATES.update( - deps.storage, - str_denom.to_string(), - |existing| match existing { - Some(_) => Err(ContractError::DenomAlreadyRegistered {}), - None => Ok(reward_state), - }, - )?; - // update the registered hooks to include the new denom - subscribe_denom_to_hook(deps, str_denom, hook_caller.clone())?; + // store the new distribution state, erroring if it already exists. this + // should never happen, but just in case. + DISTRIBUTIONS.update(deps.storage, id, |existing| match existing { + Some(_) => Err(ContractError::UnexpectedDuplicateDistributionId { id }), + None => Ok(distribution.clone()), + })?; - Ok(Response::default()) + // update the registered hooks to include the new distribution + subscribe_distribution_to_hook(deps.storage, id, hook_caller.clone())?; + + let mut response = Response::new() + .add_attribute("action", "create") + .add_attribute("id", id.to_string()) + .add_attribute("denom", distribution.get_denom_string()); + + // if native funds provided, ensure they are for this denom. if other native + // funds present, return error. if no funds, do nothing and leave registered + // denom with no funding, to be funded later. + if !info.funds.is_empty() { + match &distribution.denom { + Denom::Native(denom) => { + // ensures there is exactly 1 coin passed that matches the denom + let amount = must_pay(&info, denom)?; + + execute_fund(deps, env, distribution, amount)?; + + response = response.add_attribute("amount_funded", amount); + } + Denom::Cw20(_) => return Err(ContractError::NoFundsOnCw20Create {}), + } + } + + Ok(response) } -/// shutdown the rewards distributor contract. -/// can only be called by the admin and only during the distribution period. -/// this will clawback all (undistributed) future rewards to the admin. -/// updates the period finish expiration to the current block. -fn execute_shutdown( +/// updates the config for a distribution +#[allow(clippy::too_many_arguments)] +fn execute_update( deps: DepsMut, - info: MessageInfo, env: Env, - denom: String, + info: MessageInfo, + id: u64, + emission_rate: Option, + vp_contract: Option, + hook_caller: Option, + withdraw_destination: Option, ) -> Result { - // only the owner can initiate a shutdown + nonpayable(&info)?; + + // only the owner can update a distribution cw_ownable::assert_owner(deps.storage, &info.sender)?; - let mut reward_state = DENOM_REWARD_STATES.load(deps.storage, denom.to_string())?; + let mut distribution = DISTRIBUTIONS + .load(deps.storage, id) + .map_err(|_| ContractError::DistributionNotFound { id })?; - // shutdown is only possible during the distribution period - ensure!( - !reward_state.ends_at.is_expired(&env.block), - ContractError::ShutdownError("Reward period already finished".to_string()) - ); + if let Some(emission_rate) = emission_rate { + emission_rate.validate()?; - // we get the start and end scalar values in u64 (seconds/blocks) - let started_at = reward_state.get_started_at_scalar()?; - let ends_at = reward_state.get_ends_at_scalar()?; - let reward_duration = ends_at - started_at; + // transition the epoch to the new emission rate + distribution.transition_epoch(deps.as_ref(), emission_rate, &env.block)?; + } - // find the % of reward_duration that remains from current block - let passed_units_since_start = match reward_state.emission_rate.duration { - Duration::Height(_) => Uint128::from(env.block.height - started_at), - Duration::Time(_) => Uint128::from(env.block.time.seconds() - started_at), - }; + if let Some(vp_contract) = vp_contract { + distribution.vp_contract = validate_voting_power_contract(&deps, vp_contract)?; + } - // get the fraction of what part of rewards duration is in the past - // and sub from 1 to get the remaining rewards - let remaining_reward_duration_fraction = Decimal::one() - .checked_sub(Decimal::from_ratio( - passed_units_since_start, - reward_duration, - )) - .map_err(|e| ContractError::Std(StdError::overflow(e)))?; + if let Some(hook_caller) = hook_caller { + // remove existing from registered hooks + unsubscribe_distribution_from_hook(deps.storage, id, distribution.hook_caller)?; - // to get the clawback msg - let clawback_msg = get_transfer_msg( - reward_state.withdraw_destination.clone(), - reward_state.funded_amount * remaining_reward_duration_fraction, - reward_state.denom.clone(), - )?; + distribution.hook_caller = deps.api.addr_validate(&hook_caller)?; - // shutdown completes the rewards - reward_state.ends_at = match reward_state.emission_rate.duration { - Duration::Height(_) => Expiration::AtHeight(env.block.height), - Duration::Time(_) => Expiration::AtTime(env.block.time), - }; + // add new to registered hooks + subscribe_distribution_to_hook(deps.storage, id, distribution.hook_caller.clone())?; + } + + if let Some(withdraw_destination) = withdraw_destination { + distribution.withdraw_destination = deps.api.addr_validate(&withdraw_destination)?; + } - DENOM_REWARD_STATES.save(deps.storage, denom.to_string(), &reward_state)?; + DISTRIBUTIONS.save(deps.storage, id, &distribution)?; Ok(Response::new() - .add_attribute("action", "shutdown") - .add_message(clawback_msg)) + .add_attribute("action", "update") + .add_attribute("id", id.to_string()) + .add_attribute("denom", distribution.get_denom_string())) } -fn execute_receive( +fn execute_fund_native( deps: DepsMut, env: Env, info: MessageInfo, - wrapper: Cw20ReceiveMsg, + id: u64, ) -> Result { - // verify msg - let _msg: ReceiveMsg = from_json(&wrapper.msg)?; + let distribution = DISTRIBUTIONS + .load(deps.storage, id) + .map_err(|_| ContractError::DistributionNotFound { id })?; + + let amount = match &distribution.denom { + Denom::Native(denom) => { + must_pay(&info, denom).map_err(|_| ContractError::InvalidFunds {})? + } + Denom::Cw20(_) => return Err(ContractError::InvalidFunds {}), + }; - let reward_denom_state = DENOM_REWARD_STATES.load(deps.storage, info.sender.to_string())?; - execute_fund(deps, env, reward_denom_state, wrapper.amount) + execute_fund(deps, env, distribution, amount) } -fn execute_fund_native( +fn execute_fund( deps: DepsMut, env: Env, - info: MessageInfo, + distribution: DistributionState, + amount: Uint128, +) -> Result { + match distribution.active_epoch.emission_rate { + EmissionRate::Paused {} => execute_fund_paused(deps, distribution, amount), + EmissionRate::Immediate {} => execute_fund_immediate(deps, env, distribution, amount), + EmissionRate::Linear { .. } => execute_fund_linear(deps, env, distribution, amount), + } +} + +/// funding a paused distribution simply increases the funded amount. +fn execute_fund_paused( + deps: DepsMut, + mut distribution: DistributionState, + amount: Uint128, ) -> Result { - let fund_coin = one_coin(&info).map_err(|_| ContractError::InvalidFunds {})?; + distribution.funded_amount += amount; - let reward_denom_state = DENOM_REWARD_STATES.load(deps.storage, fund_coin.denom.clone())?; + DISTRIBUTIONS.save(deps.storage, distribution.id, &distribution)?; - execute_fund(deps, env, reward_denom_state, fund_coin.amount) + Ok(Response::new() + .add_attribute("action", "fund") + .add_attribute("id", distribution.id.to_string()) + .add_attribute("denom", distribution.get_denom_string()) + .add_attribute("amount_funded", amount)) } -fn execute_fund( +/// funding an immediate distribution instantly distributes the new amount. +fn execute_fund_immediate( deps: DepsMut, env: Env, - mut denom_reward_state: DenomRewardState, + mut distribution: DistributionState, amount: Uint128, ) -> Result { - // we derive the period for which the rewards are funded - // by looking at the existing reward emission rate and the funded amount - let funded_period_duration = denom_reward_state - .emission_rate - .get_funded_period_duration(amount)?; - let funded_period_value = get_duration_scalar(&funded_period_duration); - - denom_reward_state = denom_reward_state - .bump_funding_date(&env.block) - .bump_last_update(&env.block); - - // the duration of rewards period is extended in different ways, - // depending on the current expiration state and current block - denom_reward_state.ends_at = match denom_reward_state.ends_at { - // if this is the first funding of the denom, the new expiration is the - // funded period duration from the current block - Expiration::Never {} => funded_period_duration.after(&env.block), - // otherwise we add the duration units to the existing expiration - Expiration::AtHeight(h) => { - if h <= env.block.height { - // expiration is the funded duration after current block - Expiration::AtHeight(env.block.height + funded_period_value) - } else { - // if the previous expiration had not yet expired, we extend - // the current rewards period by the newly funded duration - Expiration::AtHeight(h + funded_period_value) - } - } - Expiration::AtTime(t) => { - if t <= env.block.time { - // expiration is the funded duration after current block time - Expiration::AtTime(env.block.time.plus_seconds(funded_period_value)) - } else { - // if the previous expiration had not yet expired, we extend - // the current rewards period by the newly funded duration - Expiration::AtTime(t.plus_seconds(funded_period_value)) - } + distribution.funded_amount += amount; + + // for immediate distribution, update total_earned_puvp instantly since we + // need to know the change in funded_amount to calculate the new + // total_earned_puvp. + distribution.update_immediate_emission_total_earned_puvp(deps.as_ref(), &env.block, amount)?; + + DISTRIBUTIONS.save(deps.storage, distribution.id, &distribution)?; + + Ok(Response::new() + .add_attribute("action", "fund") + .add_attribute("id", distribution.id.to_string()) + .add_attribute("denom", distribution.get_denom_string()) + .add_attribute("amount_funded", amount)) +} + +/// funding a linear distribution requires some complex logic based on whether +/// or not the distribution is continuous and whether or not it's expired. +/// +/// expired continuous distributions experience backfill with the new funds, +/// whereas expired discontinuous distributions begin anew (and all past rewards +/// must be taken into account before restarting distribution). +fn execute_fund_linear( + deps: DepsMut, + env: Env, + mut distribution: DistributionState, + amount: Uint128, +) -> Result { + let continuous = + if let EmissionRate::Linear { continuous, .. } = distribution.active_epoch.emission_rate { + continuous + } else { + false + }; + let previously_funded = !distribution.funded_amount.is_zero(); + let was_expired = distribution.active_epoch.ends_at.is_expired(&env.block); + let discontinuous_expired = !continuous && was_expired; + + // restart the distribution from the current block if it hasn't yet started + // (i.e. never been funded) OR if it's both discontinuous and expired (i.e. + // all past funds should have been distributed and we're effectively + // beginning a new distribution with new funds). this ensures the new funds + // start being distributed from now instead of from the past. + // + // if already funded and continuous or not expired (else block), just add + // the new funds and leave start date the same, backfilling rewards. + if !previously_funded || discontinuous_expired { + // if funding an expired discontinuous distribution that's previously + // been funded, ensure all rewards are taken into account before + // restarting, in case users haven't claimed yet, by adding the final + // total rewards earned to the historical value. + if discontinuous_expired && previously_funded { + let final_total_earned_puvp = + get_active_total_earned_puvp(deps.as_ref(), &env.block, &distribution)?; + distribution.historical_earned_puvp = distribution + .historical_earned_puvp + .checked_add(final_total_earned_puvp) + .map_err(|err| ContractError::DistributionHistoryTooLarge { + err: err.to_string(), + })?; + // last updated block is reset to the new start block below } + + // reset all starting fields since a new distribution is starting + distribution.funded_amount = amount; + distribution.active_epoch.started_at = match distribution.active_epoch.emission_rate { + EmissionRate::Paused {} => Expiration::Never {}, + EmissionRate::Immediate {} => Expiration::Never {}, + EmissionRate::Linear { duration, .. } => match duration { + Duration::Height(_) => Expiration::AtHeight(env.block.height), + Duration::Time(_) => Expiration::AtTime(env.block.time), + }, + }; + distribution.active_epoch.total_earned_puvp = Uint256::zero(); + distribution.active_epoch.last_updated_total_earned_puvp = + distribution.active_epoch.started_at; + } else { + distribution.funded_amount += amount; + } + + // update the end block based on the new funds and potentially updated start + let new_funded_duration = distribution + .active_epoch + .emission_rate + .get_funded_period_duration(distribution.funded_amount)?; + distribution.active_epoch.ends_at = match new_funded_duration { + Some(duration) => distribution.active_epoch.started_at.add(duration)?, + None => Expiration::Never {}, }; - denom_reward_state.funded_amount += amount; - DENOM_REWARD_STATES.save( - deps.storage, - denom_reward_state.to_str_denom(), - &denom_reward_state, - )?; + // if continuous, funds existed in the past, and the distribution was + // expired, some rewards may not have been distributed due to lack of + // sufficient funding. ensure the total rewards earned puvp is up to date + // based on the original start block and the newly updated end block. + if continuous && previously_funded && was_expired { + distribution.active_epoch.total_earned_puvp = + get_active_total_earned_puvp(deps.as_ref(), &env.block, &distribution)?; + distribution.active_epoch.bump_last_updated(&env.block); + } - Ok(Response::default()) + DISTRIBUTIONS.save(deps.storage, distribution.id, &distribution)?; + + Ok(Response::new() + .add_attribute("action", "fund") + .add_attribute("id", distribution.id.to_string()) + .add_attribute("denom", distribution.get_denom_string()) + .add_attribute("amount_funded", amount)) } fn execute_claim( mut deps: DepsMut, env: Env, info: MessageInfo, - denom: String, + id: u64, ) -> Result { - // update the rewards information for the sender. - update_rewards(&mut deps, &env, &info.sender, denom.to_string())?; - - // get the denom state for the string-based denom - let denom_reward_state = DENOM_REWARD_STATES.load(deps.storage, denom.to_string())?; - - let mut amount = Uint128::zero(); - - USER_REWARD_STATES.update( - deps.storage, - info.sender.clone(), - |state| -> Result<_, ContractError> { - let mut user_reward_state = state.unwrap_or_default(); - // updating the map returns the previous value if it existed. - // we set the value to zero and store it in the amount defined before the update. - amount = user_reward_state - .pending_denom_rewards - .insert(denom, Uint128::zero()) - .unwrap_or_default(); - Ok(user_reward_state) - }, - )?; + nonpayable(&info)?; + + // update the distribution for the sender. this updates the distribution + // state and the user reward state. + update_rewards(&mut deps, &env, &info.sender, id)?; + + // load the updated states. previous `update_rewards` call ensures that + // these states exist. + let distribution = DISTRIBUTIONS.load(deps.storage, id)?; + let mut user_reward_state = USER_REWARDS.load(deps.storage, info.sender.clone())?; + + // updating the map returns the previous value if it existed. we set the + // value to zero and get the amount of pending rewards until this point. + let claim_amount = user_reward_state + .pending_rewards + .insert(id, Uint128::zero()) + .unwrap_or_default(); - if amount.is_zero() { + // if there are no rewards to claim, error out + if claim_amount.is_zero() { return Err(ContractError::NoRewardsClaimable {}); } + // otherwise reflect the updated user reward state and transfer out the + // claimed rewards + USER_REWARDS.save(deps.storage, info.sender.clone(), &user_reward_state)?; + + let denom_str = distribution.get_denom_string(); + Ok(Response::new() .add_message(get_transfer_msg( info.sender.clone(), - amount, - denom_reward_state.denom, + claim_amount, + distribution.denom, )?) - .add_attribute("action", "claim")) + .add_attribute("action", "claim") + .add_attribute("id", id.to_string()) + .add_attribute("denom", denom_str) + .add_attribute("amount_claimed", claim_amount)) } -fn execute_update_owner( +/// withdraws the undistributed rewards for a distribution. members can claim +/// whatever they earned until this point. this is effectively an inverse to +/// fund and does not affect any already-distributed rewards. can only be called +/// by the admin and only during the distribution period. updates the period +/// finish expiration to the current block. +fn execute_withdraw( deps: DepsMut, info: MessageInfo, env: Env, - action: cw_ownable::Action, + id: u64, ) -> Result { - // Update the current contract owner. - // Note, this is a two step process, the new owner must accept this ownership transfer. - // First the owner specifies the new owner, then the new owner must accept. - let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; - Ok(Response::default().add_attributes(ownership.into_attributes())) -} - -pub fn update_rewards(deps: &mut DepsMut, env: &Env, addr: &Addr, denom: String) -> StdResult<()> { - let reward_state = DENOM_REWARD_STATES.load(deps.storage, denom.clone())?; - - // first, we calculate the latest total rewards per unit voting power - // and update them - let total_earned_puvp = get_total_earned_puvp(env, deps.as_ref(), &reward_state)?; - - // update the denom state's total rewards earned and last updated - DENOM_REWARD_STATES.update(deps.storage, denom.clone(), |state| -> StdResult<_> { - match state { - Some(mut rc) => { - rc.total_earned_puvp = total_earned_puvp; - Ok(rc.bump_last_update(&env.block)) - } - None => Err(StdError::generic_err("Denom reward state not found")), - } - })?; - - // then we calculate the rewards earned since last user action - let earned_rewards = get_accrued_rewards_since_last_user_action( - deps.as_ref(), - env, - addr, - total_earned_puvp, - &reward_state.vp_contract, - denom.clone(), - )?; - - // reflect the earned rewards in the user's reward state - USER_REWARD_STATES.update(deps.storage, addr.clone(), |state| -> StdResult<_> { - // if user does not yet have state, create a new one - let mut user_reward_state = state.unwrap_or_default(); - - // get the pre-existing pending reward amount for the denom - let previous_pending_denom_reward_amount = *user_reward_state - .pending_denom_rewards - .get(&denom) - .unwrap_or(&Uint128::zero()); + nonpayable(&info)?; - // get the amount of newly earned rewards for the denom - let earned_rewards_amount = earned_rewards.get(&denom).cloned().unwrap_or_default(); - - user_reward_state.pending_denom_rewards.insert( - denom.clone(), - previous_pending_denom_reward_amount + earned_rewards_amount, - ); - - // update the user's earned rewards that have been accounted for - user_reward_state - .denom_rewards_puvp - .insert(denom.clone(), total_earned_puvp); + // only the owner can initiate a withdraw + cw_ownable::assert_owner(deps.storage, &info.sender)?; - Ok(user_reward_state) - })?; - Ok(()) -} + let mut distribution = DISTRIBUTIONS + .load(deps.storage, id) + .map_err(|_| ContractError::DistributionNotFound { id })?; -/// Calculate the total rewards earned per unit voting power since the last -/// update. -fn get_total_earned_puvp( - env: &Env, - deps: Deps, - reward_state: &DenomRewardState, -) -> StdResult { - let curr = reward_state.total_earned_puvp; - - // query the total voting power just before this block from the voting power - // contract - let prev_total_power = get_prev_block_total_vp(deps, env, &reward_state.vp_contract)?; - - let last_time_rewards_distributed = - reward_state.get_latest_reward_distribution_time(&env.block); - - // get the duration from the last time rewards were updated to the last time - // rewards were distributed. this will be 0 if the rewards were updated at - // or after the last time rewards were distributed. - let new_reward_distribution_duration = Uint128::from(get_start_end_diff( - last_time_rewards_distributed, - reward_state.last_update, - )?); - - if prev_total_power.is_zero() { - Ok(curr) - } else { - let duration_value = get_duration_scalar(&reward_state.emission_rate.duration); - - // count intervals of the rewards emission that have passed since the - // last update which need to be distributed - let complete_distribution_periods = - new_reward_distribution_duration.checked_div(Uint128::from(duration_value))?; - - // It is impossible for this to overflow as total rewards can never - // exceed max value of Uint128 as total tokens in existence cannot - // exceed Uint128 (because the bank module Coin type uses Uint128). - let new_rewards_distributed = reward_state - .emission_rate - .amount - .full_mul(complete_distribution_periods) - .checked_mul(scale_factor())?; - - // the new rewards per unit voting power that have been distributed - // since the last update - let new_rewards_puvp = new_rewards_distributed.checked_div(prev_total_power.into())?; - Ok(curr + new_rewards_puvp) - } -} + // withdraw is only possible during the distribution period + ensure!( + !distribution.active_epoch.ends_at.is_expired(&env.block), + ContractError::RewardsAlreadyDistributed {} + ); -// get a user's rewards not yet accounted for in their reward state -fn get_accrued_rewards_since_last_user_action( - deps: Deps, - env: &Env, - addr: &Addr, - total_earned_puvp: Uint256, - vp_contract: &Addr, - denom: String, -) -> StdResult> { - // get the user's voting power at the current height - let voting_power = Uint256::from(get_voting_power(deps, env, vp_contract, addr)?); - - let mut accrued_rewards: HashMap = HashMap::new(); - - let user_reward_state = USER_REWARD_STATES - .load(deps.storage, addr.clone()) - .unwrap_or_default(); + // withdraw ends the epoch early + distribution.active_epoch.ends_at = match distribution.active_epoch.started_at { + Expiration::Never {} => Expiration::Never {}, + Expiration::AtHeight(_) => Expiration::AtHeight(env.block.height), + Expiration::AtTime(_) => Expiration::AtTime(env.block.time), + }; - // get previous reward per unit voting power accounted for - let user_last_reward_puvp = user_reward_state - .denom_rewards_puvp - .get(&denom) - .cloned() - .unwrap_or_default(); + // get total rewards distributed based on newly updated ends_at + let rewards_distributed = distribution.get_total_rewards()?; - // calculate the difference between the current total reward per unit - // voting power distributed and the user's latest reward per unit voting - // power accounted for - let reward_factor = total_earned_puvp.checked_sub(user_last_reward_puvp)?; + let clawback_amount = distribution.funded_amount - rewards_distributed; - // calculate the amount of rewards earned: - // voting_power * reward_factor / scale_factor - let accrued_rewards_amount: Uint128 = voting_power - .checked_mul(reward_factor)? - .checked_div(scale_factor())? - .try_into()?; + // remove withdrawn funds from amount funded since they are no longer funded + distribution.funded_amount = rewards_distributed; - accrued_rewards.insert(denom.to_string(), accrued_rewards_amount); + let clawback_msg = get_transfer_msg( + distribution.withdraw_destination.clone(), + clawback_amount, + distribution.denom.clone(), + )?; - Ok(accrued_rewards) -} + DISTRIBUTIONS.save(deps.storage, id, &distribution)?; -fn get_prev_block_total_vp(deps: Deps, env: &Env, contract_addr: &Addr) -> StdResult { - let msg = VotingQueryMsg::TotalPowerAtHeight { - height: Some(env.block.height.checked_sub(1).unwrap_or_default()), - }; - let resp: TotalPowerAtHeightResponse = deps.querier.query_wasm_smart(contract_addr, &msg)?; - Ok(resp.power) + Ok(Response::new() + .add_attribute("action", "withdraw") + .add_attribute("id", id.to_string()) + .add_attribute("denom", distribution.get_denom_string()) + .add_attribute("amount_withdrawn", clawback_amount) + .add_attribute("amount_distributed", rewards_distributed) + .add_message(clawback_msg)) } -fn get_voting_power( - deps: Deps, - env: &Env, - contract_addr: &Addr, - addr: &Addr, -) -> StdResult { - let msg = VotingQueryMsg::VotingPowerAtHeight { - address: addr.into(), - height: Some(env.block.height), - }; - let resp: VotingPowerAtHeightResponse = deps.querier.query_wasm_smart(contract_addr, &msg)?; - Ok(resp.power) -} +fn execute_update_owner( + deps: DepsMut, + info: MessageInfo, + env: Env, + action: cw_ownable::Action, +) -> Result { + nonpayable(&info)?; -/// returns underlying scalar value for a given duration. -/// if the duration is in blocks, returns the block height. -/// if the duration is in time, returns the time in seconds. -fn get_duration_scalar(duration: &Duration) -> u64 { - match duration { - Duration::Height(h) => *h, - Duration::Time(t) => *t, - } + // Update the current contract owner. Note, this is a two step process, the + // new owner must accept this ownership transfer. First the owner specifies + // the new owner, then the new owner must accept. + let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; + Ok(Response::new().add_attributes(ownership.into_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::Info {} => Ok(to_json_binary(&query_info(deps)?)?), - QueryMsg::RewardsState {} => Ok(to_json_binary(&query_rewards_state(deps, env)?)?), - QueryMsg::GetPendingRewards { address } => { - Ok(to_json_binary(&query_pending_rewards(deps, env, address)?)?) - } QueryMsg::Ownership {} => to_json_binary(&cw_ownable::get_ownership(deps.storage)?), - QueryMsg::DenomRewardState { denom } => { - let state = DENOM_REWARD_STATES.load(deps.storage, denom)?; + QueryMsg::PendingRewards { + address, + start_after, + limit, + } => Ok(to_json_binary(&query_pending_rewards( + deps, + env, + address, + start_after, + limit, + )?)?), + QueryMsg::Distribution { id } => { + let state = DISTRIBUTIONS.load(deps.storage, id)?; Ok(to_json_binary(&state)?) } + QueryMsg::Distributions { start_after, limit } => Ok(to_json_binary( + &query_distributions(deps, start_after, limit)?, + )?), } } @@ -534,115 +576,109 @@ fn query_info(deps: Deps) -> StdResult { Ok(InfoResponse { info }) } -fn query_rewards_state(deps: Deps, _env: Env) -> StdResult { - let rewards = DENOM_REWARD_STATES - .range(deps.storage, None, None, Order::Ascending) - .map(|item| item.map(|(_, v)| v)) - .collect::>>()?; - Ok(RewardsStateResponse { rewards }) -} - -fn query_pending_rewards(deps: Deps, env: Env, addr: String) -> StdResult { +/// returns the pending rewards for a given address that are ready to be +/// claimed. +fn query_pending_rewards( + deps: Deps, + env: Env, + addr: String, + start_after: Option, + limit: Option, +) -> StdResult { let addr = deps.api.addr_validate(&addr)?; - let reward_states = DENOM_REWARD_STATES - .range(deps.storage, None, None, Order::Ascending) + + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(Bound::::exclusive); + + // user may not have interacted with the contract before this query so we + // potentially return the default user reward state + let user_reward_state = USER_REWARDS + .load(deps.storage, addr.clone()) + .unwrap_or_default(); + + let distributions = DISTRIBUTIONS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) .collect::>>()?; - let mut pending_rewards: HashMap = HashMap::new(); + let mut pending_rewards: Vec = vec![]; + + // iterate over all distributions and calculate pending rewards for the user + for (id, distribution) in distributions { + // first we get the active epoch earned puvp value + let active_total_earned_puvp = + get_active_total_earned_puvp(deps, &env.block, &distribution) + .map_err(|e| StdError::generic_err(e.to_string()))?; - for (denom, reward_state) in reward_states { - let total_earned_puvp = get_total_earned_puvp(&env, deps, &reward_state)?; + // then we add that to the historical rewards earned puvp + let total_earned_puvp = + active_total_earned_puvp.checked_add(distribution.historical_earned_puvp)?; - let earned_rewards = get_accrued_rewards_since_last_user_action( + let existing_amount = user_reward_state + .pending_rewards + .get(&id) + .cloned() + .unwrap_or_default(); + + let unaccounted_for_rewards = get_accrued_rewards_not_yet_accounted_for( deps, &env, &addr, total_earned_puvp, - &reward_state.vp_contract, - denom.to_string(), + &distribution, + &user_reward_state, )?; - let user_reward_state = USER_REWARD_STATES - .load(deps.storage, addr.clone()) - .unwrap_or_default(); - - let default_amt = Uint128::zero(); - let earned_amount = earned_rewards.get(&denom).unwrap_or(&default_amt); - let existing_amount = user_reward_state - .pending_denom_rewards - .get(&denom) - .unwrap_or(&default_amt); - pending_rewards.insert(denom, *earned_amount + *existing_amount); + pending_rewards.push(DistributionPendingRewards { + id, + denom: distribution.denom, + pending_rewards: unaccounted_for_rewards + existing_amount, + }); } - let pending_rewards_response = PendingRewardsResponse { - address: addr.to_string(), - pending_rewards, - }; - Ok(pending_rewards_response) + Ok(PendingRewardsResponse { pending_rewards }) } -/// Returns the appropriate CosmosMsg for transferring the reward token. -fn get_transfer_msg(recipient: Addr, amount: Uint128, denom: Denom) -> StdResult { - match denom { - Denom::Native(denom) => Ok(BankMsg::Send { - to_address: recipient.into_string(), - amount: coins(amount.u128(), denom), - } - .into()), - Denom::Cw20(addr) => { - let cw20_msg = to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { - recipient: recipient.into_string(), - amount, - })?; - Ok(WasmMsg::Execute { - contract_addr: addr.into_string(), - msg: cw20_msg, - funds: vec![], - } - .into()) - } - } -} +fn query_distributions( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start = start_after.map(Bound::::exclusive); + + let distributions = DISTRIBUTIONS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| item.map(|(_, v)| v)) + .collect::>>()?; -pub(crate) fn scale_factor() -> Uint256 { - Uint256::from(10u8).pow(39) + Ok(DistributionsResponse { distributions }) } -/// Calculate the duration from start to end. If the end is at or before the -/// start, return 0. -fn get_start_end_diff(end: Expiration, start: Expiration) -> StdResult { - match (end, start) { - (Expiration::AtHeight(end), Expiration::AtHeight(start)) => { - if end > start { - Ok(end - start) - } else { - Ok(0) - } - } - (Expiration::AtTime(end), Expiration::AtTime(start)) => { - if end > start { - Ok(end.seconds() - start.seconds()) - } else { - Ok(0) - } - } - (Expiration::Never {}, Expiration::Never {}) => Ok(0), - _ => Err(StdError::generic_err(format!( - "incompatible expirations: got end {:?}, start {:?}", - end, start - ))), +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + let contract_version = get_contract_version(deps.storage)?; + + if contract_version.contract != CONTRACT_NAME { + return Err(ContractError::MigrationErrorIncorrectContract { + expected: CONTRACT_NAME.to_string(), + actual: contract_version.contract, + }); } -} -fn validate_voting_power_contract( - deps: &DepsMut, - vp_contract: String, -) -> Result { - let vp_contract = deps.api.addr_validate(&vp_contract)?; - let _: TotalPowerAtHeightResponse = deps.querier.query_wasm_smart( - &vp_contract, - &VotingQueryMsg::TotalPowerAtHeight { height: None }, - )?; - Ok(vp_contract) + let new_version: Version = CONTRACT_VERSION.parse()?; + let current_version: Version = contract_version.version.parse()?; + + // only allow upgrades + if new_version <= current_version { + return Err(ContractError::MigrationErrorInvalidVersion { + new: new_version.to_string(), + current: current_version.to_string(), + }); + } + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default()) } diff --git a/contracts/distribution/dao-rewards-distributor/src/error.rs b/contracts/distribution/dao-rewards-distributor/src/error.rs index 68e661bb2..a34e7a17e 100644 --- a/contracts/distribution/dao-rewards-distributor/src/error.rs +++ b/contracts/distribution/dao-rewards-distributor/src/error.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{DivideByZeroError, OverflowError, StdError}; +use cw_utils::PaymentError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -12,30 +13,60 @@ pub enum ContractError { #[error(transparent)] Cw20Error(#[from] cw20_base::ContractError), - #[error("Invalid Cw20")] + #[error(transparent)] + Overflow(#[from] OverflowError), + + #[error(transparent)] + DivideByZero(#[from] DivideByZeroError), + + #[error(transparent)] + Payment(#[from] PaymentError), + + #[error("semver parsing error: {0}")] + SemVer(String), + + #[error("Invalid CW20")] InvalidCw20 {}, #[error("Invalid funds")] InvalidFunds {}, - #[error("Staking change hook sender is not staking contract")] + #[error("You cannot send native funds when creating a CW20 distribution")] + NoFundsOnCw20Create {}, + + #[error("Voting power changed hook sender incorrect")] InvalidHookSender {}, #[error("No rewards claimable")] NoRewardsClaimable {}, - #[error("Reward period not finished")] - RewardPeriodNotFinished {}, + #[error("All rewards have already been distributed")] + RewardsAlreadyDistributed {}, + + #[error("Distribution not found with ID {id}")] + DistributionNotFound { id: u64 }, + + #[error("Unexpected duplicate distribution with ID {id}")] + UnexpectedDuplicateDistributionId { id: u64 }, - #[error("Reward rate less then one per block")] - RewardRateLessThenOnePerBlock {}, + #[error("Invalid emission rate: {field} cannot be zero")] + InvalidEmissionRateFieldZero { field: String }, - #[error("Reward duration can not be zero")] - ZeroRewardDuration {}, + #[error("There is no voting power registered, so no one will receive these funds")] + NoVotingPowerNoRewards {}, - #[error("Rewards distributor shutdown error: {0}")] - ShutdownError(String), + #[error("Cannot update emission rate because this distribution has accumulated the maximum rewards. Start a new distribution with the new emission rate instead. (Overflow: {err})")] + DistributionHistoryTooLarge { err: String }, + + #[error("Invalid version migration. {new} is not newer than {current}.")] + MigrationErrorInvalidVersion { new: String, current: String }, + + #[error("Expected to migrate from contract {expected}. Got {actual}.")] + MigrationErrorIncorrectContract { expected: String, actual: String }, +} - #[error("Denom already registered")] - DenomAlreadyRegistered {}, +impl From for ContractError { + fn from(err: semver::Error) -> Self { + Self::SemVer(err.to_string()) + } } diff --git a/contracts/distribution/dao-rewards-distributor/src/helpers.rs b/contracts/distribution/dao-rewards-distributor/src/helpers.rs new file mode 100644 index 000000000..04e88c4e9 --- /dev/null +++ b/contracts/distribution/dao-rewards-distributor/src/helpers.rs @@ -0,0 +1,147 @@ +use cosmwasm_std::{ + coins, to_json_binary, Addr, BankMsg, BlockInfo, CosmosMsg, Deps, DepsMut, StdError, StdResult, + Uint128, Uint256, WasmMsg, +}; +use cw20::{Denom, Expiration}; +use cw_utils::Duration; +use dao_interface::voting::{ + Query as VotingQueryMsg, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; + +use crate::ContractError; + +pub fn get_total_voting_power_at_block( + deps: Deps, + block: &BlockInfo, + contract_addr: &Addr, +) -> StdResult { + let msg = VotingQueryMsg::TotalPowerAtHeight { + height: Some(block.height), + }; + let resp: TotalPowerAtHeightResponse = deps.querier.query_wasm_smart(contract_addr, &msg)?; + Ok(resp.power) +} + +pub fn get_voting_power_at_block( + deps: Deps, + block: &BlockInfo, + contract_addr: &Addr, + addr: &Addr, +) -> StdResult { + let msg = VotingQueryMsg::VotingPowerAtHeight { + address: addr.into(), + height: Some(block.height), + }; + let resp: VotingPowerAtHeightResponse = deps.querier.query_wasm_smart(contract_addr, &msg)?; + Ok(resp.power) +} + +/// Returns the appropriate CosmosMsg for transferring the reward token. +pub fn get_transfer_msg(recipient: Addr, amount: Uint128, denom: Denom) -> StdResult { + match denom { + Denom::Native(denom) => Ok(BankMsg::Send { + to_address: recipient.into_string(), + amount: coins(amount.u128(), denom), + } + .into()), + Denom::Cw20(addr) => { + let cw20_msg = to_json_binary(&cw20::Cw20ExecuteMsg::Transfer { + recipient: recipient.into_string(), + amount, + })?; + Ok(WasmMsg::Execute { + contract_addr: addr.into_string(), + msg: cw20_msg, + funds: vec![], + } + .into()) + } + } +} + +pub(crate) fn scale_factor() -> Uint256 { + Uint256::from(10u8).pow(39) +} + +pub fn validate_voting_power_contract( + deps: &DepsMut, + vp_contract: String, +) -> Result { + let vp_contract = deps.api.addr_validate(&vp_contract)?; + let _: TotalPowerAtHeightResponse = deps.querier.query_wasm_smart( + &vp_contract, + &VotingQueryMsg::TotalPowerAtHeight { height: None }, + )?; + Ok(vp_contract) +} + +pub trait ExpirationExt { + /// Compute the duration since the start, flooring at 0 if the current + /// expiration is before the start. If either is never, or if they have + /// different units, returns an error as those cannot be compared. + fn duration_since(&self, start: &Self) -> StdResult; +} + +impl ExpirationExt for Expiration { + fn duration_since(&self, start: &Self) -> StdResult { + match (self, start) { + (Expiration::AtHeight(end), Expiration::AtHeight(start)) => { + if end > start { + Ok(Duration::Height(end - start)) + } else { + Ok(Duration::Height(0)) + } + } + (Expiration::AtTime(end), Expiration::AtTime(start)) => { + if end > start { + Ok(Duration::Time(end.seconds() - start.seconds())) + } else { + Ok(Duration::Time(0)) + } + } + (Expiration::Never {}, _) | (_, Expiration::Never {}) => { + Err(StdError::generic_err(format!( + "can't compute diff between expirations with never: got end {:?} and start {:?}", + self, start + ))) + } + _ => Err(StdError::generic_err(format!( + "incompatible expirations: got end {:?} and start {:?}", + self, start + ))), + } + } +} + +pub trait DurationExt { + /// Returns true if the duration is 0 blocks or 0 seconds. + fn is_zero(&self) -> bool; + + /// Perform checked integer division between two durations, erroring if the + /// units do not match or denominator is 0. + fn checked_div(&self, denominator: &Self) -> Result; +} + +impl DurationExt for Duration { + fn is_zero(&self) -> bool { + match self { + Duration::Height(h) => *h == 0, + Duration::Time(t) => *t == 0, + } + } + + fn checked_div(&self, denominator: &Self) -> Result { + match (self, denominator) { + (Duration::Height(numerator), Duration::Height(denominator)) => { + Ok(Uint128::from(*numerator).checked_div(Uint128::from(*denominator))?) + } + (Duration::Time(numerator), Duration::Time(denominator)) => { + Ok(Uint128::from(*numerator).checked_div(Uint128::from(*denominator))?) + } + _ => Err(ContractError::Std(StdError::generic_err(format!( + "incompatible durations: got numerator {:?} and denominator {:?}", + self, denominator + )))), + } + } +} diff --git a/contracts/distribution/dao-rewards-distributor/src/hooks.rs b/contracts/distribution/dao-rewards-distributor/src/hooks.rs index d57185f12..02ad08933 100644 --- a/contracts/distribution/dao-rewards-distributor/src/hooks.rs +++ b/contracts/distribution/dao-rewards-distributor/src/hooks.rs @@ -1,33 +1,54 @@ -use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cosmwasm_std::{Addr, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Storage}; use cw4::MemberChangedHookMsg; use dao_hooks::{nft_stake::NftStakeChangedHookMsg, stake::StakeChangedHookMsg}; -use crate::{contract::update_rewards, state::REGISTERED_HOOK_DENOMS, ContractError}; +use crate::{rewards::update_rewards, state::REGISTERED_HOOKS, ContractError}; -/// Register a hook caller contract for a given denom. -pub(crate) fn subscribe_denom_to_hook( - deps: DepsMut, - denom: String, +/// Register a hook caller contract for a given distribution ID. +pub(crate) fn subscribe_distribution_to_hook( + storage: &mut dyn Storage, + distribution_id: u64, hook: Addr, ) -> Result<(), ContractError> { - REGISTERED_HOOK_DENOMS.update(deps.storage, hook, |denoms| -> StdResult<_> { + REGISTERED_HOOKS.update(storage, hook, |denoms| -> StdResult<_> { let mut denoms = denoms.unwrap_or_default(); - denoms.push(denom.to_string()); + denoms.push(distribution_id); Ok(denoms) })?; Ok(()) } +/// Unregister a hook caller contract for a given distribution ID. +pub(crate) fn unsubscribe_distribution_from_hook( + storage: &mut dyn Storage, + distribution_id: u64, + hook: Addr, +) -> Result<(), ContractError> { + let mut denoms = REGISTERED_HOOKS + .may_load(storage, hook.clone())? + .unwrap_or_default(); + + denoms.retain(|id| *id != distribution_id); + + if denoms.is_empty() { + REGISTERED_HOOKS.remove(storage, hook); + } else { + REGISTERED_HOOKS.save(storage, hook, &denoms)?; + } + + Ok(()) +} + /// Ensures hooks that update voting power are only called by a designated /// hook_caller contract. -/// Returns a list of denoms that the hook caller is registered for. -pub(crate) fn get_hook_caller_registered_denoms( +/// Returns a list of distribution IDs that the hook caller is registered for. +pub(crate) fn get_hook_caller_registered_distribution_ids( deps: Deps, info: MessageInfo, -) -> Result, ContractError> { +) -> Result, ContractError> { // only a designated hook_caller contract can call this hook. // failing to load the registered denoms for a given hook returns an error. - REGISTERED_HOOK_DENOMS + REGISTERED_HOOKS .load(deps.storage, info.sender.clone()) .map_err(|_| ContractError::InvalidHookSender {}) } @@ -39,12 +60,14 @@ pub(crate) fn execute_stake_changed( msg: StakeChangedHookMsg, ) -> Result { // Check that the sender is the vp_contract (or the hook_caller if configured). - let hooked_denoms = get_hook_caller_registered_denoms(deps.as_ref(), info)?; + let hooked_distribution_ids = get_hook_caller_registered_distribution_ids(deps.as_ref(), info)?; match msg { - StakeChangedHookMsg::Stake { addr, .. } => execute_stake(deps, env, addr, hooked_denoms), + StakeChangedHookMsg::Stake { addr, .. } => { + update_for_stake(deps, env, addr, hooked_distribution_ids) + } StakeChangedHookMsg::Unstake { addr, .. } => { - execute_unstake(deps, env, addr, hooked_denoms) + execute_unstake(deps, env, addr, hooked_distribution_ids) } } } @@ -56,13 +79,13 @@ pub(crate) fn execute_membership_changed( msg: MemberChangedHookMsg, ) -> Result { // Check that the sender is the vp_contract (or the hook_caller if configured). - let hooked_denoms = get_hook_caller_registered_denoms(deps.as_ref(), info)?; + let hooked_distribution_ids = get_hook_caller_registered_distribution_ids(deps.as_ref(), info)?; // Get the addresses of members whose voting power has changed. for member in msg.diffs { let addr = deps.api.addr_validate(&member.key)?; - for denom in hooked_denoms.clone() { - update_rewards(&mut deps, &env, &addr, denom)?; + for id in hooked_distribution_ids.clone() { + update_rewards(&mut deps, &env, &addr, id)?; } } @@ -76,25 +99,28 @@ pub(crate) fn execute_nft_stake_changed( msg: NftStakeChangedHookMsg, ) -> Result { // Check that the sender is the vp_contract (or the hook_caller if configured). - let hooked_denoms = get_hook_caller_registered_denoms(deps.as_ref(), info)?; + let hooked_distribution_ids = get_hook_caller_registered_distribution_ids(deps.as_ref(), info)?; match msg { - NftStakeChangedHookMsg::Stake { addr, .. } => execute_stake(deps, env, addr, hooked_denoms), + NftStakeChangedHookMsg::Stake { addr, .. } => { + update_for_stake(deps, env, addr, hooked_distribution_ids) + } NftStakeChangedHookMsg::Unstake { addr, .. } => { - execute_unstake(deps, env, addr, hooked_denoms) + execute_unstake(deps, env, addr, hooked_distribution_ids) } } } -pub(crate) fn execute_stake( +pub(crate) fn update_for_stake( mut deps: DepsMut, env: Env, addr: Addr, - hooked_denoms: Vec, + hooked_distribution_ids: Vec, ) -> Result { - // update rewards for every denom that the hook caller is registered for - for denom in hooked_denoms { - update_rewards(&mut deps, &env, &addr, denom)?; + // update rewards for every distribution ID that the hook caller is + // registered for + for id in hooked_distribution_ids { + update_rewards(&mut deps, &env, &addr, id)?; } Ok(Response::new().add_attribute("action", "stake")) } @@ -103,11 +129,12 @@ pub(crate) fn execute_unstake( mut deps: DepsMut, env: Env, addr: Addr, - hooked_denoms: Vec, + hooked_distribution_ids: Vec, ) -> Result { - // update rewards for every denom that the hook caller is registered for - for denom in hooked_denoms { - update_rewards(&mut deps, &env, &addr, denom)?; + // update rewards for every distribution ID that the hook caller is + // registered for + for id in hooked_distribution_ids { + update_rewards(&mut deps, &env, &addr, id)?; } Ok(Response::new().add_attribute("action", "unstake")) } diff --git a/contracts/distribution/dao-rewards-distributor/src/lib.rs b/contracts/distribution/dao-rewards-distributor/src/lib.rs index 51ae5c619..8226f57a9 100644 --- a/contracts/distribution/dao-rewards-distributor/src/lib.rs +++ b/contracts/distribution/dao-rewards-distributor/src/lib.rs @@ -2,8 +2,10 @@ pub mod contract; mod error; +pub mod helpers; pub mod hooks; pub mod msg; +pub mod rewards; pub mod state; #[cfg(test)] diff --git a/contracts/distribution/dao-rewards-distributor/src/msg.rs b/contracts/distribution/dao-rewards-distributor/src/msg.rs index e2d41c112..4ca86340e 100644 --- a/contracts/distribution/dao-rewards-distributor/src/msg.rs +++ b/contracts/distribution/dao-rewards-distributor/src/msg.rs @@ -1,30 +1,28 @@ -use std::collections::HashMap; - use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{StdError, StdResult, Uint128, Uint256}; -use cw20::{Cw20ReceiveMsg, UncheckedDenom}; +use cosmwasm_std::Uint128; +use cw20::{Cw20ReceiveMsg, Denom, UncheckedDenom}; use cw4::MemberChangedHookMsg; use cw_ownable::cw_ownable_execute; -use cw_utils::Duration; use dao_hooks::{nft_stake::NftStakeChangedHookMsg, stake::StakeChangedHookMsg}; use dao_interface::voting::InfoResponse; -use crate::{state::DenomRewardState, ContractError}; - // so that consumers don't need a cw_ownable or cw_controllers dependency // to consume this contract's queries. pub use cw_controllers::ClaimsResponse; pub use cw_ownable::Ownership; +use crate::state::{DistributionState, EmissionRate}; + #[cw_serde] pub struct InstantiateMsg { - /// The owner of the contract. Is able to fund the contract and update - /// the reward duration. + /// The owner of the contract. Is able to fund the contract and update the + /// reward duration. If not provided, the instantiator is used. pub owner: Option, } #[cw_ownable_execute] #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Called when a member is added or removed /// to a cw4-groups or cw721-roles contract. @@ -33,109 +31,108 @@ pub enum ExecuteMsg { NftStakeChangeHook(NftStakeChangedHookMsg), /// Called when tokens are staked or unstaked. StakeChangeHook(StakeChangedHookMsg), - /// Claims rewards for the sender. - Claim { denom: String }, + /// registers a new distribution + Create(CreateMsg), + /// updates the config for a distribution + Update { + /// distribution ID to update + id: u64, + /// reward emission rate + emission_rate: Option, + /// address to query the voting power + vp_contract: Option, + /// address that will update the reward split when the voting power + /// distribution changes + hook_caller: Option, + /// destination address for reward clawbacks. defaults to owner + withdraw_destination: Option, + }, /// Used to fund this contract with cw20 tokens. Receive(Cw20ReceiveMsg), /// Used to fund this contract with native tokens. - Fund {}, - /// shuts down the rewards distributor. withdraws all future staking rewards - /// back to the treasury. members can claim whatever they earned until this point. - Shutdown { denom: String }, - /// registers a new reward denom - RegisterRewardDenom { - denom: UncheckedDenom, - emission_rate: RewardEmissionRate, - vp_contract: String, - hook_caller: String, - withdraw_destination: Option, - }, + #[cw_orch(payable)] + Fund(FundMsg), + /// Claims rewards for the sender. + Claim { id: u64 }, + /// withdraws the undistributed rewards for a distribution. members can + /// claim whatever they earned until this point. this is effectively an + /// inverse to fund and does not affect any already-distributed rewards. + Withdraw { id: u64 }, } -/// defines how many tokens (amount) should be distributed per amount of time -/// (duration). e.g. 5udenom per hour. #[cw_serde] -pub struct RewardEmissionRate { - pub amount: Uint128, - pub duration: Duration, -} - -impl RewardEmissionRate { - pub fn validate_emission_time_window(&self) -> Result<(), ContractError> { - // Reward duration must be greater than 0 - if let Duration::Height(0) | Duration::Time(0) = self.duration { - return Err(ContractError::ZeroRewardDuration {}); - } - Ok(()) - } - - // find the duration of the funded period given emission config and funded amount - pub fn get_funded_period_duration(&self, funded_amount: Uint128) -> StdResult { - let funded_amount_u256 = Uint256::from(funded_amount); - let amount_u256 = Uint256::from(self.amount); - let amount_to_emission_rate_ratio = funded_amount_u256.checked_div(amount_u256)?; - - let ratio_str = amount_to_emission_rate_ratio.to_string(); - let ratio = ratio_str - .parse::() - .map_err(|e| StdError::generic_err(e.to_string()))?; - - let funded_period_duration = match self.duration { - Duration::Height(h) => { - let duration_height = match ratio.checked_mul(h) { - Some(duration) => duration, - None => return Err(StdError::generic_err("overflow")), - }; - Duration::Height(duration_height) - } - Duration::Time(t) => { - let duration_time = match ratio.checked_mul(t) { - Some(duration) => duration, - None => return Err(StdError::generic_err("overflow")), - }; - Duration::Time(duration_time) - } - }; - - Ok(funded_period_duration) - } +pub struct CreateMsg { + /// denom to distribute + pub denom: UncheckedDenom, + /// reward emission rate + pub emission_rate: EmissionRate, + /// address to query the voting power + pub vp_contract: String, + /// address that will update the reward split when the voting power + /// distribution changes + pub hook_caller: String, + /// destination address for reward clawbacks. defaults to owner + pub withdraw_destination: Option, } #[cw_serde] -pub enum MigrateMsg {} +pub struct FundMsg { + /// distribution ID to fund + pub id: u64, +} #[cw_serde] -pub enum ReceiveMsg { +pub enum ReceiveCw20Msg { /// Used to fund this contract with cw20 tokens. - Fund {}, + Fund(FundMsg), } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { /// Returns contract version info #[returns(InfoResponse)] Info {}, - /// Returns the state of the registered reward distributions. - #[returns(RewardsStateResponse)] - RewardsState {}, - /// Returns the pending rewards for the given address. - #[returns(PendingRewardsResponse)] - GetPendingRewards { address: String }, /// Returns information about the ownership of this contract. #[returns(::cw_ownable::Ownership<::cosmwasm_std::Addr>)] Ownership {}, - #[returns(DenomRewardState)] - DenomRewardState { denom: String }, + /// Returns the pending rewards for the given address. + #[returns(PendingRewardsResponse)] + PendingRewards { + address: String, + start_after: Option, + limit: Option, + }, + /// Returns the state of the given distribution. + #[returns(DistributionState)] + Distribution { id: u64 }, + /// Returns the state of all the distributions. + #[returns(DistributionsResponse)] + Distributions { + start_after: Option, + limit: Option, + }, } #[cw_serde] -pub struct RewardsStateResponse { - pub rewards: Vec, +pub struct DistributionsResponse { + pub distributions: Vec, } #[cw_serde] pub struct PendingRewardsResponse { - pub address: String, - pub pending_rewards: HashMap, + pub pending_rewards: Vec, +} + +#[cw_serde] +pub struct DistributionPendingRewards { + /// distribution ID + pub id: u64, + /// denomination of the pending rewards + pub denom: Denom, + /// amount of pending rewards in the denom being distributed + pub pending_rewards: Uint128, } + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/contracts/distribution/dao-rewards-distributor/src/rewards.rs b/contracts/distribution/dao-rewards-distributor/src/rewards.rs new file mode 100644 index 000000000..f0af1a0ec --- /dev/null +++ b/contracts/distribution/dao-rewards-distributor/src/rewards.rs @@ -0,0 +1,173 @@ +use cosmwasm_std::{Addr, BlockInfo, Deps, DepsMut, Env, StdResult, Uint128, Uint256}; + +use crate::{ + helpers::{ + get_total_voting_power_at_block, get_voting_power_at_block, scale_factor, DurationExt, + ExpirationExt, + }, + state::{DistributionState, EmissionRate, UserRewardState, DISTRIBUTIONS, USER_REWARDS}, + ContractError, +}; + +/// updates the user reward state for a given distribution and user address. +/// also syncs the global reward state with the latest puvp values. +pub fn update_rewards( + deps: &mut DepsMut, + env: &Env, + addr: &Addr, + distribution_id: u64, +) -> Result<(), ContractError> { + let mut distribution = DISTRIBUTIONS + .load(deps.storage, distribution_id) + .map_err(|_| ContractError::DistributionNotFound { + id: distribution_id, + })?; + + // user may not have a reward state set yet if that is their first time + // claiming, so we default to an empty state + let mut user_reward_state = USER_REWARDS + .may_load(deps.storage, addr.clone())? + .unwrap_or_default(); + + // first update the active epoch earned puvp value up to the current block + distribution.active_epoch.total_earned_puvp = + get_active_total_earned_puvp(deps.as_ref(), &env.block, &distribution)?; + distribution.active_epoch.bump_last_updated(&env.block); + + // then calculate the total applicable puvp, which is the sum of historical + // rewards earned puvp and the active epoch total earned puvp we just + // updated above based on the current block + let total_applicable_puvp = distribution + .active_epoch + .total_earned_puvp + .checked_add(distribution.historical_earned_puvp)?; + + let unaccounted_for_rewards = get_accrued_rewards_not_yet_accounted_for( + deps.as_ref(), + env, + addr, + total_applicable_puvp, + &distribution, + &user_reward_state, + )?; + + // get the pre-existing pending reward amount for the distribution + let previous_pending_reward_amount = user_reward_state + .pending_rewards + .get(&distribution.id) + .cloned() + .unwrap_or_default(); + + let amount_sum = unaccounted_for_rewards.checked_add(previous_pending_reward_amount)?; + + // get the amount of newly earned rewards for the distribution + user_reward_state + .pending_rewards + .insert(distribution_id, amount_sum); + + // update the accounted for amount to that of the total applicable puvp + user_reward_state + .accounted_for_rewards_puvp + .insert(distribution_id, total_applicable_puvp); + + // reflect the updated state changes + USER_REWARDS.save(deps.storage, addr.clone(), &user_reward_state)?; + DISTRIBUTIONS.save(deps.storage, distribution_id, &distribution)?; + + Ok(()) +} + +/// Calculate the total rewards per unit voting power in the active epoch. +pub fn get_active_total_earned_puvp( + deps: Deps, + block: &BlockInfo, + distribution: &DistributionState, +) -> Result { + match distribution.active_epoch.emission_rate { + EmissionRate::Paused {} => Ok(Uint256::zero()), + // this is updated manually during funding, so just return it here. + EmissionRate::Immediate {} => Ok(distribution.active_epoch.total_earned_puvp), + EmissionRate::Linear { + amount, duration, .. + } => { + let curr = distribution.active_epoch.total_earned_puvp; + + let last_time_rewards_distributed = + distribution.get_latest_reward_distribution_time(block); + + // get the duration from the last time rewards were updated to the + // last time rewards were distributed. this will be 0 if the rewards + // were updated at or after the last time rewards were distributed. + let new_reward_distribution_duration = last_time_rewards_distributed + .duration_since(&distribution.active_epoch.last_updated_total_earned_puvp)?; + + // no need to query total voting power and do math if distribution + // is already up to date. + if new_reward_distribution_duration.is_zero() { + return Ok(curr); + } + + let total_power = + get_total_voting_power_at_block(deps, block, &distribution.vp_contract)?; + + // if no voting power is registered, no one should receive rewards. + if total_power.is_zero() { + Ok(curr) + } else { + // count intervals of the rewards emission that have passed + // since the last update which need to be distributed + let complete_distribution_periods = + new_reward_distribution_duration.checked_div(&duration)?; + + // It is impossible for this to overflow as total rewards can + // never exceed max value of Uint128 as total tokens in + // existence cannot exceed Uint128 (because the bank module Coin + // type uses Uint128). + let new_rewards_distributed = amount + .full_mul(complete_distribution_periods) + .checked_mul(scale_factor())?; + + // the new rewards per unit voting power that have been + // distributed since the last update + let new_rewards_puvp = new_rewards_distributed.checked_div(total_power.into())?; + Ok(curr.checked_add(new_rewards_puvp)?) + } + } + } +} + +// get a user's rewards not yet accounted for in their reward state (not pending +// nor claimed, but available to them due to the passage of time). +pub fn get_accrued_rewards_not_yet_accounted_for( + deps: Deps, + env: &Env, + addr: &Addr, + total_earned_puvp: Uint256, + distribution: &DistributionState, + user_reward_state: &UserRewardState, +) -> StdResult { + // get the user's voting power at the current height + let voting_power: Uint256 = + get_voting_power_at_block(deps, &env.block, &distribution.vp_contract, addr)?.into(); + + // get previous reward per unit voting power accounted for + let user_last_reward_puvp = user_reward_state + .accounted_for_rewards_puvp + .get(&distribution.id) + .cloned() + .unwrap_or_default(); + + // calculate the difference between the current total reward per unit + // voting power distributed and the user's latest reward per unit voting + // power accounted for. + let reward_factor = total_earned_puvp.checked_sub(user_last_reward_puvp)?; + + // calculate the amount of rewards earned: + // voting_power * reward_factor / scale_factor + let accrued_rewards_amount: Uint128 = voting_power + .checked_mul(reward_factor)? + .checked_div(scale_factor())? + .try_into()?; + + Ok(accrued_rewards_amount) +} diff --git a/contracts/distribution/dao-rewards-distributor/src/state.rs b/contracts/distribution/dao-rewards-distributor/src/state.rs index 1489266a3..812c4a3c3 100644 --- a/contracts/distribution/dao-rewards-distributor/src/state.rs +++ b/contracts/distribution/dao-rewards-distributor/src/state.rs @@ -1,163 +1,387 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{ensure, Addr, BlockInfo, StdError, StdResult, Uint128, Uint256}; +use cosmwasm_std::{ + ensure, Addr, BlockInfo, Decimal, Deps, StdError, StdResult, Timestamp, Uint128, Uint256, + Uint64, +}; use cw20::{Denom, Expiration}; -use cw_storage_plus::Map; +use cw_storage_plus::{Item, Map}; use cw_utils::Duration; use std::{cmp::min, collections::HashMap}; -use crate::{msg::RewardEmissionRate, ContractError}; +use crate::{ + helpers::{get_total_voting_power_at_block, scale_factor, DurationExt, ExpirationExt}, + rewards::get_active_total_earned_puvp, + ContractError, +}; /// map user address to their unique reward state -pub const USER_REWARD_STATES: Map = Map::new("u_r_s"); +pub const USER_REWARDS: Map = Map::new("ur"); -/// map denom string to the state of its reward distribution -pub const DENOM_REWARD_STATES: Map = Map::new("d_r_s"); +/// map distribution ID to the its distribution state +pub const DISTRIBUTIONS: Map = Map::new("d"); -/// map registered hooks to list of denoms they're registered for -pub const REGISTERED_HOOK_DENOMS: Map> = Map::new("r_h_d"); +/// map registered hooks to list of distribution IDs they're registered for +pub const REGISTERED_HOOKS: Map> = Map::new("rh"); + +/// The number of distributions that have been created. +pub const COUNT: Item = Item::new("count"); #[cw_serde] #[derive(Default)] pub struct UserRewardState { - /// map denom to the user's pending rewards - pub pending_denom_rewards: HashMap, - /// map denom string to the user's earned rewards per unit voting power that - /// have already been accounted for in pending rewards and potentially - /// claimed - pub denom_rewards_puvp: HashMap, + /// map distribution ID to the user's pending rewards that have been + /// accounted for but not yet claimed. + pub pending_rewards: HashMap, + /// map distribution ID to the user's earned rewards per unit voting power + /// that have already been accounted for (added to pending and maybe + /// claimed). + pub accounted_for_rewards_puvp: HashMap, } -/// the state of a denom's reward distribution +/// defines how many tokens (amount) should be distributed per amount of time +/// (duration). e.g. 5udenom per hour. #[cw_serde] -pub struct DenomRewardState { - /// validated denom (native or cw20) - pub denom: Denom, +pub enum EmissionRate { + /// rewards are paused + Paused {}, + /// rewards are distributed immediately + Immediate {}, + /// rewards are distributed at a constant rate + Linear { + /// amount of tokens to distribute per amount of time + amount: Uint128, + /// duration of time to distribute amount + duration: Duration, + /// whether or not reward distribution is continuous: whether future + /// funding after distribution finishes should be applied to the past, + /// or rewards are paused once all funding has been distributed. all + /// continuously backfilled rewards are distributed based on the current + /// voting power. + continuous: bool, + }, +} + +impl EmissionRate { + /// validate non-zero amount and duration if necessary + pub fn validate(&self) -> Result<(), ContractError> { + match self { + EmissionRate::Paused {} => Ok(()), + EmissionRate::Immediate {} => Ok(()), + EmissionRate::Linear { + amount, duration, .. + } => { + if amount.is_zero() { + return Err(ContractError::InvalidEmissionRateFieldZero { + field: "amount".to_string(), + }); + } + if duration.is_zero() { + return Err(ContractError::InvalidEmissionRateFieldZero { + field: "duration".to_string(), + }); + } + Ok(()) + } + } + } + + /// find the duration of the funded period given funded amount. e.g. if the + /// funded amount is twice the emission rate amount, the funded period + /// should be twice the emission rate duration, since the funded amount + /// takes two emission cycles to be distributed. + pub fn get_funded_period_duration( + &self, + funded_amount: Uint128, + ) -> StdResult> { + match self { + // if rewards are paused, return no duration + EmissionRate::Paused {} => Ok(None), + // if rewards are immediate, return no duration + EmissionRate::Immediate {} => Ok(None), + // if rewards are linear, calculate based on funded amount + EmissionRate::Linear { + amount, duration, .. + } => { + let amount_to_emission_rate_ratio = Decimal::from_ratio(funded_amount, *amount); + + let funded_duration = match duration { + Duration::Height(h) => { + let duration_height = Uint128::from(*h) + .checked_mul_floor(amount_to_emission_rate_ratio) + .map_err(|e| StdError::generic_err(e.to_string()))?; + let duration = Uint64::try_from(duration_height)?.u64(); + Duration::Height(duration) + } + Duration::Time(t) => { + let duration_time = Uint128::from(*t) + .checked_mul_floor(amount_to_emission_rate_ratio) + .map_err(|e| StdError::generic_err(e.to_string()))?; + let duration = Uint64::try_from(duration_time)?.u64(); + Duration::Time(duration) + } + }; + + Ok(Some(funded_duration)) + } + } + } +} + +#[cw_serde] +pub struct Epoch { + /// reward emission rate + pub emission_rate: EmissionRate, /// the time when the current reward distribution period started. period /// finishes iff it reaches its end. pub started_at: Expiration, /// the time when all funded rewards are allocated to users and thus the /// distribution period ends. pub ends_at: Expiration, - /// reward emission rate - pub emission_rate: RewardEmissionRate, /// total rewards earned per unit voting power from started_at to - /// last_update + /// last_updated_total_earned_puvp pub total_earned_puvp: Uint256, - /// time when total_earned_puvp was last updated for this denom - pub last_update: Expiration, + /// time when total_earned_puvp was last updated + pub last_updated_total_earned_puvp: Expiration, +} + +impl Epoch { + /// bump the last_updated_total_earned_puvp field to the minimum of the + /// current block and ends_at since rewards cannot be distributed after + /// ends_at. this is necessary in the case that a future funding backfills + /// rewards after they've finished distributing. in order to compute over + /// the missed space, last_updated can never be greater than ends_at. if + /// ends_at is never, the epoch must be paused, so it should never be + /// updated. + pub fn bump_last_updated(&mut self, current_block: &BlockInfo) { + match self.ends_at { + Expiration::Never {} => { + self.last_updated_total_earned_puvp = Expiration::Never {}; + } + Expiration::AtHeight(ends_at_height) => { + self.last_updated_total_earned_puvp = + Expiration::AtHeight(std::cmp::min(current_block.height, ends_at_height)); + } + Expiration::AtTime(ends_at_time) => { + self.last_updated_total_earned_puvp = + Expiration::AtTime(std::cmp::min(current_block.time, ends_at_time)); + } + } + } +} + +/// the state of a reward distribution +#[cw_serde] +pub struct DistributionState { + /// distribution ID + pub id: u64, + /// validated denom (native or cw20) + pub denom: Denom, + /// current distribution epoch state + pub active_epoch: Epoch, /// address to query the voting power pub vp_contract: Addr, /// address that will update the reward split when the voting power /// distribution changes pub hook_caller: Addr, - /// total amount of rewards funded + /// total amount of rewards funded that will be distributed in the active + /// epoch. pub funded_amount: Uint128, - /// optional destination address for reward clawbacks + /// destination address for reward clawbacks pub withdraw_destination: Addr, + /// historical rewards earned per unit voting power from past epochs due to + /// changes in the emission rate. each time emission rate is changed, this + /// value is increased by the `active_epoch`'s rewards earned puvp. + pub historical_earned_puvp: Uint256, } -impl DenomRewardState { - pub fn bump_last_update(mut self, current_block: &BlockInfo) -> Self { - self.last_update = match self.emission_rate.duration { - Duration::Height(_) => Expiration::AtHeight(current_block.height), - Duration::Time(_) => Expiration::AtTime(current_block.time), - }; - self - } - - /// tries to update the last funding date. - /// if distribution expiration is in the future, nothing changes. - /// if distribution expiration is in the past, or had never been set, - /// funding date becomes the current block. - pub fn bump_funding_date(mut self, current_block: &BlockInfo) -> Self { - // if its never been set before, we set it to current block and return - if let Expiration::Never {} = self.started_at { - self.started_at = match self.emission_rate.duration { - Duration::Height(_) => Expiration::AtHeight(current_block.height), - Duration::Time(_) => Expiration::AtTime(current_block.time), - }; - return self; - } - - // if current distribution is expired, we set the funding date - // to the current date - if self.ends_at.is_expired(current_block) { - self.started_at = match self.emission_rate.duration { - Duration::Height(_) => Expiration::AtHeight(current_block.height), - Duration::Time(_) => Expiration::AtTime(current_block.time), - }; - } - - self - } - - pub fn to_str_denom(&self) -> String { +impl DistributionState { + pub fn get_denom_string(&self) -> String { match &self.denom { Denom::Native(denom) => denom.to_string(), Denom::Cw20(address) => address.to_string(), } } - /// Returns the ends_at time value as a u64. - /// - If `Never`, returns an error. - /// - If `AtHeight(h)`, the value is `h`. - /// - If `AtTime(t)`, the value is `t`, where t is seconds. - pub fn get_ends_at_scalar(&self) -> StdResult { - match self.ends_at { - Expiration::Never {} => Err(StdError::generic_err("reward period is not active")), - Expiration::AtHeight(h) => Ok(h), - Expiration::AtTime(t) => Ok(t.seconds()), - } - } - - /// Returns the started_at time value as a u64. - /// - If `Never`, returns an error. - /// - If `AtHeight(h)`, the value is `h`. - /// - If `AtTime(t)`, the value is `t`, where t is seconds. - pub fn get_started_at_scalar(&self) -> StdResult { - match self.started_at { - Expiration::AtHeight(h) => Ok(h), - Expiration::AtTime(t) => Ok(t.seconds()), - Expiration::Never {} => Err(StdError::generic_err("reward period is not active")), - } - } - /// Returns the latest time when rewards were distributed. Works by /// comparing `current_block` with the distribution end time: - /// - If the end is `Never`, then no rewards are being distributed, thus we - /// return `Never`. + /// - If the end is `Never`, then no rewards are currently being + /// distributed, so return the last update. /// - If the end is `AtHeight(h)` or `AtTime(t)`, we compare the current - /// block height or time with `h` or `t` respectively. + /// block height or time with `h` or `t` respectively. /// - If current block respective value is before the end, rewards are still - /// being distributed. We therefore return the current block `height` or - /// `time`, as this block is the most recent time rewards were distributed. + /// being distributed. We therefore return the current block `height` or + /// `time`, as this block is the most recent time rewards were + /// distributed. /// - If current block respective value is after the end, rewards are no - /// longer being distributed. We therefore return the end `height` or - /// `time`, as that was the last date where rewards were distributed. + /// longer being distributed. We therefore return the end `height` or + /// `time`, as that was the last date where rewards were distributed. pub fn get_latest_reward_distribution_time(&self, current_block: &BlockInfo) -> Expiration { - match self.ends_at { - Expiration::Never {} => Expiration::Never {}, - Expiration::AtHeight(h) => Expiration::AtHeight(min(current_block.height, h)), - Expiration::AtTime(t) => Expiration::AtTime(min(current_block.time, t)), + match self.active_epoch.ends_at { + Expiration::Never {} => self.active_epoch.last_updated_total_earned_puvp, + Expiration::AtHeight(ends_at_height) => { + Expiration::AtHeight(min(current_block.height, ends_at_height)) + } + Expiration::AtTime(ends_at_time) => { + Expiration::AtTime(min(current_block.time, ends_at_time)) + } } } - /// Returns `ContractError::RewardPeriodNotFinished` if the period finish - /// expiration is of either `AtHeight` or `AtTime` variant and is earlier - /// than the current block height or time respectively. - pub fn validate_period_finish_expiration_if_set( - &self, + /// get the total rewards to be distributed based on the active epoch's + /// emission rate + pub fn get_total_rewards(&self) -> Result { + match self.active_epoch.emission_rate { + EmissionRate::Paused {} => Ok(Uint128::zero()), + EmissionRate::Immediate {} => Ok(self.funded_amount), + EmissionRate::Linear { + amount, duration, .. + } => { + let epoch_duration = self + .active_epoch + .ends_at + .duration_since(&self.active_epoch.started_at)?; + + // count total intervals of the rewards emission that will pass + // based on the start and end times. + let complete_distribution_periods = epoch_duration.checked_div(&duration)?; + + Ok(amount.checked_mul(complete_distribution_periods)?) + } + } + } + + /// Finish current epoch early and start a new one with a new emission rate. + pub fn transition_epoch( + &mut self, + deps: Deps, + new_emission_rate: EmissionRate, current_block: &BlockInfo, ) -> Result<(), ContractError> { - match self.ends_at { - Expiration::AtHeight(_) | Expiration::AtTime(_) => { - ensure!( - self.ends_at.is_expired(current_block), - ContractError::RewardPeriodNotFinished {} - ); - Ok(()) + // if the new emission rate is the same as the active one, do nothing + if self.active_epoch.emission_rate == new_emission_rate { + return Ok(()); + } + + // 1. finish current epoch by updating rewards and setting end to now + self.active_epoch.total_earned_puvp = + get_active_total_earned_puvp(deps, current_block, self)?; + self.active_epoch.ends_at = match self.active_epoch.started_at { + Expiration::Never {} => Expiration::Never {}, + Expiration::AtHeight(_) => Expiration::AtHeight(current_block.height), + Expiration::AtTime(_) => Expiration::AtTime(current_block.time), + }; + + // 2. add current epoch rewards earned to historical rewards + self.historical_earned_puvp = self + .historical_earned_puvp + .checked_add(self.active_epoch.total_earned_puvp) + .map_err(|err| ContractError::DistributionHistoryTooLarge { + err: err.to_string(), + })?; + + // 3. deduct the distributed rewards amount from total funded amount, as + // those rewards are no longer distributed in the new epoch + let active_epoch_earned_rewards = self.get_total_rewards()?; + self.funded_amount = self + .funded_amount + .checked_sub(active_epoch_earned_rewards)?; + + // 4. start new epoch + + // we get the duration of the funded period and add it to the current + // block height. if the sum overflows, we return u64::MAX, as it + // suggests that the period is infinite or so long that it doesn't + // matter. + let new_ends_at = match new_emission_rate.get_funded_period_duration(self.funded_amount)? { + Some(Duration::Height(h)) => { + if current_block.height.checked_add(h).is_some() { + Expiration::AtHeight(current_block.height + h) + } else { + Expiration::AtHeight(u64::MAX) + } + } + Some(Duration::Time(t)) => { + if current_block.time.seconds().checked_add(t).is_some() { + Expiration::AtTime(current_block.time.plus_seconds(t)) + } else { + Expiration::AtTime(Timestamp::from_seconds(u64::MAX)) + } } - Expiration::Never {} => Ok(()), + // if there is no funded period duration, but the emission rate is + // immediate, set ends_at to the current block height to match + // started_at below, since funds are distributed immediately + None => Expiration::Never {}, + }; + + let new_started_at = match new_emission_rate { + EmissionRate::Paused {} => Expiration::Never {}, + EmissionRate::Immediate {} => Expiration::Never {}, + EmissionRate::Linear { duration, .. } => match duration { + Duration::Height(_) => Expiration::AtHeight(current_block.height), + Duration::Time(_) => Expiration::AtTime(current_block.time), + }, + }; + + self.active_epoch = Epoch { + emission_rate: new_emission_rate.clone(), + started_at: new_started_at, + ends_at: new_ends_at, + // start the new active epoch with zero rewards earned + total_earned_puvp: Uint256::zero(), + last_updated_total_earned_puvp: new_started_at, + }; + + // if new emission rate is immediate, update total_earned_puvp with + // remaining funded_amount right away + if (self.active_epoch.emission_rate == EmissionRate::Immediate {}) { + self.update_immediate_emission_total_earned_puvp( + deps, + current_block, + self.funded_amount, + )?; + } + + Ok(()) + } + + /// Update the total_earned_puvp field in the active epoch for immediate + /// emission. This logic normally lives in get_active_total_earned_puvp, but + /// we need only need to execute this right when funding, and we need to + /// know the delta in funded amount, which is not accessible anywhere other + /// than when being funded or transitioning to a new emission rate. + pub fn update_immediate_emission_total_earned_puvp( + &mut self, + deps: Deps, + block: &BlockInfo, + funded_amount_delta: Uint128, + ) -> Result<(), ContractError> { + // should never happen + ensure!( + self.active_epoch.emission_rate == EmissionRate::Immediate {}, + ContractError::Std(StdError::generic_err(format!( + "expected immediate emission, got {:?}", + self.active_epoch.emission_rate + ))) + ); + + let curr = self.active_epoch.total_earned_puvp; + + let total_power = get_total_voting_power_at_block(deps, block, &self.vp_contract)?; + + // if no voting power is registered, error since rewards can't be + // distributed. + if total_power.is_zero() { + Err(ContractError::NoVotingPowerNoRewards {}) + } else { + // the new rewards per unit voting power based on the funded amount + let new_rewards_puvp = Uint256::from(funded_amount_delta) + // this can never overflow since funded_amount is a Uint128 + .checked_mul(scale_factor())? + .checked_div(total_power.into())?; + + self.active_epoch.total_earned_puvp = curr.checked_add(new_rewards_puvp)?; + + Ok(()) } } } diff --git a/contracts/distribution/dao-rewards-distributor/src/testing/mod.rs b/contracts/distribution/dao-rewards-distributor/src/testing/mod.rs index f0b1d66ab..e84477456 100644 --- a/contracts/distribution/dao-rewards-distributor/src/testing/mod.rs +++ b/contracts/distribution/dao-rewards-distributor/src/testing/mod.rs @@ -7,9 +7,10 @@ pub mod tests; pub const DENOM: &str = "ujuno"; pub const ALT_DENOM: &str = "unotjuno"; pub const OWNER: &str = "owner"; -pub const ADDR1: &str = "addr0001"; -pub const ADDR2: &str = "addr0002"; -pub const ADDR3: &str = "addr0003"; +pub const ADDR1: &str = "addr1"; +pub const ADDR2: &str = "addr2"; +pub const ADDR3: &str = "addr3"; +pub const ADDR4: &str = "addr4"; pub fn contract_rewards() -> Box> { let contract = ContractWrapper::new( @@ -219,7 +220,7 @@ mod cw721_setup { use cosmwasm_std::{to_json_binary, Addr, Binary, Empty}; use cw_multi_test::{App, Executor}; - use dao_testing::contracts::{cw721_base_contract, voting_cw721_staked_contract}; + use dao_testing::contracts::{cw721_base_contract, cw721_staked_voting_contract}; use dao_voting_cw721_staked::state::Config; use super::OWNER; @@ -255,7 +256,7 @@ mod cw721_setup { pub fn setup_cw721_test(app: &mut App, initial_nfts: Vec) -> (Addr, Addr) { let cw721_code_id = app.store_code(cw721_base_contract()); - let vp_code_id = app.store_code(voting_cw721_staked_contract()); + let vp_code_id = app.store_code(cw721_staked_voting_contract()); let msg = dao_voting_cw721_staked::msg::InstantiateMsg { nft_contract: dao_voting_cw721_staked::msg::NftContract::New { diff --git a/contracts/distribution/dao-rewards-distributor/src/testing/suite.rs b/contracts/distribution/dao-rewards-distributor/src/testing/suite.rs index 2deb7b8f2..2df17e6c1 100644 --- a/contracts/distribution/dao-rewards-distributor/src/testing/suite.rs +++ b/contracts/distribution/dao-rewards-distributor/src/testing/suite.rs @@ -3,19 +3,20 @@ use std::borrow::BorrowMut; use cosmwasm_schema::cw_serde; use cosmwasm_std::{coin, coins, to_json_binary, Addr, Coin, Empty, Timestamp, Uint128}; use cw20::{Cw20Coin, Expiration, UncheckedDenom}; -use cw20_stake::msg::ReceiveMsg; use cw4::{Member, MemberListResponse}; use cw_multi_test::{App, BankSudo, Executor, SudoMsg}; -use cw_ownable::{Action, Ownership}; +use cw_ownable::Action; use cw_utils::Duration; +use dao_interface::voting::InfoResponse; use crate::{ msg::{ - ExecuteMsg, InstantiateMsg, PendingRewardsResponse, QueryMsg, RewardEmissionRate, - RewardsStateResponse, + CreateMsg, DistributionsResponse, ExecuteMsg, FundMsg, InstantiateMsg, + PendingRewardsResponse, QueryMsg, ReceiveCw20Msg, }, - state::DenomRewardState, + state::{DistributionState, EmissionRate}, testing::cw20_setup::instantiate_cw20, + ContractError, }; use super::{ @@ -42,12 +43,14 @@ pub struct RewardsConfig { pub denom: UncheckedDenom, pub duration: Duration, pub destination: Option, + pub continuous: bool, } pub struct SuiteBuilder { pub _instantiate: InstantiateMsg, pub dao_type: DaoType, pub rewards_config: RewardsConfig, + pub cw4_members: Vec, } impl SuiteBuilder { @@ -62,7 +65,22 @@ impl SuiteBuilder { denom: UncheckedDenom::Native(DENOM.to_string()), duration: Duration::Height(10), destination: None, + continuous: true, }, + cw4_members: vec![ + Member { + addr: ADDR1.to_string(), + weight: 2, + }, + Member { + addr: ADDR2.to_string(), + weight: 1, + }, + Member { + addr: ADDR3.to_string(), + weight: 1, + }, + ], } } @@ -71,6 +89,11 @@ impl SuiteBuilder { self } + pub fn with_cw4_members(mut self, cw4_members: Vec) -> Self { + self.cw4_members = cw4_members; + self + } + pub fn with_withdraw_destination(mut self, withdraw_destination: Option) -> Self { self.rewards_config.destination = withdraw_destination; self @@ -86,6 +109,7 @@ impl SuiteBuilder { owner: Some(owner.clone()), staking_addr: Addr::unchecked(""), voting_power_addr: Addr::unchecked(""), + reward_code_id: 0, distribution_contract: Addr::unchecked(""), cw20_addr: Addr::unchecked(""), reward_denom: DENOM.to_string(), @@ -100,23 +124,8 @@ impl SuiteBuilder { match self.dao_type { DaoType::CW4 => { - let members = vec![ - Member { - addr: ADDR1.to_string(), - weight: 2, - }, - Member { - addr: ADDR2.to_string(), - weight: 1, - }, - Member { - addr: ADDR3.to_string(), - weight: 1, - }, - ]; - let (voting_power_addr, dao_voting_addr) = - setup_cw4_test(suite_built.app.borrow_mut(), members); + setup_cw4_test(suite_built.app.borrow_mut(), self.cw4_members); suite_built.voting_power_addr = voting_power_addr.clone(); suite_built.staking_addr = dao_voting_addr.clone(); } @@ -221,12 +230,12 @@ impl SuiteBuilder { }; // initialize the rewards distributor - let reward_code_id = suite_built.app.borrow_mut().store_code(contract_rewards()); + suite_built.reward_code_id = suite_built.app.borrow_mut().store_code(contract_rewards()); let reward_addr = suite_built .app .borrow_mut() .instantiate_contract( - reward_code_id, + suite_built.reward_code_id, owner.clone(), &InstantiateMsg { owner: Some(owner.clone().into_string()), @@ -242,19 +251,23 @@ impl SuiteBuilder { match self.dao_type { DaoType::CW721 => { suite_built.register_hook(suite_built.voting_power_addr.clone()); - suite_built.register_reward_denom( + suite_built.create( self.rewards_config.clone(), suite_built.voting_power_addr.to_string().as_ref(), + None, ); match self.rewards_config.denom { UncheckedDenom::Native(_) => { - suite_built.fund_distributor_native(coin(100_000_000, DENOM.to_string())); + suite_built.fund_native(1, coin(100_000_000, DENOM.to_string())); } UncheckedDenom::Cw20(_) => { - suite_built.fund_distributor_cw20(Cw20Coin { - address: suite_built.cw20_addr.to_string(), - amount: Uint128::new(100_000_000), - }); + suite_built.fund_cw20( + 1, + Cw20Coin { + address: suite_built.cw20_addr.to_string(), + amount: Uint128::new(100_000_000), + }, + ); } }; } @@ -279,19 +292,23 @@ impl SuiteBuilder { }; suite_built.register_hook(suite_built.staking_addr.clone()); - suite_built.register_reward_denom( + suite_built.create( self.rewards_config.clone(), suite_built.staking_addr.to_string().as_ref(), + None, ); match &self.rewards_config.denom { UncheckedDenom::Native(_) => { - suite_built.fund_distributor_native(coin(100_000_000, DENOM.to_string())); + suite_built.fund_native(1, coin(100_000_000, DENOM.to_string())); } UncheckedDenom::Cw20(addr) => { - suite_built.fund_distributor_cw20(Cw20Coin { - address: addr.to_string(), - amount: Uint128::new(100_000_000), - }); + suite_built.fund_cw20( + 1, + Cw20Coin { + address: addr.to_string(), + amount: Uint128::new(100_000_000), + }, + ); } }; } @@ -311,6 +328,7 @@ pub struct Suite { pub voting_power_addr: Addr, pub reward_denom: String, + pub reward_code_id: u64, pub distribution_contract: Addr, // cw20 type fields @@ -320,9 +338,9 @@ pub struct Suite { // SUITE QUERIES impl Suite { pub fn get_time_until_rewards_expiration(&mut self) -> u64 { - let rewards_state_response = self.get_rewards_state_response(); + let distribution = &self.get_distributions().distributions[0]; let current_block = self.app.block_info(); - let (expiration_unit, current_unit) = match rewards_state_response.rewards[0].ends_at { + let (expiration_unit, current_unit) = match distribution.active_epoch.ends_at { cw20::Expiration::AtHeight(h) => (h, current_block.height), cw20::Expiration::AtTime(t) => (t.seconds(), current_block.time.seconds()), cw20::Expiration::Never {} => return 0, @@ -364,93 +382,107 @@ impl Suite { result.balance.u128() } - #[allow(dead_code)] - pub fn get_ownership>(&mut self, address: T) -> Ownership { - self.app - .wrap() - .query_wasm_smart(address, &QueryMsg::Ownership {}) - .unwrap() - } - - pub fn get_rewards_state_response(&mut self) -> RewardsStateResponse { + pub fn get_distributions(&mut self) -> DistributionsResponse { self.app .wrap() .query_wasm_smart( self.distribution_contract.clone(), - &QueryMsg::RewardsState {}, + &QueryMsg::Distributions { + start_after: None, + limit: None, + }, ) .unwrap() } - pub fn _get_denom_reward_state(&mut self, denom: &str) -> DenomRewardState { - let resp: DenomRewardState = self + pub fn get_distribution(&mut self, id: u64) -> DistributionState { + let resp: DistributionState = self .app .wrap() .query_wasm_smart( self.distribution_contract.clone(), - &QueryMsg::DenomRewardState { - denom: denom.to_string(), - }, + &QueryMsg::Distribution { id }, ) .unwrap(); - println!("[{} REWARD STATE] {:?}", denom, resp); resp } + + pub fn get_owner(&mut self) -> Addr { + let ownable_response: cw_ownable::Ownership = self + .app + .borrow_mut() + .wrap() + .query_wasm_smart(self.distribution_contract.clone(), &QueryMsg::Ownership {}) + .unwrap(); + ownable_response.owner.unwrap() + } + + pub fn get_info(&mut self) -> InfoResponse { + self.app + .borrow_mut() + .wrap() + .query_wasm_smart(self.distribution_contract.clone(), &QueryMsg::Info {}) + .unwrap() + } } // SUITE ASSERTIONS impl Suite { pub fn assert_ends_at(&mut self, expected: Expiration) { - let rewards_state_response = self.get_rewards_state_response(); - assert_eq!(rewards_state_response.rewards[0].ends_at, expected); + let distribution = &self.get_distributions().distributions[0]; + assert_eq!(distribution.active_epoch.ends_at, expected); } pub fn assert_started_at(&mut self, expected: Expiration) { - let denom_configs = self.get_rewards_state_response(); - assert_eq!(denom_configs.rewards[0].started_at, expected); + let distribution = &self.get_distributions().distributions[0]; + assert_eq!(distribution.active_epoch.started_at, expected); } pub fn assert_amount(&mut self, expected: u128) { - let rewards_state_response = self.get_rewards_state_response(); - assert_eq!( - rewards_state_response.rewards[0].emission_rate.amount, - Uint128::new(expected) - ); + let distribution = &self.get_distributions().distributions[0]; + match distribution.active_epoch.emission_rate { + EmissionRate::Paused {} => panic!("expected non-paused emission rate"), + EmissionRate::Immediate {} => panic!("expected non-immediate emission rate"), + EmissionRate::Linear { amount, .. } => assert_eq!(amount, Uint128::new(expected)), + } } pub fn assert_duration(&mut self, expected: u64) { - let rewards_state_response = self.get_rewards_state_response(); - let units = match rewards_state_response.rewards[0].emission_rate.duration { - Duration::Height(h) => h, - Duration::Time(t) => t, - }; - assert_eq!(units, expected); - } - - pub fn get_owner(&mut self) -> Addr { - let ownable_response: cw_ownable::Ownership = self - .app - .borrow_mut() - .wrap() - .query_wasm_smart(self.distribution_contract.clone(), &QueryMsg::Ownership {}) - .unwrap(); - ownable_response.owner.unwrap() + let distribution = &self.get_distributions().distributions[0]; + match distribution.active_epoch.emission_rate { + EmissionRate::Paused {} => panic!("expected non-paused emission rate"), + EmissionRate::Immediate {} => panic!("expected non-immediate emission rate"), + EmissionRate::Linear { duration, .. } => assert_eq!( + match duration { + Duration::Height(h) => h, + Duration::Time(t) => t, + }, + expected + ), + } } - pub fn assert_pending_rewards(&mut self, address: &str, _denom: &str, expected: u128) { + pub fn assert_pending_rewards(&mut self, address: &str, id: u64, expected: u128) { let res: PendingRewardsResponse = self .app .borrow_mut() .wrap() .query_wasm_smart( self.distribution_contract.clone(), - &QueryMsg::GetPendingRewards { + &QueryMsg::PendingRewards { address: address.to_string(), + start_after: None, + limit: None, }, ) .unwrap(); - let pending = res.pending_rewards.get(self.reward_denom.as_str()).unwrap(); + let pending = res + .pending_rewards + .iter() + .find(|p| p.id == id) + .unwrap() + .pending_rewards; assert_eq!( pending, @@ -461,23 +493,21 @@ impl Suite { ); } - pub fn assert_native_balance(&mut self, address: &str, denom: &str, expected: u128) { + pub fn assert_native_balance(&self, address: &str, denom: &str, expected: u128) { let balance = self.get_balance_native(address, denom); assert_eq!(balance, expected); } - pub fn assert_cw20_balance(&mut self, address: &str, expected: u128) { - let balance = self.get_balance_cw20(self.reward_denom.clone(), address); + pub fn assert_cw20_balance(&self, cw20: &str, address: &str, expected: u128) { + let balance = self.get_balance_cw20(cw20, address); assert_eq!(balance, expected); } } // SUITE ACTIONS impl Suite { - pub fn shutdown_denom_distribution(&mut self, denom: &str) { - let msg = ExecuteMsg::Shutdown { - denom: denom.to_string(), - }; + pub fn withdraw(&mut self, id: u64) { + let msg = ExecuteMsg::Withdraw { id }; self.app .execute_contract( Addr::unchecked(OWNER), @@ -488,26 +518,55 @@ impl Suite { .unwrap(); } + pub fn withdraw_error(&mut self, id: u64) -> ContractError { + let msg = ExecuteMsg::Withdraw { id }; + self.app + .execute_contract( + Addr::unchecked(OWNER), + self.distribution_contract.clone(), + &msg, + &[], + ) + .unwrap_err() + .downcast() + .unwrap() + } + pub fn register_hook(&mut self, addr: Addr) { let msg = cw4_group::msg::ExecuteMsg::AddHook { addr: self.distribution_contract.to_string(), }; - // TODO: cw721 check here self.app .execute_contract(Addr::unchecked(OWNER), addr, &msg, &[]) .unwrap(); } - pub fn register_reward_denom(&mut self, reward_config: RewardsConfig, hook_caller: &str) { - let register_reward_denom_msg = ExecuteMsg::RegisterRewardDenom { + pub fn create( + &mut self, + reward_config: RewardsConfig, + hook_caller: &str, + funds: Option, + ) { + let execute_create_msg = ExecuteMsg::Create(CreateMsg { denom: reward_config.denom.clone(), - emission_rate: RewardEmissionRate { + emission_rate: EmissionRate::Linear { amount: Uint128::new(reward_config.amount), duration: reward_config.duration, + continuous: reward_config.continuous, }, hook_caller: hook_caller.to_string(), vp_contract: self.voting_power_addr.to_string(), withdraw_destination: reward_config.destination, + }); + + // include funds if provided + let send_funds = if let Some(funds) = funds { + match reward_config.denom { + UncheckedDenom::Native(denom) => vec![coin(funds.u128(), denom)], + UncheckedDenom::Cw20(_) => vec![], + } + } else { + vec![] }; self.app @@ -515,13 +574,13 @@ impl Suite { .execute_contract( self.owner.clone().unwrap(), self.distribution_contract.clone(), - ®ister_reward_denom_msg, - &[], + &execute_create_msg, + &send_funds, ) .unwrap(); } - pub fn mint_native_coin(&mut self, coin: Coin, dest: &str) { + pub fn mint_native(&mut self, coin: Coin, dest: &str) { // mint the tokens to be funded self.app .borrow_mut() @@ -534,32 +593,25 @@ impl Suite { .unwrap(); } - pub fn mint_cw20_coin(&mut self, coin: Cw20Coin, dest: &str, name: &str) -> Addr { - let _msg = cw20::Cw20ExecuteMsg::Mint { - recipient: dest.to_string(), - amount: coin.amount, - }; + pub fn mint_cw20(&mut self, coin: Cw20Coin, name: &str) -> Addr { cw20_setup::instantiate_cw20(self.app.borrow_mut(), name, vec![coin]) } - pub fn fund_distributor_native(&mut self, coin: Coin) { - self.mint_native_coin(coin.clone(), OWNER); - println!("[FUNDING EVENT] native funding: {}", coin); + pub fn fund_native(&mut self, id: u64, coin: Coin) { + self.mint_native(coin.clone(), OWNER); self.app .borrow_mut() .execute_contract( Addr::unchecked(OWNER), self.distribution_contract.clone(), - &ExecuteMsg::Fund {}, + &ExecuteMsg::Fund(FundMsg { id }), &[coin], ) .unwrap(); } - pub fn fund_distributor_cw20(&mut self, coin: Cw20Coin) { - println!("[FUNDING EVENT] cw20 funding: {}", coin); - - let fund_sub_msg = to_json_binary(&ReceiveMsg::Fund {}).unwrap(); + pub fn fund_cw20(&mut self, id: u64, coin: Cw20Coin) { + let fund_sub_msg = to_json_binary(&ReceiveCw20Msg::Fund(FundMsg { id })).unwrap(); self.app .execute_contract( Addr::unchecked(OWNER), @@ -599,11 +651,8 @@ impl Suite { }); } - pub fn claim_rewards(&mut self, address: &str, denom: &str) { - let msg = ExecuteMsg::Claim { - denom: denom.to_string(), - }; - + pub fn claim_rewards(&mut self, address: &str, id: u64) { + let msg = ExecuteMsg::Claim { id }; self.app .execute_contract( Addr::unchecked(address), @@ -621,7 +670,6 @@ impl Suite { amount: Uint128::new(amount), msg: to_json_binary(&cw20_stake::msg::ReceiveMsg::Stake {}).unwrap(), }; - println!("[STAKING EVENT] {} staked {}", sender, amount); self.app .execute_contract(Addr::unchecked(sender), self.cw20_addr.clone(), &msg, &[]) .unwrap(); @@ -631,7 +679,6 @@ impl Suite { let msg = cw20_stake::msg::ExecuteMsg::Unstake { amount: Uint128::new(amount), }; - println!("[STAKING EVENT] {} unstaked {}", sender, amount); self.app .execute_contract( Addr::unchecked(sender), @@ -669,6 +716,136 @@ impl Suite { unstake_tokenfactory_tokens(self.app.borrow_mut(), &self.staking_addr, address, amount) } + pub fn update_emission_rate( + &mut self, + id: u64, + epoch_duration: Duration, + epoch_rewards: u128, + continuous: bool, + ) { + let msg: ExecuteMsg = ExecuteMsg::Update { + id, + emission_rate: Some(EmissionRate::Linear { + amount: Uint128::new(epoch_rewards), + duration: epoch_duration, + continuous, + }), + vp_contract: None, + hook_caller: None, + withdraw_destination: None, + }; + + let _resp = self + .app + .execute_contract( + Addr::unchecked(OWNER), + self.distribution_contract.clone(), + &msg, + &[], + ) + .unwrap(); + } + + pub fn set_immediate_emission(&mut self, id: u64) { + let msg: ExecuteMsg = ExecuteMsg::Update { + id, + emission_rate: Some(EmissionRate::Immediate {}), + vp_contract: None, + hook_caller: None, + withdraw_destination: None, + }; + + let _resp = self + .app + .execute_contract( + Addr::unchecked(OWNER), + self.distribution_contract.clone(), + &msg, + &[], + ) + .unwrap(); + } + + pub fn pause_emission(&mut self, id: u64) { + let msg: ExecuteMsg = ExecuteMsg::Update { + id, + emission_rate: Some(EmissionRate::Paused {}), + vp_contract: None, + hook_caller: None, + withdraw_destination: None, + }; + + let _resp = self + .app + .execute_contract( + Addr::unchecked(OWNER), + self.distribution_contract.clone(), + &msg, + &[], + ) + .unwrap(); + } + + pub fn update_vp_contract(&mut self, id: u64, vp_contract: &str) { + let msg: ExecuteMsg = ExecuteMsg::Update { + id, + emission_rate: None, + vp_contract: Some(vp_contract.to_string()), + hook_caller: None, + withdraw_destination: None, + }; + + let _resp = self + .app + .execute_contract( + Addr::unchecked(OWNER), + self.distribution_contract.clone(), + &msg, + &[], + ) + .unwrap(); + } + + pub fn update_hook_caller(&mut self, id: u64, hook_caller: &str) { + let msg: ExecuteMsg = ExecuteMsg::Update { + id, + emission_rate: None, + vp_contract: None, + hook_caller: Some(hook_caller.to_string()), + withdraw_destination: None, + }; + + let _resp = self + .app + .execute_contract( + Addr::unchecked(OWNER), + self.distribution_contract.clone(), + &msg, + &[], + ) + .unwrap(); + } + + pub fn update_withdraw_destination(&mut self, id: u64, withdraw_destination: &str) { + let msg: ExecuteMsg = ExecuteMsg::Update { + id, + emission_rate: None, + vp_contract: None, + hook_caller: None, + withdraw_destination: Some(withdraw_destination.to_string()), + }; + + let _resp = self + .app + .execute_contract( + Addr::unchecked(OWNER), + self.distribution_contract.clone(), + &msg, + &[], + ) + .unwrap(); + } + pub fn update_members(&mut self, add: Vec, remove: Vec) { let msg = cw4_group::msg::ExecuteMsg::UpdateMembers { remove, add }; @@ -689,7 +866,7 @@ impl Suite { }, ) .unwrap(); - println!("[UPDATE CW4] new members: {:?}", members); + // println!("[UPDATE CW4] new members: {:?}", members); members.members } diff --git a/contracts/distribution/dao-rewards-distributor/src/testing/tests.rs b/contracts/distribution/dao-rewards-distributor/src/testing/tests.rs index 764a04d3a..1e6620fa2 100644 --- a/contracts/distribution/dao-rewards-distributor/src/testing/tests.rs +++ b/contracts/distribution/dao-rewards-distributor/src/testing/tests.rs @@ -1,15 +1,23 @@ use std::borrow::BorrowMut; -use cosmwasm_std::Uint128; -use cosmwasm_std::{coin, to_json_binary, Addr, Timestamp}; +use cosmwasm_std::testing::{mock_dependencies, mock_env}; +use cosmwasm_std::{coin, coins, to_json_binary, Addr, Timestamp}; +use cosmwasm_std::{Uint128, Uint256}; +use cw2::ContractVersion; use cw20::{Cw20Coin, Expiration, UncheckedDenom}; use cw4::Member; use cw_multi_test::Executor; use cw_utils::Duration; +use dao_interface::voting::InfoResponse; +use crate::contract::{CONTRACT_NAME, CONTRACT_VERSION}; +use crate::msg::{CreateMsg, FundMsg, MigrateMsg}; +use crate::state::{EmissionRate, Epoch}; +use crate::testing::native_setup::setup_native_token_test; +use crate::ContractError; use crate::{ msg::ExecuteMsg, - testing::{ADDR1, ADDR2, ADDR3, DENOM}, + testing::{ADDR1, ADDR2, ADDR3, ADDR4, DENOM}, }; use super::{ @@ -20,6 +28,390 @@ use super::{ // By default, the tests are set up to distribute rewards over 1_000_000 units of time. // Over that time, 100_000_000 token rewards will be distributed. +#[test] +#[should_panic(expected = "Distribution not found with ID 3")] +fn test_fund_native_404() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + let mint_coin = coin(100, DENOM); + + suite.mint_native(mint_coin.clone(), OWNER); + suite.fund_native(3, mint_coin); +} + +#[test] +#[should_panic(expected = "Distribution not found with ID 3")] +fn test_fund_cw20_404() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::CW20) + .with_rewards_config(RewardsConfig { + amount: 1_000, + denom: UncheckedDenom::Cw20("irrelevant".to_string()), + duration: Duration::Height(10), + destination: None, + continuous: true, + }) + .build(); + + let mint_cw20 = Cw20Coin { + address: OWNER.to_string(), + amount: Uint128::new(100), + }; + + let address = suite.mint_cw20(mint_cw20.clone(), "newcoin").to_string(); + + suite.fund_cw20( + 3, + Cw20Coin { + address, + amount: mint_cw20.amount, + }, + ); +} + +#[test] +fn test_native_dao_rewards_update_reward_rate() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + suite.assert_amount(1_000); + suite.assert_ends_at(Expiration::AtHeight(1_000_000)); + suite.assert_duration(10); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); + + // ADDR1 claims rewards + suite.claim_rewards(ADDR1, 1); + suite.assert_pending_rewards(ADDR1, 1, 0); + + // set the rewards rate to half of the current one + // now there will be 5_000_000 tokens distributed over 100_000 blocks + suite.update_emission_rate(1, Duration::Height(10), 500, true); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 2_500_000); + suite.assert_pending_rewards(ADDR2, 1, 6_250_000); + suite.assert_pending_rewards(ADDR3, 1, 6_250_000); + + // double the rewards rate + // now there will be 10_000_000 tokens distributed over 100_000 blocks + suite.update_emission_rate(1, Duration::Height(10), 1_000, true); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 7_500_000); + suite.assert_pending_rewards(ADDR2, 1, 8_750_000); + suite.assert_pending_rewards(ADDR3, 1, 8_750_000); + + // skip 2/10ths of the time + suite.skip_blocks(200_000); + + suite.assert_pending_rewards(ADDR1, 1, 17_500_000); + suite.assert_pending_rewards(ADDR2, 1, 13_750_000); + suite.assert_pending_rewards(ADDR3, 1, 13_750_000); + + // pause the rewards distribution + suite.pause_emission(1); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // assert no pending rewards changed + suite.assert_pending_rewards(ADDR1, 1, 17_500_000); + suite.assert_pending_rewards(ADDR2, 1, 13_750_000); + suite.assert_pending_rewards(ADDR3, 1, 13_750_000); + + // assert ADDR1 pre-claim balance + suite.assert_native_balance(ADDR1, DENOM, 10_000_000); + // ADDR1 claims their rewards + suite.claim_rewards(ADDR1, 1); + // assert ADDR1 post-claim balance to be pre-claim + pending + suite.assert_native_balance(ADDR1, DENOM, 10_000_000 + 17_500_000); + // assert ADDR1 is now entitled to 0 pending rewards + suite.assert_pending_rewards(ADDR1, 1, 0); + + // user 2 unstakes their stake + suite.unstake_native_tokens(ADDR2, 50); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // only the ADDR1 pending rewards should have changed + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 13_750_000); + suite.assert_pending_rewards(ADDR3, 1, 13_750_000); + + // ADDR2 claims their rewards (has 50 to begin with as they unstaked) + suite.assert_native_balance(ADDR2, DENOM, 50); + suite.claim_rewards(ADDR2, 1); + // assert ADDR2 post-claim balance to be pre-claim + pending and has 0 pending rewards + suite.assert_native_balance(ADDR2, DENOM, 13_750_000 + 50); + suite.assert_pending_rewards(ADDR2, 1, 0); + + // update the reward rate back to 1_000 / 10blocks + // this should now distribute 10_000_000 tokens over 100_000 blocks + // between ADDR1 (2/3rds) and ADDR3 (1/3rd) + suite.update_emission_rate(1, Duration::Height(10), 1000, true); + + // update with the same rate does nothing + suite.update_emission_rate(1, Duration::Height(10), 1000, true); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // assert that rewards are being distributed at the expected rate + suite.assert_pending_rewards(ADDR1, 1, 6_666_666); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 13_750_000 + 3_333_333); + + // ADDR3 claims their rewards + suite.assert_native_balance(ADDR3, DENOM, 0); + suite.claim_rewards(ADDR3, 1); + suite.assert_pending_rewards(ADDR3, 1, 0); + suite.assert_native_balance(ADDR3, DENOM, 13_750_000 + 3_333_333); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 6_666_666 + 6_666_666 + 1); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 3_333_333); + + // claim everything so that there are 0 pending rewards + suite.claim_rewards(ADDR3, 1); + suite.claim_rewards(ADDR1, 1); + + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 0); + + // update the rewards rate to 40_000_000 per 100_000 blocks. + // split is still 2/3rds to ADDR1 and 1/3rd to ADDR3 + suite.update_emission_rate(1, Duration::Height(10), 4000, true); + suite.assert_ends_at(Expiration::AtHeight(1_062_500)); + + suite.skip_blocks(50_000); // allocates 20_000_000 tokens + + let addr1_pending = 20_000_000 * 2 / 3; + let addr3_pending = 20_000_000 / 3; + suite.assert_pending_rewards(ADDR1, 1, addr1_pending); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, addr3_pending); + + // ADDR2 wakes up to the increased staking rate and stakes 50 tokens + // this brings new split to: [ADDR1: 50%, ADDR2: 25%, ADDR3: 25%] + suite.stake_native_tokens(ADDR2, 50); + + suite.skip_blocks(10_000); // allocates 4_000_000 tokens + + suite.assert_pending_rewards(ADDR1, 1, addr1_pending + 4_000_000 * 2 / 4); + suite.assert_pending_rewards(ADDR2, 1, 4_000_000 / 4); + suite.assert_pending_rewards(ADDR3, 1, addr3_pending + 4_000_000 / 4); + + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR3, 1); + let addr1_pending = 0; + let addr3_pending = 0; + suite.skip_blocks(10_000); // skips from 1,060,000 to 1,070,000, and the end is 1,062,500, so this allocates only 1_000_000 tokens instead of 4_000_000 + + suite.assert_pending_rewards(ADDR1, 1, addr1_pending + 1_000_000 * 2 / 4); + suite.assert_pending_rewards(ADDR2, 1, 4_000_000 / 4 + 1_000_000 / 4); + suite.assert_pending_rewards(ADDR3, 1, addr3_pending + 1_000_000 / 4); + + suite.claim_rewards(ADDR2, 1); + + // TODO: there's a few denoms remaining here, ensure such cases are handled properly + let remaining_rewards = suite.get_balance_native(suite.distribution_contract.clone(), DENOM); + println!("Remaining rewards: {}", remaining_rewards); +} + +#[test] +fn test_native_dao_rewards_reward_rate_switch_unit() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) + .with_rewards_config(RewardsConfig { + amount: 1_000, + denom: UncheckedDenom::Native(DENOM.to_string()), + duration: Duration::Height(10), + destination: None, + continuous: true, + }) + .build(); + + suite.assert_amount(1_000); + suite.assert_ends_at(Expiration::AtHeight(1_000_000)); + suite.assert_duration(10); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); + + // ADDR1 claims rewards + suite.claim_rewards(ADDR1, 1); + suite.assert_pending_rewards(ADDR1, 1, 0); + + // set the rewards rate to time-based rewards + suite.update_emission_rate(1, Duration::Time(10), 500, true); + + // skip 1/10th of the time + suite.skip_seconds(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 2_500_000); + suite.assert_pending_rewards(ADDR2, 1, 6_250_000); + suite.assert_pending_rewards(ADDR3, 1, 6_250_000); + + // double the rewards rate + // now there will be 10_000_000 tokens distributed over 100_000 seconds + suite.update_emission_rate(1, Duration::Time(10), 1_000, true); + + // skip 1/10th of the time + suite.skip_seconds(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 7_500_000); + suite.assert_pending_rewards(ADDR2, 1, 8_750_000); + suite.assert_pending_rewards(ADDR3, 1, 8_750_000); + + // skip 2/10ths of the time + suite.skip_seconds(200_000); + + suite.assert_pending_rewards(ADDR1, 1, 17_500_000); + suite.assert_pending_rewards(ADDR2, 1, 13_750_000); + suite.assert_pending_rewards(ADDR3, 1, 13_750_000); + + // pause the rewards distribution + suite.pause_emission(1); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // assert no pending rewards changed + suite.assert_pending_rewards(ADDR1, 1, 17_500_000); + suite.assert_pending_rewards(ADDR2, 1, 13_750_000); + suite.assert_pending_rewards(ADDR3, 1, 13_750_000); + + // assert ADDR1 pre-claim balance + suite.assert_native_balance(ADDR1, DENOM, 10_000_000); + // ADDR1 claims their rewards + suite.claim_rewards(ADDR1, 1); + // assert ADDR1 post-claim balance to be pre-claim + pending + suite.assert_native_balance(ADDR1, DENOM, 10_000_000 + 17_500_000); + // assert ADDR1 is now entitled to 0 pending rewards + suite.assert_pending_rewards(ADDR1, 1, 0); + + // user 2 unstakes their stake + suite.unstake_native_tokens(ADDR2, 50); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // only the ADDR1 pending rewards should have changed + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 13_750_000); + suite.assert_pending_rewards(ADDR3, 1, 13_750_000); + + // ADDR2 claims their rewards (has 50 to begin with as they unstaked) + suite.assert_native_balance(ADDR2, DENOM, 50); + suite.claim_rewards(ADDR2, 1); + // assert ADDR2 post-claim balance to be pre-claim + pending and has 0 pending rewards + suite.assert_native_balance(ADDR2, DENOM, 13_750_000 + 50); + suite.assert_pending_rewards(ADDR2, 1, 0); + + // update the reward rate back to 1_000 / 10blocks + // this should now distribute 10_000_000 tokens over 100_000 blocks + // between ADDR1 (2/3rds) and ADDR3 (1/3rd) + suite.update_emission_rate(1, Duration::Height(10), 1000, true); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // assert that rewards are being distributed at the expected rate + suite.assert_pending_rewards(ADDR1, 1, 6_666_666); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 13_750_000 + 3_333_333); + + // ADDR3 claims their rewards + suite.assert_native_balance(ADDR3, DENOM, 0); + suite.claim_rewards(ADDR3, 1); + suite.assert_pending_rewards(ADDR3, 1, 0); + suite.assert_native_balance(ADDR3, DENOM, 13_750_000 + 3_333_333); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 6_666_666 + 6_666_666 + 1); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 3_333_333); + + // claim everything so that there are 0 pending rewards + suite.claim_rewards(ADDR3, 1); + suite.claim_rewards(ADDR1, 1); + + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 0); + + // update the rewards rate to 40_000_000 per 100_000 seconds. + // split is still 2/3rds to ADDR1 and 1/3rd to ADDR3 + suite.update_emission_rate(1, Duration::Time(10), 4000, true); + suite.assert_ends_at(Expiration::AtTime(Timestamp::from_seconds(462_500))); + + suite.skip_seconds(50_000); // allocates 20_000_000 tokens + + let addr1_pending = 20_000_000 * 2 / 3; + let addr3_pending = 20_000_000 / 3; + suite.assert_pending_rewards(ADDR1, 1, addr1_pending); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, addr3_pending); + + // ADDR2 wakes up to the increased staking rate and stakes 50 tokens + // this brings new split to: [ADDR1: 50%, ADDR2: 25%, ADDR3: 25%] + suite.stake_native_tokens(ADDR2, 50); + + suite.skip_seconds(10_000); // allocates 4_000_000 tokens + + suite.assert_pending_rewards(ADDR1, 1, addr1_pending + 4_000_000 * 2 / 4); + suite.assert_pending_rewards(ADDR2, 1, 4_000_000 / 4); + suite.assert_pending_rewards(ADDR3, 1, addr3_pending + 4_000_000 / 4); + + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR3, 1); + let addr1_pending = 0; + let addr3_pending = 0; + suite.skip_seconds(10_000); // skips from 460,000 to 470,000, and the end is 462,500, so this allocates only 1_000_000 tokens instead of 4_000_000 + + suite.assert_pending_rewards(ADDR1, 1, addr1_pending + 1_000_000 * 2 / 4); + suite.assert_pending_rewards(ADDR2, 1, 4_000_000 / 4 + 1_000_000 / 4); + suite.assert_pending_rewards(ADDR3, 1, addr3_pending + 1_000_000 / 4); + + suite.claim_rewards(ADDR2, 1); + + // TODO: there's a few denoms remaining here, ensure such cases are handled properly + let remaining_rewards = suite.get_balance_native(suite.distribution_contract.clone(), DENOM); + println!("Remaining rewards: {}", remaining_rewards); +} + #[test] fn test_cw20_dao_native_rewards_block_height_based() { let mut suite = SuiteBuilder::base(super::suite::DaoType::CW20).build(); @@ -31,21 +423,21 @@ fn test_cw20_dao_native_rewards_block_height_based() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR1 claims rewards - suite.claim_rewards(ADDR1, DENOM); + suite.claim_rewards(ADDR1, 1); suite.assert_native_balance(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR1, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); // ADDR2 and ADDR3 unstake their rewards suite.unstake_cw20_tokens(50, ADDR2); @@ -56,13 +448,13 @@ fn test_cw20_dao_native_rewards_block_height_based() { // because ADDR2 and ADDR3 are not staking, ADDR1 receives all the rewards. // ADDR2 and ADDR3 should have the same amount of pending rewards as before. - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR2 and ADDR3 wake up, claim and restake their rewards - suite.claim_rewards(ADDR2, DENOM); - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); suite.stake_cw20_tokens(50, ADDR2); @@ -71,36 +463,36 @@ fn test_cw20_dao_native_rewards_block_height_based() { suite.stake_cw20_tokens(50, ADDR3); - suite.assert_pending_rewards(ADDR1, DENOM, 30_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 30_000_000); + suite.assert_pending_rewards(ADDR2, 1, 10_000_000); + suite.assert_pending_rewards(ADDR3, 1, 0); - suite.claim_rewards(ADDR1, DENOM); - suite.claim_rewards(ADDR2, DENOM); + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); - suite.assert_pending_rewards(ADDR1, DENOM, 0); - suite.assert_pending_rewards(ADDR2, DENOM, 0); - suite.assert_pending_rewards(ADDR3, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 0); let remaining_time = suite.get_time_until_rewards_expiration(); suite.skip_blocks(remaining_time - 100_000); - suite.claim_rewards(ADDR1, DENOM); + suite.claim_rewards(ADDR1, 1); suite.unstake_cw20_tokens(100, ADDR1); - suite.assert_pending_rewards(ADDR1, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); suite.skip_blocks(100_000); suite.unstake_cw20_tokens(50, ADDR2); suite.skip_blocks(100_000); - suite.claim_rewards(ADDR2, DENOM); - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); - suite.assert_pending_rewards(ADDR1, DENOM, 0); - suite.assert_pending_rewards(ADDR2, DENOM, 0); - suite.assert_pending_rewards(ADDR3, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 0); let addr1_bal = suite.get_balance_native(ADDR1, DENOM); let addr2_bal = suite.get_balance_native(ADDR2, DENOM); @@ -120,21 +512,21 @@ fn test_cw721_dao_rewards() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR1 claims rewards - suite.claim_rewards(ADDR1, DENOM); + suite.claim_rewards(ADDR1, 1); suite.assert_native_balance(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR1, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); // ADDR2 and ADDR3 unstake their nfts suite.unstake_nft(ADDR2, 3); @@ -145,13 +537,13 @@ fn test_cw721_dao_rewards() { // because ADDR2 and ADDR3 are not staking, ADDR1 receives all the rewards. // ADDR2 and ADDR3 should have the same amount of pending rewards as before. - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR2 and ADDR3 wake up, claim and restake their nfts - suite.claim_rewards(ADDR2, DENOM); - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); suite.stake_nft(ADDR2, 3); suite.stake_nft(ADDR3, 4); @@ -165,13 +557,13 @@ fn test_claim_zero_rewards() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); // ADDR1 claims rewards - suite.claim_rewards(ADDR1, DENOM); + suite.claim_rewards(ADDR1, 1); // ADDR1 attempts to claim again - suite.claim_rewards(ADDR1, DENOM); + suite.claim_rewards(ADDR1, 1); } #[test] @@ -184,9 +576,12 @@ fn test_native_dao_cw20_rewards_time_based() { denom: UncheckedDenom::Cw20(DENOM.to_string()), duration: Duration::Time(10), destination: None, + continuous: true, }) .build(); + let cw20_denom = &suite.reward_denom.clone(); + suite.assert_amount(1_000); suite.assert_duration(10); suite.assert_ends_at(Expiration::AtTime(Timestamp::from_seconds(1_000_000))); @@ -194,21 +589,21 @@ fn test_native_dao_cw20_rewards_time_based() { // skip 1/10th of the time suite.skip_seconds(100_000); - // suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + // suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // skip 1/10th of the time suite.skip_seconds(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR1 claims rewards - suite.claim_rewards(ADDR1, suite.reward_denom.clone().as_str()); - suite.assert_cw20_balance(ADDR1, 10_000_000); - suite.assert_pending_rewards(ADDR1, DENOM, 0); + suite.claim_rewards(ADDR1, 1); + suite.assert_cw20_balance(cw20_denom, ADDR1, 10_000_000); + suite.assert_pending_rewards(ADDR1, 1, 0); // ADDR2 and ADDR3 unstake their stake suite.unstake_cw20_tokens(50, ADDR2); @@ -219,16 +614,16 @@ fn test_native_dao_cw20_rewards_time_based() { // because ADDR2 and ADDR3 are not staking, ADDR1 receives all the rewards. // ADDR2 and ADDR3 should have the same amount of pending rewards as before. - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR2 and ADDR3 wake up and claim their rewards - suite.claim_rewards(ADDR2, suite.reward_denom.clone().as_str()); - suite.claim_rewards(ADDR3, suite.reward_denom.clone().as_str()); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); - suite.assert_cw20_balance(ADDR1, 10_000_000); - suite.assert_cw20_balance(ADDR2, 5_000_000); + suite.assert_cw20_balance(cw20_denom, ADDR1, 10_000_000); + suite.assert_cw20_balance(cw20_denom, ADDR2, 5_000_000); } #[test] @@ -241,6 +636,7 @@ fn test_native_dao_rewards_time_based() { denom: UncheckedDenom::Native(DENOM.to_string()), duration: Duration::Time(10), destination: None, + continuous: true, }) .build(); @@ -251,21 +647,21 @@ fn test_native_dao_rewards_time_based() { // skip 1/10th of the time suite.skip_seconds(100_000); - // suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + // suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // skip 1/10th of the time suite.skip_seconds(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR1 claims rewards - suite.claim_rewards(ADDR1, DENOM); + suite.claim_rewards(ADDR1, 1); suite.assert_native_balance(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR1, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); // ADDR2 and ADDR3 unstake their stake suite.unstake_native_tokens(ADDR2, 50); @@ -276,13 +672,13 @@ fn test_native_dao_rewards_time_based() { // because ADDR2 and ADDR3 are not staking, ADDR1 receives all the rewards. // ADDR2 and ADDR3 should have the same amount of pending rewards as before. - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR2 and ADDR3 wake up, claim and restake their rewards - suite.claim_rewards(ADDR2, DENOM); - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); let addr1_balance = suite.get_balance_native(ADDR1, DENOM); let addr2_balance = suite.get_balance_native(ADDR2, DENOM); @@ -291,6 +687,344 @@ fn test_native_dao_rewards_time_based() { suite.stake_native_tokens(ADDR2, addr2_balance); } +// all of the `+1` corrections highlight rounding +#[test] +fn test_native_dao_rewards_time_based_with_rounding() { + // 100udenom/100sec = 1udenom/1sec reward emission rate + // given funding of 100_000_000udenom, we have a reward duration of 100_000_000sec + let mut suite = SuiteBuilder::base(super::suite::DaoType::CW4) + .with_rewards_config(RewardsConfig { + amount: 100, + denom: UncheckedDenom::Native(DENOM.to_string()), + duration: Duration::Time(100), + destination: None, + continuous: true, + }) + .with_cw4_members(vec![ + Member { + addr: ADDR1.to_string(), + weight: 140, + }, + Member { + addr: ADDR2.to_string(), + weight: 40, + }, + Member { + addr: ADDR3.to_string(), + weight: 20, + }, + ]) + .build(); + + suite.assert_amount(100); + suite.assert_duration(100); + suite.assert_ends_at(Expiration::AtTime(Timestamp::from_seconds(100_000_000))); + + // skip 1 interval + suite.skip_seconds(100); + + suite.assert_pending_rewards(ADDR1, 1, 70); + suite.assert_pending_rewards(ADDR2, 1, 20); + suite.assert_pending_rewards(ADDR3, 1, 10); + + // change voting power of one of the members and claim + suite.update_members( + vec![Member { + addr: ADDR2.to_string(), + weight: 60, + }], + vec![], + ); + suite.claim_rewards(ADDR2, 1); + suite.assert_native_balance(ADDR2, DENOM, 20); + suite.assert_pending_rewards(ADDR2, 1, 0); + + // skip 1 interval + suite.skip_seconds(100); + + suite.assert_pending_rewards(ADDR1, 1, 70 + 63); + suite.assert_pending_rewards(ADDR2, 1, 27); + suite.assert_pending_rewards(ADDR3, 1, 10 + 9); + + // increase reward rate and claim + suite.update_emission_rate(1, Duration::Time(100), 150, true); + suite.claim_rewards(ADDR3, 1); + suite.assert_native_balance(ADDR3, DENOM, 10 + 9); + suite.assert_pending_rewards(ADDR3, 1, 0); + + // skip 1 interval + suite.skip_seconds(100); + + suite.assert_pending_rewards(ADDR1, 1, 70 + 63 + 95 + 1); + suite.assert_pending_rewards(ADDR2, 1, 27 + 40 + 1); + suite.assert_pending_rewards(ADDR3, 1, 13); + + // claim rewards + suite.claim_rewards(ADDR1, 1); + suite.assert_native_balance(ADDR1, DENOM, 70 + 63 + 95 + 1); + suite.assert_pending_rewards(ADDR1, 1, 0); + + // skip 3 intervals + suite.skip_seconds(300); + + suite.assert_pending_rewards(ADDR1, 1, 3 * 95 + 1); + suite.assert_pending_rewards(ADDR2, 1, 27 + 4 * 40 + 1 + 1 + 1); + suite.assert_pending_rewards(ADDR3, 1, 4 * 13 + 1 + 1); + + // change voting power for all + suite.update_members( + vec![ + Member { + addr: ADDR1.to_string(), + weight: 100, + }, + Member { + addr: ADDR2.to_string(), + weight: 80, + }, + Member { + addr: ADDR3.to_string(), + weight: 40, + }, + ], + vec![], + ); + + suite.claim_rewards(ADDR2, 1); + suite.assert_native_balance(ADDR2, DENOM, 20 + 27 + 4 * 40 + 1 + 1 + 1); + suite.assert_pending_rewards(ADDR2, 1, 0); + + // skip 1 interval + suite.skip_seconds(100); + + suite.assert_pending_rewards(ADDR1, 1, 3 * 95 + 1 + 68); + suite.assert_pending_rewards(ADDR2, 1, 54); + suite.assert_pending_rewards(ADDR3, 1, 4 * 13 + 1 + 1 + 27); + + // claim all + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); + suite.assert_native_balance(ADDR1, DENOM, 70 + 63 + 95 + 1 + 3 * 95 + 1 + 68); + suite.assert_native_balance(ADDR2, DENOM, 20 + 27 + 4 * 40 + 1 + 1 + 1 + 54); + suite.assert_native_balance(ADDR3, DENOM, 10 + 9 + 4 * 13 + 1 + 1 + 27); + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 0); + + // TODO: fix this rug of 3 udenom by the distribution contract + suite.assert_native_balance( + suite.distribution_contract.as_str(), + DENOM, + 100_000_000 - (100 * 2 + 150 * 5) + 3, + ); +} + +#[test] +fn test_immediate_emission() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + // skip 2 blocks since the contract depends on the previous block's total + // voting power, and voting power takes 1 block to take effect. so if voting + // power is staked on block 0, it takes effect on block 1, so immediate + // distribution is only effective on block 2. + suite.skip_blocks(2); + + suite.mint_native(coin(500_000_000, ALT_DENOM), OWNER); + + let execute_create_msg = ExecuteMsg::Create(CreateMsg { + denom: cw20::UncheckedDenom::Native(ALT_DENOM.to_string()), + emission_rate: EmissionRate::Immediate {}, + hook_caller: suite.staking_addr.to_string(), + vp_contract: suite.voting_power_addr.to_string(), + withdraw_destination: None, + }); + + // create distribution + suite + .app + .execute_contract( + Addr::unchecked(OWNER), + suite.distribution_contract.clone(), + &execute_create_msg, + &coins(100_000_000, ALT_DENOM), + ) + .unwrap(); + + // users immediately have access to rewards + suite.assert_pending_rewards(ADDR1, 2, 50_000_000); + suite.assert_pending_rewards(ADDR2, 2, 25_000_000); + suite.assert_pending_rewards(ADDR3, 2, 25_000_000); + + // another fund immediately adds to the pending rewards + suite.fund_native(2, coin(100_000_000, ALT_DENOM)); + + // users immediately have access to new rewards + suite.assert_pending_rewards(ADDR1, 2, 2 * 50_000_000); + suite.assert_pending_rewards(ADDR2, 2, 2 * 25_000_000); + suite.assert_pending_rewards(ADDR3, 2, 2 * 25_000_000); + + // a new user stakes tokens + suite.mint_native(coin(200, DENOM), ADDR4); + suite.stake_native_tokens(ADDR4, 200); + + // skip 2 blocks so stake takes effect + suite.skip_blocks(2); + + // another fund takes into account new voting power + suite.fund_native(2, coin(100_000_000, ALT_DENOM)); + + suite.assert_pending_rewards(ADDR1, 2, 2 * 50_000_000 + 25_000_000); + suite.assert_pending_rewards(ADDR2, 2, 2 * 25_000_000 + 12_500_000); + suite.assert_pending_rewards(ADDR3, 2, 2 * 25_000_000 + 12_500_000); + suite.assert_pending_rewards(ADDR4, 2, 50_000_000); + + suite.claim_rewards(ADDR1, 2); + suite.claim_rewards(ADDR2, 2); + suite.claim_rewards(ADDR3, 2); + suite.claim_rewards(ADDR4, 2); + + suite.unstake_native_tokens(ADDR1, 100); + suite.unstake_native_tokens(ADDR2, 50); + suite.unstake_native_tokens(ADDR3, 50); + + // skip 2 blocks so stake takes effect + suite.skip_blocks(2); + + // another fund takes into account new voting power + suite.fund_native(2, coin(100_000_000, ALT_DENOM)); + + suite.assert_pending_rewards(ADDR1, 2, 0); + suite.assert_pending_rewards(ADDR2, 2, 0); + suite.assert_pending_rewards(ADDR3, 2, 0); + suite.assert_pending_rewards(ADDR4, 2, 100_000_000); +} + +#[test] +#[should_panic( + expected = "There is no voting power registered, so no one will receive these funds" +)] +fn test_immediate_emission_fails_if_no_voting_power() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + // all users unstake + suite.unstake_native_tokens(ADDR1, 100); + suite.unstake_native_tokens(ADDR2, 50); + suite.unstake_native_tokens(ADDR3, 50); + + // skip 2 blocks since the contract depends on the previous block's total + // voting power, and voting power takes 1 block to take effect. so if voting + // power is staked on block 0, it takes effect on block 1, so immediate + // distribution is only effective on block 2. + suite.skip_blocks(2); + + suite.mint_native(coin(200_000_000, ALT_DENOM), OWNER); + + let execute_create_msg = ExecuteMsg::Create(CreateMsg { + denom: cw20::UncheckedDenom::Native(ALT_DENOM.to_string()), + emission_rate: EmissionRate::Immediate {}, + hook_caller: suite.staking_addr.to_string(), + vp_contract: suite.voting_power_addr.to_string(), + withdraw_destination: None, + }); + + // create and fund distribution + suite + .app + .execute_contract( + Addr::unchecked(OWNER), + suite.distribution_contract.clone(), + &execute_create_msg, + &coins(100_000_000, ALT_DENOM), + ) + .unwrap(); +} + +#[test] +fn test_transition_to_immediate() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + suite.assert_amount(1_000); + suite.assert_ends_at(Expiration::AtHeight(1_000_000)); + suite.assert_duration(10); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); + + // ADDR1 claims rewards + suite.claim_rewards(ADDR1, 1); + suite.assert_native_balance(ADDR1, DENOM, 10_000_000); + suite.assert_pending_rewards(ADDR1, 1, 0); + + // ADDR2 unstakes their stake + suite.unstake_native_tokens(ADDR2, 50); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // because ADDR2 is not staking, ADDR1 and ADDR3 receive the rewards. ADDR2 + // should have the same amount of pending rewards as before. + suite.assert_pending_rewards(ADDR1, 1, 6_666_666); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000 + 3_333_333); + + // ADDR2 claims their rewards + suite.claim_rewards(ADDR2, 1); + suite.assert_pending_rewards(ADDR2, 1, 0); + + // switching to immediate emission instantly distributes the remaining 70M + suite.set_immediate_emission(1); + + // ADDR1 and ADDR3 split the rewards, and ADDR2 gets none + suite.assert_pending_rewards(ADDR1, 1, 6_666_666 + 46_666_666 + 1); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000 + 3_333_333 + 23_333_333); + + // claim all rewards + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR3, 1); + + // ADDR3 unstakes their stake, leaving only ADDR1 staked + suite.unstake_native_tokens(ADDR3, 50); + + // skip 2 blocks so unstake takes effect + suite.skip_blocks(2); + + // another fund immediately adds to the pending rewards + suite.mint_native(coin(100_000_000, DENOM), OWNER); + suite.fund_native(1, coin(100_000_000, DENOM)); + + // ADDR1 gets all + suite.assert_pending_rewards(ADDR1, 1, 100_000_000); + + // change back to linear emission + suite.update_emission_rate(1, Duration::Height(10), 1000, true); + + // fund with 100M again + suite.mint_native(coin(100_000_000, DENOM), OWNER); + suite.fund_native(1, coin(100_000_000, DENOM)); + + // ADDR1 has same pending as before + suite.assert_pending_rewards(ADDR1, 1, 100_000_000); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // ADDR1 has new linearly distributed rewards + suite.assert_pending_rewards(ADDR1, 1, 100_000_000 + 10_000_000); +} + #[test] fn test_native_dao_rewards() { let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); @@ -302,21 +1036,21 @@ fn test_native_dao_rewards() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR1 claims rewards - suite.claim_rewards(ADDR1, DENOM); + suite.claim_rewards(ADDR1, 1); suite.assert_native_balance(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR1, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); // ADDR2 and ADDR3 unstake their stake suite.unstake_native_tokens(ADDR2, 50); @@ -327,13 +1061,13 @@ fn test_native_dao_rewards() { // because ADDR2 and ADDR3 are not staking, ADDR1 receives all the rewards. // ADDR2 and ADDR3 should have the same amount of pending rewards as before. - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_000_000); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 5_000_000); + suite.assert_pending_rewards(ADDR3, 1, 5_000_000); // ADDR2 and ADDR3 wake up, claim and restake their rewards - suite.claim_rewards(ADDR2, DENOM); - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); let addr1_balance = suite.get_balance_native(ADDR1, DENOM); let addr2_balance = suite.get_balance_native(ADDR2, DENOM); @@ -342,6 +1076,52 @@ fn test_native_dao_rewards() { suite.stake_native_tokens(ADDR2, addr2_balance); } +#[test] +fn test_continuous_backfill_latest_voting_power() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + suite.assert_amount(1_000); + suite.assert_ends_at(Expiration::AtHeight(1_000_000)); + suite.assert_duration(10); + + // skip all of the time + suite.skip_blocks(1_000_000); + + suite.assert_pending_rewards(ADDR1, 1, 50_000_000); + suite.assert_pending_rewards(ADDR2, 1, 25_000_000); + suite.assert_pending_rewards(ADDR3, 1, 25_000_000); + + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // change voting powers (1 = 200, 2 = 50, 3 = 50) + suite.stake_native_tokens(ADDR1, 100); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // change voting powers again (1 = 50, 2 = 100, 3 = 100) + suite.unstake_native_tokens(ADDR1, 150); + suite.stake_native_tokens(ADDR2, 50); + suite.stake_native_tokens(ADDR3, 50); + + // skip 1/10th of the time + suite.skip_blocks(100_000); + + // fund with 100M + suite.fund_native(1, coin(100_000_000, DENOM)); + + // since this is continuous, rewards should backfill based on the latest + // voting powers. we skipped 30% of the time, so 30M should be distributed + suite.assert_pending_rewards(ADDR1, 1, 6_000_000); + suite.assert_pending_rewards(ADDR2, 1, 12_000_000); + suite.assert_pending_rewards(ADDR3, 1, 12_000_000); +} + #[test] fn test_cw4_dao_rewards() { let mut suite = SuiteBuilder::base(super::suite::DaoType::CW4).build(); @@ -353,9 +1133,9 @@ fn test_cw4_dao_rewards() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // remove the second member suite.update_members(vec![], vec![ADDR2.to_string()]); @@ -365,9 +1145,9 @@ fn test_cw4_dao_rewards() { suite.skip_blocks(100_000); // now that ADDR2 is no longer a member, ADDR1 and ADDR3 will split the rewards - suite.assert_pending_rewards(ADDR1, DENOM, 11_666_666); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_833_333); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000 + 6_666_666); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 3_333_333 + 2_500_000); // reintroduce the 2nd member with double the vp let add_member_2 = Member { @@ -381,27 +1161,27 @@ fn test_cw4_dao_rewards() { // meaning the token reward per 100k blocks is 4mil, 4mil, 2mil // ADDR1 claims rewards - suite.claim_rewards(ADDR1, DENOM); - suite.assert_native_balance(ADDR1, DENOM, 11_666_666); + suite.claim_rewards(ADDR1, 1); + suite.assert_native_balance(ADDR1, DENOM, 5_000_000 + 6_666_666); // assert pending rewards are still the same (other than ADDR1) - suite.assert_pending_rewards(ADDR1, DENOM, 0); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 5_833_333); + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 3_333_333 + 2_500_000); // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 4_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 6_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 7_833_333); + suite.assert_pending_rewards(ADDR1, 1, 4_000_000); + suite.assert_pending_rewards(ADDR2, 1, 6_500_000); + suite.assert_pending_rewards(ADDR3, 1, 7_833_333); // skip 1/2 of time, leaving 200k blocks left suite.skip_blocks(500_000); - suite.assert_pending_rewards(ADDR1, DENOM, 24_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 26_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 17_833_333); + suite.assert_pending_rewards(ADDR1, 1, 24_000_000); + suite.assert_pending_rewards(ADDR2, 1, 26_500_000); + suite.assert_pending_rewards(ADDR3, 1, 17_833_333); // remove all members suite.update_members( @@ -409,16 +1189,16 @@ fn test_cw4_dao_rewards() { vec![ADDR1.to_string(), ADDR2.to_string(), ADDR3.to_string()], ); - suite.assert_pending_rewards(ADDR1, DENOM, 24_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 26_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 17_833_333); + suite.assert_pending_rewards(ADDR1, 1, 24_000_000); + suite.assert_pending_rewards(ADDR2, 1, 26_500_000); + suite.assert_pending_rewards(ADDR3, 1, 17_833_333); // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 24_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 26_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 17_833_333); + suite.assert_pending_rewards(ADDR1, 1, 24_000_000); + suite.assert_pending_rewards(ADDR2, 1, 26_500_000); + suite.assert_pending_rewards(ADDR3, 1, 17_833_333); suite.update_members( vec![ @@ -438,34 +1218,34 @@ fn test_cw4_dao_rewards() { vec![], ); - suite.assert_pending_rewards(ADDR1, DENOM, 24_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 26_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 17_833_333); + suite.assert_pending_rewards(ADDR1, 1, 24_000_000); + suite.assert_pending_rewards(ADDR2, 1, 26_500_000); + suite.assert_pending_rewards(ADDR3, 1, 17_833_333); - suite.claim_rewards(ADDR1, DENOM); - suite.assert_pending_rewards(ADDR1, DENOM, 0); + suite.claim_rewards(ADDR1, 1); + suite.assert_pending_rewards(ADDR1, 1, 0); suite.assert_native_balance(ADDR1, DENOM, 35_666_666); // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 4_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 30_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 19_833_333); + suite.assert_pending_rewards(ADDR1, 1, 4_000_000); + suite.assert_pending_rewards(ADDR2, 1, 30_500_000); + suite.assert_pending_rewards(ADDR3, 1, 19_833_333); // at the very expiration block, claim rewards - suite.claim_rewards(ADDR2, DENOM); - suite.assert_pending_rewards(ADDR2, DENOM, 0); + suite.claim_rewards(ADDR2, 1); + suite.assert_pending_rewards(ADDR2, 1, 0); suite.assert_native_balance(ADDR2, DENOM, 30_500_000); suite.skip_blocks(100_000); - suite.claim_rewards(ADDR1, DENOM); - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR3, 1); - suite.assert_pending_rewards(ADDR1, DENOM, 0); - suite.assert_pending_rewards(ADDR2, DENOM, 0); - suite.assert_pending_rewards(ADDR3, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 0); let contract = suite.distribution_contract.clone(); @@ -481,17 +1261,19 @@ fn test_fund_multiple_denoms() { let alt_coin = coin(100_000_000, ALT_DENOM); let coin = coin(100_000_000, DENOM); - suite.mint_native_coin(alt_coin.clone(), OWNER); - suite.mint_native_coin(coin.clone(), OWNER); + suite.mint_native(alt_coin.clone(), OWNER); + suite.mint_native(coin.clone(), OWNER); let hook_caller = suite.staking_addr.to_string(); - suite.register_reward_denom( + suite.create( RewardsConfig { amount: 1000, denom: cw20::UncheckedDenom::Native(ALT_DENOM.to_string()), duration: Duration::Height(100), destination: None, + continuous: true, }, &hook_caller, + None, ); suite @@ -500,12 +1282,41 @@ fn test_fund_multiple_denoms() { .execute_contract( Addr::unchecked(OWNER), suite.distribution_contract.clone(), - &ExecuteMsg::Fund {}, + &ExecuteMsg::Fund(FundMsg { id: 2 }), &[coin, alt_coin], ) .unwrap(); } +#[test] +#[should_panic(expected = "Invalid CW20")] +fn test_fund_cw20_wrong_denom() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) + .with_rewards_config(RewardsConfig { + amount: 1_000, + denom: UncheckedDenom::Cw20("irrelevant".to_string()), + duration: Duration::Height(10), + destination: None, + continuous: true, + }) + .build(); + + let mint_cw20 = Cw20Coin { + address: OWNER.to_string(), + amount: Uint128::new(100), + }; + + let address = suite.mint_cw20(mint_cw20.clone(), "newcoin").to_string(); + + suite.fund_cw20( + 1, + Cw20Coin { + address, + amount: mint_cw20.amount, + }, + ); +} + #[test] #[should_panic(expected = "unknown variant `not_the_fund: {}`")] fn test_fund_cw20_with_invalid_cw20_receive_msg() { @@ -517,8 +1328,7 @@ fn test_fund_cw20_with_invalid_cw20_receive_msg() { amount: Uint128::new(1_000_000), }; - let new_cw20_mint = suite.mint_cw20_coin(unregistered_cw20_coin.clone(), ADDR1, "newcoin"); - println!("[FUNDING EVENT] cw20 funding: {}", unregistered_cw20_coin); + let new_cw20_mint = suite.mint_cw20(unregistered_cw20_coin.clone(), "newcoin"); let fund_sub_msg = to_json_binary(&"not_the_fund: {}").unwrap(); suite @@ -547,23 +1357,22 @@ fn test_fund_invalid_cw20_denom() { amount: Uint128::new(1_000_000), }; - println!("attempting to fund the distributor contract with unregistered cw20 coin"); - suite.fund_distributor_cw20(unregistered_cw20_coin); + suite.fund_cw20(1, unregistered_cw20_coin); } #[test] -#[should_panic(expected = "Reward period already finished")] -fn test_shutdown_finished_rewards_period() { +#[should_panic(expected = "All rewards have already been distributed")] +fn test_withdraw_finished_rewards_period() { let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); // skip to expiration suite.skip_blocks(2_000_000); - suite.shutdown_denom_distribution(DENOM); + suite.withdraw(1); } #[test] -fn test_shutdown_alternative_destination_address() { +fn test_withdraw_alternative_destination_address() { let subdao_addr = "some_subdao_maybe".to_string(); let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) .with_withdraw_destination(Some(subdao_addr.to_string())) @@ -572,13 +1381,13 @@ fn test_shutdown_alternative_destination_address() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // user 1 and 2 claim their rewards - suite.claim_rewards(ADDR1, DENOM); - suite.claim_rewards(ADDR2, DENOM); + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); // user 2 unstakes suite.unstake_native_tokens(ADDR2, 50); @@ -588,37 +1397,37 @@ fn test_shutdown_alternative_destination_address() { let distribution_contract = suite.distribution_contract.to_string(); suite.assert_native_balance(subdao_addr.as_str(), DENOM, 0); - let pre_shutdown_distributor_balance = + let pre_withdraw_distributor_balance = suite.get_balance_native(distribution_contract.clone(), DENOM); - suite.shutdown_denom_distribution(DENOM); + suite.withdraw(1); - let post_shutdown_distributor_balance = + let post_withdraw_distributor_balance = suite.get_balance_native(distribution_contract.clone(), DENOM); - let post_shutdown_subdao_balance = suite.get_balance_native(subdao_addr.to_string(), DENOM); + let post_withdraw_subdao_balance = suite.get_balance_native(subdao_addr.to_string(), DENOM); - // after shutdown the balance of the subdao should be the same - // as pre-shutdown-distributor-bal minus post-shutdown-distributor-bal + // after withdraw the balance of the subdao should be the same + // as pre-withdraw-distributor-bal minus post-withdraw-distributor-bal assert_eq!( - pre_shutdown_distributor_balance - post_shutdown_distributor_balance, - post_shutdown_subdao_balance + pre_withdraw_distributor_balance - post_withdraw_distributor_balance, + post_withdraw_subdao_balance ); } #[test] -fn test_shutdown_block_based() { +fn test_withdraw_block_based() { let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + // suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + // suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + // suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // user 1 and 2 claim their rewards - suite.claim_rewards(ADDR1, DENOM); - suite.claim_rewards(ADDR2, DENOM); + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); // user 2 unstakes suite.unstake_native_tokens(ADDR2, 50); @@ -627,120 +1436,300 @@ fn test_shutdown_block_based() { let distribution_contract = suite.distribution_contract.to_string(); - let pre_shutdown_distributor_balance = + let pre_withdraw_distributor_balance = + suite.get_balance_native(distribution_contract.clone(), DENOM); + + suite.assert_native_balance(suite.owner.clone().unwrap().as_str(), DENOM, 0); + suite.withdraw(1); + + let post_withdraw_distributor_balance = + suite.get_balance_native(distribution_contract.clone(), DENOM); + let post_withdraw_owner_balance = suite.get_balance_native(suite.owner.clone().unwrap(), DENOM); + + // after withdraw the balance of the owner should be the same + // as pre-withdraw-distributor-bal minus post-withdraw-distributor-bal + assert_eq!( + pre_withdraw_distributor_balance - post_withdraw_distributor_balance, + post_withdraw_owner_balance + ); + + assert_eq!(pre_withdraw_distributor_balance, 92_500_000); + assert_eq!(post_withdraw_distributor_balance, 12_500_000); + assert_eq!(post_withdraw_owner_balance, 80_000_000); + + suite.skip_blocks(100_000); + + // ensure cannot withdraw again + assert_eq!( + suite.withdraw_error(1), + ContractError::RewardsAlreadyDistributed {} + ); + + // we assert that pending rewards did not change + suite.assert_pending_rewards(ADDR1, 1, 6_666_666); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 3_333_333 + 2_500_000); + + // user 1 can claim their rewards + suite.claim_rewards(ADDR1, 1); + // suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_native_balance(ADDR1, DENOM, 11_666_666); + + // user 3 can unstake and claim their rewards + suite.unstake_native_tokens(ADDR3, 50); + suite.skip_blocks(100_000); + suite.assert_native_balance(ADDR3, DENOM, 50); + suite.claim_rewards(ADDR3, 1); + // suite.assert_pending_rewards(ADDR3, 1, 0); + suite.assert_native_balance(ADDR3, DENOM, 3_333_333 + 2_500_000 + 50); + + // TODO: fix this rug of 1 udenom by the distribution contract + suite.assert_native_balance(&distribution_contract, DENOM, 1); +} + +#[test] +fn test_withdraw_time_based() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) + .with_rewards_config(RewardsConfig { + amount: 1_000, + denom: UncheckedDenom::Native(DENOM.to_string()), + duration: Duration::Time(10), + destination: None, + continuous: true, + }) + .build(); + + // skip 1/10th of the time + suite.skip_seconds(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + + // user 1 and 2 claim their rewards + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); + + // user 2 unstakes + suite.unstake_native_tokens(ADDR2, 50); + + suite.skip_seconds(100_000); + + let distribution_contract = suite.distribution_contract.to_string(); + + let pre_withdraw_distributor_balance = suite.get_balance_native(distribution_contract.clone(), DENOM); suite.assert_native_balance(suite.owner.clone().unwrap().as_str(), DENOM, 0); - suite.shutdown_denom_distribution(DENOM); + suite.withdraw(1); - let post_shutdown_distributor_balance = + let post_withdraw_distributor_balance = suite.get_balance_native(distribution_contract.clone(), DENOM); - let post_shutdown_owner_balance = suite.get_balance_native(suite.owner.clone().unwrap(), DENOM); + let post_withdraw_owner_balance = suite.get_balance_native(suite.owner.clone().unwrap(), DENOM); + + // after withdraw the balance of the owner should be the same + // as pre-withdraw-distributor-bal minus post-withdraw-distributor-bal + assert_eq!( + pre_withdraw_distributor_balance - post_withdraw_distributor_balance, + post_withdraw_owner_balance + ); + + assert_eq!(pre_withdraw_distributor_balance, 92_500_000); + assert_eq!(post_withdraw_distributor_balance, 12_500_000); + assert_eq!(post_withdraw_owner_balance, 80_000_000); + + suite.skip_seconds(100_000); + + // ensure cannot withdraw again + assert_eq!( + suite.withdraw_error(1), + ContractError::RewardsAlreadyDistributed {} + ); + + // we assert that pending rewards did not change + suite.assert_pending_rewards(ADDR1, 1, 6_666_666); + suite.assert_pending_rewards(ADDR2, 1, 0); + suite.assert_pending_rewards(ADDR3, 1, 3_333_333 + 2_500_000); + + // user 1 can claim their rewards + suite.claim_rewards(ADDR1, 1); + suite.assert_pending_rewards(ADDR1, 1, 0); + suite.assert_native_balance(ADDR1, DENOM, 11_666_666); + + // user 3 can unstake and claim their rewards + suite.unstake_native_tokens(ADDR3, 50); + suite.skip_seconds(100_000); + suite.assert_native_balance(ADDR3, DENOM, 50); + suite.claim_rewards(ADDR3, 1); + suite.assert_pending_rewards(ADDR3, 1, 0); + suite.assert_native_balance(ADDR3, DENOM, 3_333_333 + 2_500_000 + 50); + + // TODO: fix this rug of 1 udenom by the distribution contract + suite.assert_native_balance(&distribution_contract, DENOM, 1); +} + +#[test] +fn test_withdraw_and_restart_with_continuous() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) + .with_rewards_config(RewardsConfig { + amount: 1_000, + denom: UncheckedDenom::Native(DENOM.to_string()), + duration: Duration::Time(10), + destination: None, + continuous: true, + }) + .build(); + + // skip 1/10th of the time + suite.skip_seconds(100_000); + + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + + // users claim their rewards + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); + + // skip 1/10th of the time + suite.skip_seconds(100_000); + + let distribution_contract = suite.distribution_contract.to_string(); + + let pre_withdraw_distributor_balance = + suite.get_balance_native(distribution_contract.clone(), DENOM); + + suite.assert_native_balance(suite.owner.clone().unwrap().as_str(), DENOM, 0); + suite.withdraw(1); + + let post_withdraw_distributor_balance = + suite.get_balance_native(distribution_contract.clone(), DENOM); + let post_withdraw_owner_balance = suite.get_balance_native(suite.owner.clone().unwrap(), DENOM); + + // after withdraw the balance of the owner should be the same + // as pre-withdraw-distributor-bal minus post-withdraw-distributor-bal + assert_eq!( + pre_withdraw_distributor_balance - post_withdraw_distributor_balance, + post_withdraw_owner_balance + ); + + assert_eq!(pre_withdraw_distributor_balance, 90_000_000); + assert_eq!(post_withdraw_distributor_balance, 10_000_000); + assert_eq!(post_withdraw_owner_balance, 80_000_000); + + // skip 1/10th of the time + suite.skip_seconds(100_000); - // after shutdown the balance of the owner should be the same - // as pre-shutdown-distributor-bal minus post-shutdown-distributor-bal + // ensure cannot withdraw again assert_eq!( - pre_shutdown_distributor_balance - post_shutdown_distributor_balance, - post_shutdown_owner_balance + suite.withdraw_error(1), + ContractError::RewardsAlreadyDistributed {} ); - suite.skip_blocks(100_000); - // we assert that pending rewards did not change - suite.assert_pending_rewards(ADDR1, DENOM, 6_666_666); - suite.assert_pending_rewards(ADDR2, DENOM, 0); - suite.assert_pending_rewards(ADDR3, DENOM, 5_833_333); - - // user 1 can claim their rewards - suite.claim_rewards(ADDR1, DENOM); - suite.assert_pending_rewards(ADDR1, DENOM, 0); - suite.assert_native_balance(ADDR1, DENOM, 11_666_666); - - // user 3 can unstake and claim their rewards - suite.unstake_native_tokens(ADDR3, 50); - suite.skip_blocks(100_000); - suite.assert_native_balance(ADDR3, DENOM, 50); - suite.claim_rewards(ADDR3, DENOM); - suite.assert_pending_rewards(ADDR3, DENOM, 0); - suite.assert_native_balance(ADDR3, DENOM, 5_833_333 + 50); - - // TODO: fix this rug of 1 udenom by the distribution contract - suite.assert_native_balance(&distribution_contract, DENOM, 1); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); + + // fund again + suite.fund_native(1, coin(100_000_000, DENOM)); + + // check that pending rewards did not restart. since we skipped 1/10th the + // time after the withdraw occurred, everyone should already have 10% of the + // new amount pending. + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); } #[test] -fn test_shutdown_time_based() { +fn test_withdraw_and_restart_not_continuous() { let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) .with_rewards_config(RewardsConfig { amount: 1_000, denom: UncheckedDenom::Native(DENOM.to_string()), duration: Duration::Time(10), destination: None, + continuous: false, }) .build(); // skip 1/10th of the time suite.skip_seconds(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); - - // user 1 and 2 claim their rewards - suite.claim_rewards(ADDR1, DENOM); - suite.claim_rewards(ADDR2, DENOM); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); - // user 2 unstakes - suite.unstake_native_tokens(ADDR2, 50); + // users claim their rewards + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); + // skip 1/10th of the time suite.skip_seconds(100_000); let distribution_contract = suite.distribution_contract.to_string(); - let pre_shutdown_distributor_balance = + let pre_withdraw_distributor_balance = suite.get_balance_native(distribution_contract.clone(), DENOM); suite.assert_native_balance(suite.owner.clone().unwrap().as_str(), DENOM, 0); - suite.shutdown_denom_distribution(DENOM); + suite.withdraw(1); - let post_shutdown_distributor_balance = + let post_withdraw_distributor_balance = suite.get_balance_native(distribution_contract.clone(), DENOM); - let post_shutdown_owner_balance = suite.get_balance_native(suite.owner.clone().unwrap(), DENOM); + let post_withdraw_owner_balance = suite.get_balance_native(suite.owner.clone().unwrap(), DENOM); - // after shutdown the balance of the owner should be the same - // as pre-shutdown-distributor-bal minus post-shutdown-distributor-bal + // after withdraw the balance of the owner should be the same + // as pre-withdraw-distributor-bal minus post-withdraw-distributor-bal assert_eq!( - pre_shutdown_distributor_balance - post_shutdown_distributor_balance, - post_shutdown_owner_balance + pre_withdraw_distributor_balance - post_withdraw_distributor_balance, + post_withdraw_owner_balance ); + assert_eq!(pre_withdraw_distributor_balance, 90_000_000); + assert_eq!(post_withdraw_distributor_balance, 10_000_000); + assert_eq!(post_withdraw_owner_balance, 80_000_000); + + // skip 1/10th of the time suite.skip_seconds(100_000); + // ensure cannot withdraw again + assert_eq!( + suite.withdraw_error(1), + ContractError::RewardsAlreadyDistributed {} + ); + // we assert that pending rewards did not change - suite.assert_pending_rewards(ADDR1, DENOM, 6_666_666); - suite.assert_pending_rewards(ADDR2, DENOM, 0); - suite.assert_pending_rewards(ADDR3, DENOM, 5_833_333); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); - // user 1 can claim their rewards - suite.claim_rewards(ADDR1, DENOM); - suite.assert_pending_rewards(ADDR1, DENOM, 0); - suite.assert_native_balance(ADDR1, DENOM, 11_666_666); + // fund again + suite.fund_native(1, coin(100_000_000, DENOM)); - // user 3 can unstake and claim their rewards - suite.unstake_native_tokens(ADDR3, 50); + // skip 1/10th of the time suite.skip_seconds(100_000); - suite.assert_native_balance(ADDR3, DENOM, 50); - suite.claim_rewards(ADDR3, DENOM); - suite.assert_pending_rewards(ADDR3, DENOM, 0); - suite.assert_native_balance(ADDR3, DENOM, 5_833_333 + 50); - // TODO: fix this rug of 1 udenom by the distribution contract - suite.assert_native_balance(&distribution_contract, DENOM, 1); + // check that pending rewards restarted from the funding date. since we + // skipped 1/10th the time after the funding occurred, everyone should + // have 10% of the new amount pending + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); } #[test] #[should_panic(expected = "Caller is not the contract's current owner")] -fn test_shudown_unauthorized() { +fn test_withdraw_unauthorized() { let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); // skip 1/10th of the time @@ -752,37 +1741,30 @@ fn test_shudown_unauthorized() { .execute_contract( Addr::unchecked(ADDR1), suite.distribution_contract.clone(), - &ExecuteMsg::Shutdown { - denom: DENOM.to_string(), - }, + &ExecuteMsg::Withdraw { id: 1 }, &[], ) .unwrap(); } #[test] -#[should_panic] -fn test_shutdown_unregistered_denom() { +#[should_panic(expected = "Distribution not found with ID 3")] +fn test_withdraw_404() { let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); suite.skip_blocks(100_000); - suite.shutdown_denom_distribution("not-the-denom"); + suite.withdraw(3); } #[test] -#[should_panic(expected = "Denom already registered")] -fn test_register_duplicate_denom() { +#[should_panic(expected = "Distribution not found with ID 3")] +fn test_claim_404() { let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); - let hook_caller = suite.staking_addr.to_string(); - let reward_config = RewardsConfig { - amount: 1000, - denom: cw20::UncheckedDenom::Native(DENOM.to_string()), - duration: Duration::Height(100), - destination: None, - }; - suite.register_reward_denom(reward_config, &hook_caller); + suite.skip_blocks(100_000); + + suite.claim_rewards(ADDR1, 3); } #[test] @@ -793,42 +1775,30 @@ fn test_fund_invalid_native_denom() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.mint_native_coin(coin(100_000_000, ALT_DENOM), OWNER); + suite.mint_native(coin(100_000_000, ALT_DENOM), OWNER); suite .app .borrow_mut() .execute_contract( Addr::unchecked(OWNER), suite.distribution_contract.clone(), - &ExecuteMsg::Fund {}, + &ExecuteMsg::Fund(FundMsg { id: 1 }), &[coin(100_000_000, ALT_DENOM)], ) .unwrap(); } #[test] -fn test_fund_unauthorized() { - let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); - - // skip 1/10th of the time - suite.skip_blocks(100_000); - - suite.mint_native_coin(coin(100_000_000, DENOM), ADDR1); - suite - .app - .borrow_mut() - .execute_contract( - Addr::unchecked(ADDR1), - suite.distribution_contract.clone(), - &ExecuteMsg::Fund {}, - &[coin(100_000_000, DENOM)], - ) - .unwrap(); -} - -#[test] -fn test_fund_native_block_based_post_expiration() { - let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); +fn test_fund_native_block_based_post_expiration_not_continuous() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) + .with_rewards_config(RewardsConfig { + amount: 1_000, + denom: UncheckedDenom::Native(DENOM.to_string()), + duration: Duration::Height(10), + destination: None, + continuous: false, + }) + .build(); let started_at = Expiration::AtHeight(0); let funded_blocks = 1_000_000; @@ -841,22 +1811,22 @@ fn test_fund_native_block_based_post_expiration() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // ADDR2 unstake their stake suite.unstake_native_tokens(ADDR2, 50); // addr3 claims their rewards - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR3, 1); // skip to 100_000 blocks past the expiration suite.skip_blocks(1_000_000); - suite.assert_pending_rewards(ADDR1, DENOM, 65_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 30_000_000); + suite.assert_pending_rewards(ADDR1, 1, 65_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 30_000_000); suite.assert_ends_at(expiration_date); suite.assert_started_at(started_at); @@ -864,7 +1834,7 @@ fn test_fund_native_block_based_post_expiration() { // we fund the distributor with the same amount of coins as // during setup, meaning that the rewards distribution duration // should be the same. - suite.fund_distributor_native(coin(100_000_000, DENOM)); + suite.fund_native(1, coin(100_000_000, DENOM)); let current_block = suite.app.block_info(); @@ -879,16 +1849,19 @@ fn test_fund_native_block_based_post_expiration() { } #[test] -fn test_fund_cw20_time_based_post_expiration() { +fn test_fund_cw20_time_based_post_expiration_not_continuous() { let mut suite = SuiteBuilder::base(super::suite::DaoType::CW20) .with_rewards_config(RewardsConfig { amount: 1_000, denom: UncheckedDenom::Cw20(DENOM.to_string()), duration: Duration::Time(10), destination: None, + continuous: false, }) .build(); + let cw20_denom = &suite.reward_denom.clone(); + let started_at = Expiration::AtTime(Timestamp::from_seconds(0)); let funded_timestamp = Timestamp::from_seconds(1_000_000); let expiration_date = Expiration::AtTime(funded_timestamp); @@ -900,23 +1873,23 @@ fn test_fund_cw20_time_based_post_expiration() { // skip 1/10th of the time suite.skip_seconds(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // ADDR2 unstake their stake suite.unstake_cw20_tokens(50, ADDR2); // addr3 claims their rewards - suite.claim_rewards(ADDR3, suite.reward_denom.clone().as_str()); - suite.assert_cw20_balance(ADDR3, 2_500_000); + suite.claim_rewards(ADDR3, 1); + suite.assert_cw20_balance(cw20_denom, ADDR3, 2_500_000); // skip to 100_000 blocks past the expiration suite.skip_seconds(1_000_000); - suite.assert_pending_rewards(ADDR1, DENOM, 65_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 30_000_000); + suite.assert_pending_rewards(ADDR1, 1, 65_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 30_000_000); suite.assert_ends_at(expiration_date); suite.assert_started_at(started_at); @@ -929,7 +1902,7 @@ fn test_fund_cw20_time_based_post_expiration() { amount: Uint128::new(100_000_000), }; - suite.fund_distributor_cw20(funding_denom.clone()); + suite.fund_cw20(1, funding_denom.clone()); let current_block = suite.app.block_info(); @@ -953,6 +1926,7 @@ fn test_fund_cw20_time_based_pre_expiration() { denom: UncheckedDenom::Cw20(DENOM.to_string()), duration: Duration::Time(10), destination: None, + continuous: true, }) .build(); @@ -967,22 +1941,22 @@ fn test_fund_cw20_time_based_pre_expiration() { // skip 1/10th of the time suite.skip_seconds(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // ADDR2 unstake their stake suite.unstake_cw20_tokens(50, ADDR2); // addr3 claims their rewards - suite.claim_rewards(ADDR3, suite.reward_denom.clone().as_str()); + suite.claim_rewards(ADDR3, 1); // skip to 100_000 blocks before the expiration suite.skip_seconds(800_000); - suite.assert_pending_rewards(ADDR1, DENOM, 58_333_333); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 26_666_666); + suite.assert_pending_rewards(ADDR1, 1, 58_333_333); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 26_666_666); suite.assert_ends_at(expiration_date); suite.assert_started_at(started_at); @@ -994,7 +1968,7 @@ fn test_fund_cw20_time_based_pre_expiration() { address: suite.reward_denom.to_string(), amount: Uint128::new(100_000_000), }; - suite.fund_distributor_cw20(funding_denom.clone()); + suite.fund_cw20(1, funding_denom.clone()); // funding before the reward period expires should // not reset the existing rewards cycle @@ -1023,22 +1997,22 @@ fn test_fund_native_height_based_pre_expiration() { // skip 1/10th of the time suite.skip_blocks(100_000); - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // ADDR2 unstake their stake suite.unstake_native_tokens(ADDR2, 50); // addr3 claims their rewards - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR3, 1); // skip to 100_000 blocks before the expiration suite.skip_blocks(800_000); - suite.assert_pending_rewards(ADDR1, DENOM, 58_333_333); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 26_666_666); + suite.assert_pending_rewards(ADDR1, 1, 58_333_333); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 26_666_666); suite.assert_ends_at(expiration_date); suite.assert_started_at(started_at); @@ -1046,7 +2020,7 @@ fn test_fund_native_height_based_pre_expiration() { // we fund the distributor with the same amount of coins as // during setup, meaning that the rewards distribution duration // should be the same. - suite.fund_distributor_native(coin(100_000_000, DENOM)); + suite.fund_native(1, coin(100_000_000, DENOM)); // funding before the reward period expires should // not reset the existing rewards cycle @@ -1075,27 +2049,26 @@ fn test_native_dao_rewards_entry_edge_case() { // [ADDR1: 200, ADDR2: 50, ADDR3: 50], or [ADDR1: 66.6%, ADDR2: 16.6%, ADDR3: 16.6%] // this means that per 100_000 blocks, ADDR1 should receive 6_666_666, while // ADDR2 and ADDR3 should receive 1_666_666 each. - suite.mint_native_coin(coin(100, DENOM), ADDR1); - println!("staking native coins\n"); + suite.mint_native(coin(100, DENOM), ADDR1); suite.stake_native_tokens(ADDR1, 100); // rewards here should not be affected by the new stake, - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); // skip 1/10th of the time suite.skip_blocks(100_000); // here we should see the new stake affecting the rewards split. - suite.assert_pending_rewards(ADDR1, DENOM, 5_000_000 + 6_666_666); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000 + 1_666_666); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000 + 1_666_666); + suite.assert_pending_rewards(ADDR1, 1, 5_000_000 + 6_666_666); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000 + 1_666_666); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000 + 1_666_666); // ADDR1 claims rewards - suite.claim_rewards(ADDR1, DENOM); + suite.claim_rewards(ADDR1, 1); suite.assert_native_balance(ADDR1, DENOM, 5_000_000 + 6_666_666); - suite.assert_pending_rewards(ADDR1, DENOM, 0); + suite.assert_pending_rewards(ADDR1, 1, 0); // ADDR2 and ADDR3 unstake their stake // new voting power split is [ADDR1: 100%, ADDR2: 0%, ADDR3: 0%] @@ -1103,26 +2076,26 @@ fn test_native_dao_rewards_entry_edge_case() { suite.unstake_native_tokens(ADDR3, 50); // we assert that by unstaking, ADDR2 and ADDR3 do not forfeit their earned but unclaimed rewards - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000 + 1_666_666); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000 + 1_666_666); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000 + 1_666_666); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000 + 1_666_666); // skip a block and assert that nothing changes suite.skip_blocks(1); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000 + 1_666_666); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000 + 1_666_666); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000 + 1_666_666); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000 + 1_666_666); // skip the remaining blocks to reach 1/10th of the time suite.skip_blocks(99_999); // because ADDR2 and ADDR3 are not staking, ADDR1 receives all the rewards. // ADDR2 and ADDR3 should have the same amount of pending rewards as before. - suite.assert_pending_rewards(ADDR1, DENOM, 10_000_000); - suite.assert_pending_rewards(ADDR2, DENOM, 2_500_000 + 1_666_666); - suite.assert_pending_rewards(ADDR3, DENOM, 2_500_000 + 1_666_666); + suite.assert_pending_rewards(ADDR1, 1, 10_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000 + 1_666_666); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000 + 1_666_666); // ADDR2 and ADDR3 wake up, claim and restake their rewards - suite.claim_rewards(ADDR2, DENOM); - suite.claim_rewards(ADDR3, DENOM); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); let addr1_balance = suite.get_balance_native(ADDR1, DENOM); let addr2_balance = suite.get_balance_native(ADDR2, DENOM); @@ -1131,6 +2104,175 @@ fn test_native_dao_rewards_entry_edge_case() { suite.stake_native_tokens(ADDR2, addr2_balance); } +#[test] +fn test_fund_native_on_create() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + let alt_coin = coin(100_000_000, ALT_DENOM); + suite.mint_native(alt_coin.clone(), OWNER); + let hook_caller = suite.staking_addr.to_string(); + + suite.create( + RewardsConfig { + amount: 1000, + denom: cw20::UncheckedDenom::Native(ALT_DENOM.to_string()), + duration: Duration::Height(100), + destination: None, + continuous: true, + }, + &hook_caller, + Some(alt_coin.amount), + ); + + let distribution = suite.get_distribution(2); + assert_eq!(distribution.funded_amount, alt_coin.amount); + assert_eq!( + distribution.active_epoch, + Epoch { + emission_rate: EmissionRate::Linear { + amount: Uint128::new(1000), + duration: Duration::Height(100), + continuous: true, + }, + started_at: Expiration::AtHeight(0), + ends_at: Expiration::AtHeight(10_000_000), + total_earned_puvp: Uint256::zero(), + last_updated_total_earned_puvp: Expiration::AtHeight(0), + } + ); + + suite.skip_blocks(1_000_000); // skip 1/10th of the time + + suite.assert_pending_rewards(ADDR1, 2, 5_000_000); + suite.assert_pending_rewards(ADDR2, 2, 2_500_000); + suite.assert_pending_rewards(ADDR3, 2, 2_500_000); +} + +#[test] +#[should_panic(expected = "Must send reserve token 'ujuno'")] +fn test_fund_native_with_other_denom() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + suite.mint_native(coin(100, ALT_DENOM), OWNER); + + let execute_create_msg = ExecuteMsg::Create(CreateMsg { + denom: cw20::UncheckedDenom::Native(DENOM.to_string()), + emission_rate: EmissionRate::Linear { + amount: Uint128::new(1000), + duration: Duration::Height(100), + continuous: true, + }, + hook_caller: suite.staking_addr.to_string(), + vp_contract: suite.voting_power_addr.to_string(), + withdraw_destination: None, + }); + + // create distribution with other denom provided + suite + .app + .execute_contract( + Addr::unchecked(OWNER), + suite.distribution_contract.clone(), + &execute_create_msg, + &coins(100, ALT_DENOM), + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Sent more than one denomination")] +fn test_fund_native_multiple_denoms() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + suite.mint_native(coin(100, DENOM), OWNER); + suite.mint_native(coin(100, ALT_DENOM), OWNER); + + let execute_create_msg = ExecuteMsg::Create(CreateMsg { + denom: cw20::UncheckedDenom::Native(DENOM.to_string()), + emission_rate: EmissionRate::Linear { + amount: Uint128::new(1000), + duration: Duration::Height(100), + continuous: true, + }, + hook_caller: suite.staking_addr.to_string(), + vp_contract: suite.voting_power_addr.to_string(), + withdraw_destination: None, + }); + + // create distribution with 0 amount + suite + .app + .execute_contract( + Addr::unchecked(OWNER), + suite.distribution_contract.clone(), + &execute_create_msg, + &[coin(100, DENOM), coin(100, ALT_DENOM)], + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "You cannot send native funds when creating a CW20 distribution")] +fn test_fund_native_on_create_cw20() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + suite.mint_native(coin(100, DENOM), OWNER); + + let cw20_denom = suite + .mint_cw20( + Cw20Coin { + address: OWNER.to_string(), + amount: Uint128::new(100), + }, + "newcoin", + ) + .to_string(); + + let execute_create_msg = ExecuteMsg::Create(CreateMsg { + denom: cw20::UncheckedDenom::Cw20(cw20_denom), + emission_rate: EmissionRate::Linear { + amount: Uint128::new(1000), + duration: Duration::Height(100), + continuous: true, + }, + hook_caller: suite.staking_addr.to_string(), + vp_contract: suite.voting_power_addr.to_string(), + withdraw_destination: None, + }); + + // create cw20 distribution with native funds provided + suite + .app + .execute_contract( + Addr::unchecked(OWNER), + suite.distribution_contract.clone(), + &execute_create_msg, + &coins(100, DENOM), + ) + .unwrap(); +} + +#[test] +fn test_update_continuous() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + suite.update_emission_rate(1, Duration::Height(100), 1000, true); + + let distribution = suite.get_distribution(1); + match distribution.active_epoch.emission_rate { + EmissionRate::Linear { continuous, .. } => assert!(continuous), + _ => panic!("Invalid emission rate"), + } + + suite.update_emission_rate(1, Duration::Height(100), 1000, false); + + let distribution = suite.get_distribution(1); + match distribution.active_epoch.emission_rate { + EmissionRate::Linear { continuous, .. } => assert!(!continuous), + _ => panic!("Invalid emission rate"), + } +} + #[test] fn test_update_owner() { let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); @@ -1141,3 +2283,248 @@ fn test_update_owner() { let owner = suite.get_owner().to_string(); assert_eq!(owner, new_owner); } + +#[test] +fn test_update_vp_contract() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + let new_vp_contract = setup_native_token_test(suite.app.borrow_mut()); + + suite.update_vp_contract(1, new_vp_contract.as_str()); + + let distribution = suite.get_distribution(1); + assert_eq!(distribution.vp_contract, new_vp_contract); +} + +#[test] +fn test_update_hook_caller() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + let new_hook_caller = "new_hook_caller"; + suite.update_hook_caller(1, new_hook_caller); + + let distribution = suite.get_distribution(1); + assert_eq!(distribution.hook_caller, new_hook_caller); +} + +#[test] +fn test_update_withdraw_destination() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + let new_withdraw_destination = "new_withdraw_destination"; + suite.update_withdraw_destination(1, new_withdraw_destination); + + let distribution = suite.get_distribution(1); + assert_eq!(distribution.withdraw_destination, new_withdraw_destination); +} + +#[test] +#[should_panic(expected = "Distribution not found with ID 3")] +fn test_update_404() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + suite.update_emission_rate(3, Duration::Height(100), 1000, false); +} + +#[test] +#[should_panic(expected = "Invalid emission rate: amount cannot be zero")] +fn test_validate_emission_rate_amount() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + suite.update_emission_rate(1, Duration::Time(100), 0, true); +} + +#[test] +#[should_panic(expected = "Invalid emission rate: duration cannot be zero")] +fn test_validate_emission_rate_duration_height() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + suite.update_emission_rate(1, Duration::Height(0), 100, true); +} + +#[test] +#[should_panic(expected = "Invalid emission rate: duration cannot be zero")] +fn test_validate_emission_rate_duration_time() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + suite.update_emission_rate(1, Duration::Time(0), 100, true); +} + +#[test] +fn test_query_info() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native).build(); + + let info = suite.get_info(); + + assert_eq!( + info, + InfoResponse { + info: ContractVersion { + contract: env!("CARGO_PKG_NAME").to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + } + } + ); +} + +#[test] +fn test_rewards_not_lost_after_discontinuous_restart() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) + .with_rewards_config(RewardsConfig { + amount: 3_000, + denom: UncheckedDenom::Native(DENOM.to_string()), + duration: Duration::Height(1), + destination: None, + continuous: false, + }) + .build(); + + suite.assert_amount(3_000); + suite.assert_ends_at(Expiration::AtHeight(33_333)); + suite.assert_duration(1); + + // skip to end + suite.skip_blocks(33_333); + + // check pending rewards + suite.assert_pending_rewards(ADDR1, 1, 49999500); + suite.assert_pending_rewards(ADDR2, 1, 24999750); + suite.assert_pending_rewards(ADDR3, 1, 24999750); + + // before user claim rewards, someone funded + suite.fund_native(1, coin(1u128, DENOM)); + + // pending rewards should still exist + suite.assert_pending_rewards(ADDR1, 1, 49999500); + suite.assert_pending_rewards(ADDR2, 1, 24999750); + suite.assert_pending_rewards(ADDR3, 1, 24999750); +} + +#[test] +fn test_fund_while_paused() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::CW4).build(); + + suite.assert_amount(1_000); + suite.assert_ends_at(Expiration::AtHeight(1_000_000)); + suite.assert_duration(10); + + // skip 1/10th + suite.skip_blocks(100_000); + + // check pending rewards + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + + // pause + suite.pause_emission(1); + + // pending rewards should still exist + suite.assert_pending_rewards(ADDR1, 1, 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2_500_000); + + // fund during pause the amount that's already been distributed + suite.fund_native(1, coin(10_000_000, DENOM)); + + // restart + suite.update_emission_rate(1, Duration::Height(10), 1_000, true); + + // expect it to last as long as it was initially going to + suite.assert_ends_at(Expiration::AtHeight(1_000_000 + 100_000)); + + // skip 1/10th + suite.skip_blocks(100_000); + + // check pending rewards + suite.assert_pending_rewards(ADDR1, 1, 2 * 5_000_000); + suite.assert_pending_rewards(ADDR2, 1, 2 * 2_500_000); + suite.assert_pending_rewards(ADDR3, 1, 2 * 2_500_000); + + // pause and fund more + suite.pause_emission(1); + suite.fund_native(1, coin(100_000_000, DENOM)); + + // restart + suite.update_emission_rate(1, Duration::Height(10), 1_000, true); + + // expect the start and end to adjust again + suite.assert_started_at(Expiration::AtHeight(200_000)); + suite.assert_ends_at(Expiration::AtHeight(1_000_000 + 100_000 + 1_000_000)); +} + +#[test] +fn test_large_stake_before_claim() { + let mut suite = SuiteBuilder::base(super::suite::DaoType::Native) + .with_rewards_config(RewardsConfig { + amount: 3_000, + denom: UncheckedDenom::Native(DENOM.to_string()), + duration: Duration::Height(1), + destination: None, + continuous: true, + }) + .build(); + + suite.assert_amount(3_000); + suite.assert_ends_at(Expiration::AtHeight(33_333)); + suite.assert_duration(1); + + // ADDR1 stake big amount of tokens + suite.skip_blocks(33_000); + suite.mint_native(coin(10_000, &suite.reward_denom), ADDR1); + suite.stake_native_tokens(ADDR1, 10_000); + + // ADD1 claims rewards in the next block + suite.skip_blocks(1); + suite.claim_rewards(ADDR1, 1); + + // skip to end + suite.skip_blocks(100_000_000); + + // all users should be able to claim rewards + suite.claim_rewards(ADDR1, 1); + suite.claim_rewards(ADDR2, 1); + suite.claim_rewards(ADDR3, 1); +} + +#[test] +fn test_migrate() { + let mut deps = mock_dependencies(); + + cw2::set_contract_version(&mut deps.storage, "test", "0.0.1").unwrap(); + + // wrong contract name errors + let err: ContractError = + crate::contract::migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap_err(); + assert_eq!( + err, + ContractError::MigrationErrorIncorrectContract { + expected: CONTRACT_NAME.to_string(), + actual: "test".to_string(), + } + ); + + // migration succeeds from past version of same contract + cw2::set_contract_version(&mut deps.storage, CONTRACT_NAME, "0.0.1").unwrap(); + crate::contract::migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap(); + + // same-version migration errors + let err: ContractError = + crate::contract::migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap_err(); + assert_eq!( + err, + ContractError::MigrationErrorInvalidVersion { + new: CONTRACT_VERSION.to_string(), + current: CONTRACT_VERSION.to_string(), + } + ); + + // future version errors + cw2::set_contract_version(&mut deps.storage, CONTRACT_NAME, "9.9.9").unwrap(); + let err: ContractError = + crate::contract::migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap_err(); + assert_eq!( + err, + ContractError::MigrationErrorInvalidVersion { + new: CONTRACT_VERSION.to_string(), + current: "9.9.9".to_string(), + } + ); +} diff --git a/contracts/external/btsg-ft-factory/Cargo.toml b/contracts/external/btsg-ft-factory/Cargo.toml new file mode 100644 index 000000000..e70ec7e6f --- /dev/null +++ b/contracts/external/btsg-ft-factory/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "btsg-ft-factory" +authors = ["noah "] +description = "A CosmWasm factory contract for issuing fantokens on BitSong." +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 = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw2 = { workspace = true } +cw-orch = { workspace = true } +cw-storage-plus = { workspace = true } +dao-interface = { workspace = true } +osmosis-std-derive = { workspace = true } +prost = { workspace = true } +prost-derive = { workspace = true } +prost-types = { workspace = true } +schemars = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +anyhow = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-multi-test = { workspace = true } +cw-utils = { workspace = true } +dao-dao-core = { workspace = true, features = ["library"] } +dao-proposal-single = { workspace = true, features = ["library"] } +dao-testing = { workspace = true } +dao-voting-token-staked = { workspace = true, features = ["library"] } +dao-voting = { workspace = true } diff --git a/contracts/external/btsg-ft-factory/README.md b/contracts/external/btsg-ft-factory/README.md new file mode 100644 index 000000000..4ccf751f5 --- /dev/null +++ b/contracts/external/btsg-ft-factory/README.md @@ -0,0 +1,11 @@ +# btsg-ft-factory + +Serves as a factory that issues new +[fantokens](https://github.com/bitsongofficial/go-bitsong/tree/main/x/fantoken) +on BitSong and returns their denom for use with the +[dao-voting-token-staked](../../voting/dao-voting-token-staked) voting module +contract. + +Instantiation and execution are permissionless. All DAOs will use the same +factory and execute `Issue` to create new fantokens through `TokenInfo::Factory` +during voting module instantiation. diff --git a/contracts/external/btsg-ft-factory/examples/schema.rs b/contracts/external/btsg-ft-factory/examples/schema.rs new file mode 100644 index 000000000..ffa4b588a --- /dev/null +++ b/contracts/external/btsg-ft-factory/examples/schema.rs @@ -0,0 +1,11 @@ +use btsg_ft_factory::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use cosmwasm_schema::write_api; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/external/btsg-ft-factory/schema/btsg-ft-factory.json b/contracts/external/btsg-ft-factory/schema/btsg-ft-factory.json new file mode 100644 index 000000000..66c4d8f67 --- /dev/null +++ b/contracts/external/btsg-ft-factory/schema/btsg-ft-factory.json @@ -0,0 +1,117 @@ +{ + "contract_name": "btsg-ft-factory", + "contract_version": "2.5.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Issues a new fantoken.", + "type": "object", + "required": [ + "issue" + ], + "properties": { + "issue": { + "$ref": "#/definitions/NewFanToken" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "InitialBalance": { + "type": "object", + "required": [ + "address", + "amount" + ], + "properties": { + "address": { + "type": "string" + }, + "amount": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "NewFanToken": { + "type": "object", + "required": [ + "initial_balances", + "max_supply", + "name", + "symbol", + "uri" + ], + "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" + } + ] + }, + "max_supply": { + "description": "Fan token max supply.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "name": { + "description": "Fan token name.", + "type": "string" + }, + "symbol": { + "description": "Fan token symbol.", + "type": "string" + }, + "uri": { + "description": "Fan token URI.", + "type": "string" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "type": "string", + "enum": [] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": {} +} diff --git a/contracts/external/btsg-ft-factory/src/bitsong.rs b/contracts/external/btsg-ft-factory/src/bitsong.rs new file mode 100644 index 000000000..0f047e97e --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/bitsong.rs @@ -0,0 +1,209 @@ +use osmosis_std_derive::CosmwasmExt; + +/// Coin defines a token with a denomination and an amount. +/// +/// NOTE: The amount field is an Int which implements the custom method +/// signatures required by gogoproto. +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + ::serde::Serialize, + ::serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/cosmos.base.v1beta1.Coin")] +pub struct Coin { + #[prost(string, tag = "1")] + pub denom: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub amount: ::prost::alloc::string::String, +} + +// see https://github.com/bitsongofficial/go-bitsong/blob/dfa3563dccf990eac1d9dc4462c2850b9b2a21e1/proto/bitsong/fantoken/v1beta1/tx.proto + +/// MsgIssue defines a message for issuing a new fan token +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgIssue")] +pub struct MsgIssue { + #[prost(string, tag = "1")] + pub symbol: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub name: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub max_supply: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub authority: ::prost::alloc::string::String, + #[prost(string, tag = "5")] + pub minter: ::prost::alloc::string::String, + #[prost(string, tag = "6")] + pub uri: ::prost::alloc::string::String, +} + +/// MsgIssueResponse defines the MsgIssue response type +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgIssueResponse")] +pub struct MsgIssueResponse { + #[prost(string, tag = "1")] + pub denom: ::prost::alloc::string::String, +} + +/// MsgMint defines a message for minting a new fan token +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgMint")] +pub struct MsgMint { + #[prost(string, tag = "1")] + pub recipient: ::prost::alloc::string::String, + #[prost(message, tag = "2")] + pub coin: ::core::option::Option, + #[prost(string, tag = "3")] + pub minter: ::prost::alloc::string::String, +} + +/// MsgMintResponse defines the MsgMint response type +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgMintResponse")] +pub struct MsgMintResponse {} + +/// MsgSetMinter defines a message for changing the fan token minter address +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgSetMinter")] +pub struct MsgSetMinter { + #[prost(string, tag = "1")] + pub denom: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub old_minter: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub new_minter: ::prost::alloc::string::String, +} + +/// MsgSetMinterResponse defines the MsgSetMinter response type +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgSetMinterResponse")] +pub struct MsgSetMinterResponse {} + +// MsgSetAuthority defines a message for changing the fan token minter address +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgSetAuthority")] +pub struct MsgSetAuthority { + #[prost(string, tag = "1")] + pub denom: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub old_authority: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub new_authority: ::prost::alloc::string::String, +} + +// MsgSetAuthorityResponse defines the MsgSetAuthority response type +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgSetAuthorityResponse")] +pub struct MsgSetAuthorityResponse {} + +/// MsgSetUri defines a message for updating the fan token URI +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgSetUri")] +pub struct MsgSetUri { + #[prost(string, tag = "1")] + pub authority: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub denom: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub uri: ::prost::alloc::string::String, +} + +/// MsgSetUriResponse defines the MsgSetUri response type +#[derive( + Clone, + PartialEq, + Eq, + ::prost::Message, + serde::Serialize, + serde::Deserialize, + schemars::JsonSchema, + CosmwasmExt, +)] +#[proto_message(type_url = "/bitsong.fantoken.MsgSetUriResponse")] +pub struct MsgSetUriResponse {} diff --git a/contracts/external/btsg-ft-factory/src/contract.rs b/contracts/external/btsg-ft-factory/src/contract.rs new file mode 100644 index 000000000..86f0806b5 --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/contract.rs @@ -0,0 +1,181 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdError, StdResult, SubMsg, +}; + +use cw2::set_contract_version; +use dao_interface::token::{InitialBalance, TokenFactoryCallback}; + +use crate::bitsong::{Coin, MsgIssue, MsgIssueResponse, MsgMint, MsgSetAuthority, MsgSetMinter}; +use crate::error::ContractError; +use crate::msg::{CreatingFanToken, ExecuteMsg, InstantiateMsg, MigrateMsg, NewFanToken, QueryMsg}; +use crate::state::CREATING_FAN_TOKEN; + +pub(crate) const CONTRACT_NAME: &str = "crates.io:btsg-ft-factory"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +pub const ISSUE_REPLY_ID: u64 = 0; + +#[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") + .add_attribute("creator", info.sender)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Issue(issue_info) => execute_issue(deps, env, info, issue_info), + } +} + +pub fn execute_issue( + deps: DepsMut, + env: Env, + info: MessageInfo, + token: NewFanToken, +) -> Result { + let dao: Addr = deps + .querier + .query_wasm_smart(info.sender, &dao_interface::voting::Query::Dao {})?; + + CREATING_FAN_TOKEN.save( + deps.storage, + &CreatingFanToken { + token: token.clone(), + dao: dao.clone(), + }, + )?; + + let msg = SubMsg::reply_on_success( + MsgIssue { + symbol: token.symbol, + name: token.name, + max_supply: token.max_supply.to_string(), + // this needs to be the current contract address as the authority is + // used to determine who is allowed to send this message. will be + // set to DAO in reply once token is issued. + authority: env.contract.address.to_string(), + // this needs to be the current contract address as we mint initial + // balances in the reply. will be set to DAO in reply once initial + // balances are minted. + minter: env.contract.address.to_string(), + uri: token.uri, + }, + ISSUE_REPLY_ID, + ); + + Ok(Response::default() + .add_attribute("action", "issue") + .add_submessage(msg)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { + Err(StdError::generic_err("no queries")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + match msg.id { + ISSUE_REPLY_ID => { + let MsgIssueResponse { denom } = msg.result.try_into()?; + + // must load fan token info from execution + let CreatingFanToken { token, dao } = CREATING_FAN_TOKEN.load(deps.storage)?; + + // mgs to be executed to finalize setup + let mut msgs: Vec = vec![]; + + // mint tokens for initial balances + token + .initial_balances + .iter() + .for_each(|b: &InitialBalance| { + msgs.push( + MsgMint { + recipient: b.address.clone(), + coin: Some(Coin { + amount: b.amount.to_string(), + denom: denom.clone(), + }), + minter: env.contract.address.to_string(), + } + .into(), + ); + }); + + // 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( + MsgMint { + recipient: dao.to_string(), + coin: Some(Coin { + amount: initial_dao_balance.to_string(), + denom: denom.clone(), + }), + minter: env.contract.address.to_string(), + } + .into(), + ); + } + } + + // set authority and minter to DAO + msgs.push( + MsgSetAuthority { + denom: denom.clone(), + old_authority: env.contract.address.to_string(), + new_authority: dao.to_string(), + } + .into(), + ); + msgs.push( + MsgSetMinter { + denom: denom.clone(), + old_minter: env.contract.address.to_string(), + new_minter: dao.to_string(), + } + .into(), + ); + + // create reply data for dao-voting-token-staked + let data = to_json_binary(&TokenFactoryCallback { + denom: denom.clone(), + token_contract: None, + module_instantiate_callback: None, + })?; + + // remove since we don't need it anymore + CREATING_FAN_TOKEN.remove(deps.storage); + + Ok(Response::default() + .add_messages(msgs) + .set_data(data) + .add_attribute("fantoken_denom", denom)) + } + _ => Err(ContractError::UnknownReplyID {}), + } +} + +#[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/external/btsg-ft-factory/src/error.rs b/contracts/external/btsg-ft-factory/src/error.rs new file mode 100644 index 000000000..b51988fea --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/error.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("An unknown reply ID was received.")] + UnknownReplyID {}, +} diff --git a/contracts/external/btsg-ft-factory/src/lib.rs b/contracts/external/btsg-ft-factory/src/lib.rs new file mode 100644 index 000000000..f5c4da5bd --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod bitsong; +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +mod shim; + +pub use crate::error::ContractError; + +#[cfg(test)] +mod testing; diff --git a/contracts/external/btsg-ft-factory/src/msg.rs b/contracts/external/btsg-ft-factory/src/msg.rs new file mode 100644 index 000000000..68937afed --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/msg.rs @@ -0,0 +1,44 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint128}; +use dao_interface::token::InitialBalance; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +#[derive(cw_orch::ExecuteFns)] +pub enum ExecuteMsg { + /// Issues a new fantoken. + Issue(NewFanToken), +} + +#[cw_serde] +pub struct CreatingFanToken { + /// Fan token info. + pub token: NewFanToken, + /// DAO address. + pub dao: Addr, +} + +#[cw_serde] +pub struct NewFanToken { + /// Fan token symbol. + pub symbol: String, + /// Fan token name. + pub name: String, + /// Fan token max supply. + pub max_supply: Uint128, + /// Fan token URI. + pub uri: String, + /// 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] +#[derive(QueryResponses, cw_orch::QueryFns)] +pub enum QueryMsg {} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/contracts/external/btsg-ft-factory/src/shim.rs b/contracts/external/btsg-ft-factory/src/shim.rs new file mode 100644 index 000000000..bd5d321da --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/shim.rs @@ -0,0 +1,63 @@ +// depended on by osmosis_std +#[derive(Clone, PartialEq, Eq, ::prost::Message, schemars::JsonSchema)] +pub struct Any { + /// A URL/resource name that uniquely identifies the type of the serialized + /// protocol buffer message. This string must contain at least + /// one "/" character. The last segment of the URL's path must represent + /// the fully qualified name of the type (as in + /// `path/google.protobuf.Duration`). The name should be in a canonical form + /// (e.g., leading "." is not accepted). + /// + /// In practice, teams usually precompile into the binary all types that they + /// expect it to use in the context of Any. However, for URLs which use the + /// scheme `http`, `https`, or no scheme, one can optionally set up a type + /// server that maps type URLs to message definitions as follows: + /// + /// * If no scheme is provided, `https` is assumed. + /// * An HTTP GET on the URL must yield a \[google.protobuf.Type][\] + /// value in binary format, or produce an error. + /// * Applications are allowed to cache lookup results based on the + /// URL, or have them precompiled into a binary to avoid any + /// lookup. Therefore, binary compatibility needs to be preserved + /// on changes to types. (Use versioned type names to manage + /// breaking changes.) + /// + /// Note: this functionality is not currently available in the official + /// protobuf release, and it is not used for type URLs beginning with + /// type.googleapis.com. + /// + /// Schemes other than `http`, `https` (or the empty scheme) might be + /// used with implementation specific semantics. + /// + #[prost(string, tag = "1")] + pub type_url: ::prost::alloc::string::String, + /// Must be a valid serialized protocol buffer of the above specified type. + #[prost(bytes = "vec", tag = "2")] + pub value: ::prost::alloc::vec::Vec, +} + +macro_rules! impl_prost_types_exact_conversion { + ($t:ident | $($arg:ident),*) => { + impl From<$t> for prost_types::$t { + fn from(src: $t) -> Self { + prost_types::$t { + $( + $arg: src.$arg, + )* + } + } + } + + impl From for $t { + fn from(src: prost_types::$t) -> Self { + $t { + $( + $arg: src.$arg, + )* + } + } + } + }; +} + +impl_prost_types_exact_conversion! { Any | type_url, value } diff --git a/contracts/external/btsg-ft-factory/src/state.rs b/contracts/external/btsg-ft-factory/src/state.rs new file mode 100644 index 000000000..aef1663f9 --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/state.rs @@ -0,0 +1,7 @@ +use cw_storage_plus::Item; + +use crate::msg::CreatingFanToken; + +/// Temporarily holds data about the fan token being created that's needed in +/// reply so we can mint initial tokens and reset the minter. +pub const CREATING_FAN_TOKEN: Item = Item::new("cft"); diff --git a/contracts/external/btsg-ft-factory/src/testing/app.rs b/contracts/external/btsg-ft-factory/src/testing/app.rs new file mode 100644 index 000000000..558e16f80 --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/testing/app.rs @@ -0,0 +1,61 @@ +use std::ops::{Deref, DerefMut}; + +use crate::testing::bitsong_stargate::StargateKeeper; +use cosmwasm_std::{testing::MockApi, Empty, GovMsg, IbcMsg, IbcQuery, MemoryStorage}; +use cw_multi_test::{ + no_init, App, AppBuilder, BankKeeper, DistributionKeeper, FailingModule, StakeKeeper, + WasmKeeper, +}; +#[allow(clippy::type_complexity)] +pub struct BitsongApp( + App< + BankKeeper, + MockApi, + MemoryStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + FailingModule, + FailingModule, + StargateKeeper, + >, +); +impl Deref for BitsongApp { + type Target = App< + BankKeeper, + MockApi, + MemoryStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + FailingModule, + FailingModule, + StargateKeeper, + >; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for BitsongApp { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl Default for BitsongApp { + fn default() -> Self { + Self::new() + } +} + +impl BitsongApp { + pub fn new() -> Self { + let app_builder = AppBuilder::default(); + let stargate = StargateKeeper {}; + let app = app_builder.with_stargate(stargate).build(no_init); + BitsongApp(app) + } +} diff --git a/contracts/external/btsg-ft-factory/src/testing/bitsong_stargate.rs b/contracts/external/btsg-ft-factory/src/testing/bitsong_stargate.rs new file mode 100644 index 000000000..024527888 --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/testing/bitsong_stargate.rs @@ -0,0 +1,180 @@ +use anyhow::Error; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + from_json, to_json_binary, Addr, Api, Binary, BlockInfo, Coin, Querier, Storage, Uint64, +}; +use cw_multi_test::{error::AnyResult, AppResponse, BankSudo, CosmosRouter, Stargate, SudoMsg}; +use prost::Message; + +use crate::bitsong::{ + MsgIssue, MsgIssueResponse, MsgMint, MsgMintResponse, MsgSetAuthority, MsgSetMinter, + MsgSetMinterResponse, MsgSetUri, MsgSetUriResponse, +}; + +const DENOMS_PREFIX: &str = "denoms"; +const DENOMS_COUNT_KEY: &str = "denoms_count"; + +#[cw_serde] +struct FanToken { + pub denom: String, + pub name: String, + pub symbol: String, + pub max_supply: String, + pub authority: String, + pub minter: String, + pub uri: String, +} + +pub struct StargateKeeper {} + +impl StargateKeeper {} + +impl Stargate for StargateKeeper { + fn execute( + &self, + api: &dyn Api, + storage: &mut dyn Storage, + router: &dyn CosmosRouter, + block: &BlockInfo, + sender: Addr, + type_url: String, + value: Binary, + ) -> AnyResult { + if type_url == *"/bitsong.fantoken.MsgIssue" { + let denoms_count: Uint64 = storage + .get(DENOMS_COUNT_KEY.as_bytes()) + .map_or_else(Uint64::zero, |d| from_json(d).unwrap()); + let denom = format!("fantoken{}", denoms_count.u64() + 1); + + let msg: MsgIssue = Message::decode(value.as_slice()).unwrap(); + let ft = FanToken { + denom: denom.clone(), + name: msg.name, + symbol: msg.symbol, + max_supply: msg.max_supply, + authority: msg.authority, + minter: msg.minter, + uri: msg.uri, + }; + + let key = format!("{}:{}", DENOMS_PREFIX, denom.clone()); + let serialized_ft = to_json_binary(&ft).expect("Failed to serialize FanToken"); + storage.set(key.as_bytes(), &serialized_ft); + + return Ok(AppResponse { + events: vec![], + data: Some(Binary::from(MsgIssueResponse { denom })), + }); + } + if type_url == *"/bitsong.fantoken.MsgMint" { + let msg: MsgMint = Message::decode(value.as_slice()).unwrap(); + + let coin = msg.coin.unwrap(); + + let key = format!("{}:{}", DENOMS_PREFIX, coin.denom.clone()); + let serialized_ft = storage.get(key.as_bytes()); + let fantoken: FanToken = + from_json(serialized_ft.unwrap()).expect("Failed to deserialize FanToken"); + + if sender != fantoken.minter || msg.minter != fantoken.minter { + return Err(Error::msg("Minter unauthorized")); + } + + router.sudo( + api, + storage, + block, + SudoMsg::Bank(BankSudo::Mint { + to_address: msg.recipient.clone(), + amount: vec![Coin::new(coin.amount.parse().unwrap(), coin.denom.clone())], + }), + )?; + + return Ok(AppResponse { + events: vec![], + data: Some(Binary::from(MsgMintResponse {})), + }); + } + if type_url == *"/bitsong.fantoken.MsgSetMinter" { + let msg: MsgSetMinter = Message::decode(value.as_slice()).unwrap(); + + let key = format!("{}:{}", DENOMS_PREFIX, msg.denom.clone()); + let serialized_ft = storage.get(key.as_bytes()); + let mut fantoken: FanToken = + from_json(serialized_ft.unwrap()).expect("Failed to deserialize FanToken"); + + if sender != fantoken.minter { + return Err(Error::msg("Unauthorized")); + } + + if msg.old_minter != fantoken.minter { + return Err(Error::msg("Old minter does not match")); + } + + fantoken.minter = msg.new_minter; + storage.set(key.as_bytes(), &to_json_binary(&fantoken).unwrap()); + + return Ok(AppResponse { + events: vec![], + data: Some(Binary::from(MsgSetMinterResponse {})), + }); + } + if type_url == *"/bitsong.fantoken.MsgSetAuthority" { + let msg: MsgSetAuthority = Message::decode(value.as_slice()).unwrap(); + + let key = format!("{}:{}", DENOMS_PREFIX, msg.denom.clone()); + let serialized_ft = storage.get(key.as_bytes()); + let mut fantoken: FanToken = + from_json(serialized_ft.unwrap()).expect("Failed to deserialize FanToken"); + + if sender != fantoken.authority { + return Err(Error::msg("Unauthorized")); + } + + if msg.old_authority != fantoken.authority { + return Err(Error::msg("Old authority does not match")); + } + + fantoken.authority = msg.new_authority; + storage.set(key.as_bytes(), &to_json_binary(&fantoken).unwrap()); + + return Ok(AppResponse { + events: vec![], + data: Some(Binary::from(MsgSetMinterResponse {})), + }); + } + if type_url == *"/bitsong.fantoken.MsgSetUri" { + let msg: MsgSetUri = Message::decode(value.as_slice()).unwrap(); + + let key = format!("{}:{}", DENOMS_PREFIX, msg.denom.clone()); + let serialized_ft = storage.get(key.as_bytes()); + let mut fantoken: FanToken = + from_json(serialized_ft.unwrap()).expect("Failed to deserialize FanToken"); + + if sender != fantoken.authority || msg.authority != fantoken.authority { + return Err(Error::msg("Authority unauthorized")); + } + + fantoken.uri = msg.uri; + storage.set(key.as_bytes(), &to_json_binary(&fantoken).unwrap()); + + return Ok(AppResponse { + events: vec![], + data: Some(Binary::from(MsgSetUriResponse {})), + }); + } + Ok(AppResponse::default()) + } + + fn query( + &self, + _api: &dyn Api, + _storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + _path: String, + data: Binary, + ) -> AnyResult { + Ok(data) + } +} diff --git a/contracts/external/btsg-ft-factory/src/testing/mod.rs b/contracts/external/btsg-ft-factory/src/testing/mod.rs new file mode 100644 index 000000000..2282ac98d --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/testing/mod.rs @@ -0,0 +1,64 @@ +mod app; +mod bitsong_stargate; +mod tests; + +use app::BitsongApp; +use cosmwasm_std::{Addr, Empty}; +use cw_multi_test::{Contract, ContractWrapper, Executor}; +use dao_testing::contracts::native_staked_balances_voting_contract; + +use crate::msg::InstantiateMsg; + +/// Address used to stake stuff. +pub(crate) const STAKER: &str = "staker"; + +pub(crate) fn btsg_ft_factory_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) +} + +pub(crate) struct CommonTest { + app: BitsongApp, + module_id: u64, + factory: Addr, +} + +pub(crate) fn setup_test() -> CommonTest { + let mut app = BitsongApp::new(); + let factory_id = app.store_code(btsg_ft_factory_contract()); + let module_id = app.store_code(native_staked_balances_voting_contract()); + + let factory = app + .instantiate_contract( + factory_id, + Addr::unchecked("anyone"), + &InstantiateMsg {}, + &[], + "bitsong_fantoken_factory", + None, + ) + .unwrap(); + + CommonTest { + app, + module_id, + factory, + } +} + +// Advantage to using a macro for this is that the error trace links +// to the exact line that the error occured, instead of inside of a +// function where the assertion would otherwise happen. +macro_rules! is_error { + ($x:expr => $e:tt) => { + assert!(format!("{:#}", $x.unwrap_err()).contains($e)) + }; +} + +pub(crate) use is_error; diff --git a/contracts/external/btsg-ft-factory/src/testing/tests.rs b/contracts/external/btsg-ft-factory/src/testing/tests.rs new file mode 100644 index 000000000..21cb4f58a --- /dev/null +++ b/contracts/external/btsg-ft-factory/src/testing/tests.rs @@ -0,0 +1,534 @@ +use cosmwasm_std::{ + coins, + testing::{mock_dependencies, mock_env}, + to_json_binary, Addr, Uint128, WasmMsg, +}; +use cw_multi_test::Executor; +use cw_utils::Duration; +use dao_interface::{ + state::{Admin, ModuleInstantiateInfo}, + token::InitialBalance, +}; +use dao_testing::contracts::{dao_dao_contract, proposal_single_contract}; + +use crate::{ + bitsong::{Coin, MsgMint, MsgSetUri}, + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + msg::{ExecuteMsg, MigrateMsg, NewFanToken}, + testing::is_error, +}; + +use super::{setup_test, CommonTest, STAKER}; + +/// I can create a new fantoken on DAO creation. +#[test] +fn test_issue_fantoken() -> anyhow::Result<()> { + let CommonTest { + mut app, + factory, + module_id, + .. + } = setup_test(); + + let core_id = app.store_code(dao_dao_contract()); + let proposal_single_id = app.store_code(proposal_single_contract()); + + let initial_balances = vec![InitialBalance { + amount: Uint128::new(100), + address: STAKER.to_string(), + }]; + + let governance_instantiate = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: module_id, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: dao_voting_token_staked::msg::TokenInfo::Factory(to_json_binary( + &WasmMsg::Execute { + contract_addr: factory.to_string(), + msg: to_json_binary(&ExecuteMsg::Issue(NewFanToken { + symbol: "FAN".to_string(), + name: "Fantoken".to_string(), + max_supply: Uint128::new(1_000_000_000_000_000_000), + uri: "".to_string(), + initial_balances, + initial_dao_balance: Some(Uint128::new(100_000_000)), + }))?, + funds: vec![], + }, + )?), + unstaking_duration: None, + active_threshold: None, + }) + .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 { + threshold: dao_voting::threshold::Threshold::AbsoluteCount { + threshold: Uint128::new(100), + }, + max_voting_period: Duration::Time(86400), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + close_proposal_on_execution_failure: true, + veto: None, + })?, + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let dao = app + .instantiate_contract( + core_id, + Addr::unchecked(STAKER), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let voting_module: Addr = app + .wrap() + .query_wasm_smart(dao, &dao_interface::msg::QueryMsg::VotingModule {}) + .unwrap(); + + let denom_res: dao_interface::voting::DenomResponse = app + .wrap() + .query_wasm_smart( + voting_module, + &dao_voting_token_staked::msg::QueryMsg::Denom {}, + ) + .unwrap(); + + // first fantoken created has the denom "fantoken1" + assert_eq!(denom_res.denom, "fantoken1"); + + Ok(()) +} + +/// I can create a new fantoken on DAO creation with initial balances. +#[test] +fn test_initial_fantoken_balances() -> anyhow::Result<()> { + let CommonTest { + mut app, + factory, + module_id, + .. + } = setup_test(); + + let core_id = app.store_code(dao_dao_contract()); + let proposal_single_id = app.store_code(proposal_single_contract()); + + let initial_balances = vec![InitialBalance { + amount: Uint128::new(100), + address: STAKER.to_string(), + }]; + + let governance_instantiate = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: module_id, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: dao_voting_token_staked::msg::TokenInfo::Factory(to_json_binary( + &WasmMsg::Execute { + contract_addr: factory.to_string(), + msg: to_json_binary(&ExecuteMsg::Issue(NewFanToken { + symbol: "FAN".to_string(), + name: "Fantoken".to_string(), + max_supply: Uint128::new(1_000_000_000_000_000_000), + uri: "".to_string(), + initial_balances, + initial_dao_balance: Some(Uint128::new(100_000_000)), + }))?, + funds: vec![], + }, + )?), + unstaking_duration: None, + active_threshold: None, + }) + .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 { + threshold: dao_voting::threshold::Threshold::AbsoluteCount { + threshold: Uint128::new(100), + }, + max_voting_period: Duration::Time(86400), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + close_proposal_on_execution_failure: true, + veto: None, + })?, + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let dao = app + .instantiate_contract( + core_id, + Addr::unchecked(STAKER), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let voting_module: Addr = app + .wrap() + .query_wasm_smart(&dao, &dao_interface::msg::QueryMsg::VotingModule {}) + .unwrap(); + + let denom_res: dao_interface::voting::DenomResponse = app + .wrap() + .query_wasm_smart( + voting_module, + &dao_voting_token_staked::msg::QueryMsg::Denom {}, + ) + .unwrap(); + + // verify DAO has initial balance + let dao_balance = app.wrap().query_balance(&dao, &denom_res.denom).unwrap(); + assert_eq!(dao_balance.amount, Uint128::new(100_000_000)); + + // verify staker has initial balance + let staker_balance = app.wrap().query_balance(STAKER, &denom_res.denom).unwrap(); + assert_eq!(staker_balance.amount, Uint128::new(100)); + + Ok(()) +} + +/// The minter and authority are set to the DAO. +#[test] +fn test_fantoken_minter_and_authority_set_to_dao() -> anyhow::Result<()> { + let CommonTest { + mut app, + factory, + module_id, + .. + } = setup_test(); + + let core_id = app.store_code(dao_dao_contract()); + let proposal_single_id = app.store_code(proposal_single_contract()); + + let initial_balances = vec![InitialBalance { + amount: Uint128::new(100), + address: STAKER.to_string(), + }]; + + let governance_instantiate = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: module_id, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: dao_voting_token_staked::msg::TokenInfo::Factory(to_json_binary( + &WasmMsg::Execute { + contract_addr: factory.to_string(), + msg: to_json_binary(&ExecuteMsg::Issue(NewFanToken { + symbol: "FAN".to_string(), + name: "Fantoken".to_string(), + max_supply: Uint128::new(1_000_000_000_000_000_000), + uri: "".to_string(), + initial_balances, + initial_dao_balance: Some(Uint128::new(100_000_000)), + }))?, + funds: vec![], + }, + )?), + unstaking_duration: None, + active_threshold: None, + }) + .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 { + threshold: dao_voting::threshold::Threshold::AbsoluteCount { + threshold: Uint128::new(100), + }, + max_voting_period: Duration::Time(86400), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + close_proposal_on_execution_failure: true, + veto: None, + })?, + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let dao = app + .instantiate_contract( + core_id, + Addr::unchecked(STAKER), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let voting_module: Addr = app + .wrap() + .query_wasm_smart(&dao, &dao_interface::msg::QueryMsg::VotingModule {}) + .unwrap(); + + let denom_res: dao_interface::voting::DenomResponse = app + .wrap() + .query_wasm_smart( + voting_module, + &dao_voting_token_staked::msg::QueryMsg::Denom {}, + ) + .unwrap(); + + // attempt to mint with factory that created the token, and fail + let res = app.execute( + factory.clone(), + MsgMint { + recipient: STAKER.to_string(), + coin: Some(Coin { + amount: "100".to_string(), + denom: denom_res.denom.clone(), + }), + minter: factory.to_string(), + } + .into(), + ); + is_error!(res => "Minter unauthorized"); + + // verify minter is the DAO + app.execute( + dao.clone(), + MsgMint { + recipient: STAKER.to_string(), + coin: Some(Coin { + amount: "100".to_string(), + denom: denom_res.denom.clone(), + }), + minter: dao.to_string(), + } + .into(), + ) + .unwrap(); + + // attempt to change URI with factory that created the token, and fail + let res = app.execute( + factory.clone(), + MsgSetUri { + authority: factory.to_string(), + denom: denom_res.denom.clone(), + uri: "https://example.com".to_string(), + } + .into(), + ); + is_error!(res => "Authority unauthorized"); + + // verify authority is the DAO + app.execute( + dao.clone(), + MsgSetUri { + authority: dao.to_string(), + denom: denom_res.denom.clone(), + uri: "https://example.com".to_string(), + } + .into(), + ) + .unwrap(); + + // verify staker has new balance + let staker_balance = app.wrap().query_balance(STAKER, &denom_res.denom).unwrap(); + assert_eq!(staker_balance.amount, Uint128::new(200)); + + Ok(()) +} + +/// A staker can stake fantokens. +#[test] +fn test_fantoken_can_be_staked() -> anyhow::Result<()> { + let CommonTest { + mut app, + factory, + module_id, + .. + } = setup_test(); + + let core_id = app.store_code(dao_dao_contract()); + let proposal_single_id = app.store_code(proposal_single_contract()); + + let initial_balances = vec![InitialBalance { + amount: Uint128::new(100), + address: STAKER.to_string(), + }]; + + let governance_instantiate = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: module_id, + msg: to_json_binary(&dao_voting_token_staked::msg::InstantiateMsg { + token_info: dao_voting_token_staked::msg::TokenInfo::Factory(to_json_binary( + &WasmMsg::Execute { + contract_addr: factory.to_string(), + msg: to_json_binary(&ExecuteMsg::Issue(NewFanToken { + symbol: "FAN".to_string(), + name: "Fantoken".to_string(), + max_supply: Uint128::new(1_000_000_000_000_000_000), + uri: "".to_string(), + initial_balances, + initial_dao_balance: Some(Uint128::new(100_000_000)), + }))?, + funds: vec![], + }, + )?), + unstaking_duration: None, + active_threshold: None, + }) + .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 { + threshold: dao_voting::threshold::Threshold::AbsoluteCount { + threshold: Uint128::new(100), + }, + max_voting_period: Duration::Time(86400), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + close_proposal_on_execution_failure: true, + veto: None, + })?, + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let dao = app + .instantiate_contract( + core_id, + Addr::unchecked(STAKER), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + let voting_module: Addr = app + .wrap() + .query_wasm_smart(dao, &dao_interface::msg::QueryMsg::VotingModule {}) + .unwrap(); + + let denom_res: dao_interface::voting::DenomResponse = app + .wrap() + .query_wasm_smart( + &voting_module, + &dao_voting_token_staked::msg::QueryMsg::Denom {}, + ) + .unwrap(); + + // verify staker voting power is 0 + let vp: dao_interface::voting::VotingPowerAtHeightResponse = app.wrap().query_wasm_smart( + &voting_module, + &dao_interface::voting::Query::VotingPowerAtHeight { + address: STAKER.to_string(), + height: None, + }, + )?; + assert_eq!(vp.power, Uint128::new(0)); + + // stake from staker + app.execute_contract( + Addr::unchecked(STAKER), + voting_module.clone(), + &dao_voting_token_staked::msg::ExecuteMsg::Stake {}, + &coins(100, denom_res.denom), + )?; + + // next block so voting power is updated + app.update_block(|b| b.height += 1); + + // verify staker voting power is 100 + let vp: dao_interface::voting::VotingPowerAtHeightResponse = app.wrap().query_wasm_smart( + &voting_module, + &dao_interface::voting::Query::VotingPowerAtHeight { + address: STAKER.to_string(), + height: None, + }, + )?; + assert_eq!(vp.power, Uint128::new(100)); + + Ok(()) +} + +#[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); + + // migrate again, should do nothing + 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/external/cw-admin-factory/Cargo.toml b/contracts/external/cw-admin-factory/Cargo.toml index f7f0034ea..92e299c2f 100644 --- a/contracts/external/cw-admin-factory/Cargo.toml +++ b/contracts/external/cw-admin-factory/Cargo.toml @@ -1,5 +1,5 @@ [package] -name ="cw-admin-factory" +name = "cw-admin-factory" authors = ["Jake Hartnell", "blue-note", "ekez "] description = "A CosmWasm factory contract for instantiating a contract as its own admin." edition = { workspace = true } @@ -15,18 +15,32 @@ crate-type = ["cdylib", "rlib"] 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-std = { workspace = true, features = ["cosmwasm_1_2"] } cosmwasm-schema = { workspace = true } cw-storage-plus = { workspace = true } cw2 = { workspace = true } +cw-orch = { workspace = true } thiserror = { workspace = true } cw-utils = { workspace = true } [dev-dependencies] +bech32 = { workspace = true } cosmwasm-schema = { workspace = true } +cw-admin-factory = { workspace = true } cw-multi-test = { workspace = true } +cw20-base = { workspace = true, features = ["library"] } +cw4 = { workspace = true } dao-dao-core = { workspace = true, features = ["library"] } dao-interface = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } +dao-proposal-single = { workspace = true } +dao-testing = { workspace = true } +dao-voting = { workspace = true } +dao-voting-cw4 = { workspace = true } +osmosis-test-tube = { workspace = true } 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 a85308e6e..d6f3ebcd9 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -22,7 +22,7 @@ "title": "ExecuteMsg", "oneOf": [ { - "description": "Instantiates the target contract with the provided instantiate message and code id and updates the contract's admin to be itself.", + "description": "Instantiates the target contract with the provided instantiate message, code ID, and label and updates the contract's admin to be itself.", "type": "object", "required": [ "instantiate_contract_with_self_admin" @@ -52,6 +52,49 @@ } }, "additionalProperties": false + }, + { + "description": "Instantiates the target contract with the provided instantiate message, code ID, label, and salt, via instantiate2 to give a predictable address, and updates the contract's admin to be itself.", + "type": "object", + "required": [ + "instantiate2_contract_with_self_admin" + ], + "properties": { + "instantiate2_contract_with_self_admin": { + "type": "object", + "required": [ + "code_id", + "instantiate_msg", + "label", + "salt" + ], + "properties": { + "code_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "expect": { + "description": "Optionally specify the expected address and fail if it doesn't match the instantiated contract. This makes it easy for a consumer to validate that they are using the correct address elsewhere.", + "type": [ + "string", + "null" + ] + }, + "instantiate_msg": { + "$ref": "#/definitions/Binary" + }, + "label": { + "type": "string" + }, + "salt": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { diff --git a/contracts/external/cw-admin-factory/src/contract.rs b/contracts/external/cw-admin-factory/src/contract.rs index 00eaf73cb..d0ac8fc95 100644 --- a/contracts/external/cw-admin-factory/src/contract.rs +++ b/contracts/external/cw-admin-factory/src/contract.rs @@ -10,11 +10,13 @@ use cw_utils::parse_reply_instantiate_data; use crate::error::ContractError; use crate::msg::{AdminResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use crate::state::ADMIN; +use crate::state::{ADMIN, EXPECT}; pub(crate) const CONTRACT_NAME: &str = "crates.io:cw-admin-factory"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + pub const INSTANTIATE_CONTRACT_REPLY_ID: u64 = 0; +pub const INSTANTIATE2_CONTRACT_REPLY_ID: u64 = 2; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -46,6 +48,13 @@ pub fn execute( code_id, label, } => instantiate_contract(deps, env, info, msg, code_id, label), + ExecuteMsg::Instantiate2ContractWithSelfAdmin { + instantiate_msg: msg, + code_id, + label, + salt, + expect, + } => instantiate2_contract(deps, env, info, msg, code_id, label, salt, expect), } } @@ -75,7 +84,46 @@ pub fn instantiate_contract( let msg = SubMsg::reply_on_success(instantiate, INSTANTIATE_CONTRACT_REPLY_ID); Ok(Response::default() - .add_attribute("action", "instantiate_cw_core") + .add_attribute("action", "instantiate_contract_with_self_admin") + .add_submessage(msg)) +} + +#[allow(clippy::too_many_arguments)] +pub fn instantiate2_contract( + deps: DepsMut, + env: Env, + info: MessageInfo, + instantiate_msg: Binary, + code_id: u64, + label: String, + salt: Binary, + expect: Option, +) -> Result { + // If admin set, require the sender to be the admin. + if let Some(admin) = ADMIN.load(deps.storage)? { + if admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + } + + if let Some(expect) = expect { + let expect = deps.api.addr_validate(&expect)?; + EXPECT.save(deps.storage, &expect)?; + } + + // Instantiate the specified contract with factory as the admin. + let instantiate = WasmMsg::Instantiate2 { + admin: Some(env.contract.address.to_string()), + code_id, + msg: instantiate_msg, + funds: info.funds, + label, + salt, + }; + + let msg = SubMsg::reply_on_success(instantiate, INSTANTIATE2_CONTRACT_REPLY_ID); + Ok(Response::default() + .add_attribute("action", "instantiate2_contract_with_self_admin") .add_submessage(msg)) } @@ -90,10 +138,26 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { - match msg.id { - INSTANTIATE_CONTRACT_REPLY_ID => { + let msg_id = msg.id; + match msg_id { + INSTANTIATE_CONTRACT_REPLY_ID | INSTANTIATE2_CONTRACT_REPLY_ID => { let res = parse_reply_instantiate_data(msg)?; let contract_addr = deps.api.addr_validate(&res.contract_address)?; + + if msg_id == INSTANTIATE2_CONTRACT_REPLY_ID { + // If saved an expected address, verify it matches and clear it. + let expect = EXPECT.may_load(deps.storage)?; + if let Some(expect) = expect { + EXPECT.remove(deps.storage); + if contract_addr != expect { + return Err(ContractError::UnexpectedContractAddress { + expected: expect.to_string(), + actual: contract_addr.to_string(), + }); + } + } + } + // Make the contract its own admin. let msg = WasmMsg::UpdateAdmin { contract_addr: contract_addr.to_string(), diff --git a/contracts/external/cw-admin-factory/src/error.rs b/contracts/external/cw-admin-factory/src/error.rs index 299c8bfe1..83d9eac74 100644 --- a/contracts/external/cw-admin-factory/src/error.rs +++ b/contracts/external/cw-admin-factory/src/error.rs @@ -15,4 +15,7 @@ pub enum ContractError { #[error("An unknown reply ID was received.")] UnknownReplyID {}, + + #[error("Expected contract address {expected} but instantiated {actual}.")] + UnexpectedContractAddress { expected: String, actual: String }, } diff --git a/contracts/external/cw-admin-factory/src/integration_tests.rs b/contracts/external/cw-admin-factory/src/integration_tests.rs new file mode 100644 index 000000000..f5533e0bd --- /dev/null +++ b/contracts/external/cw-admin-factory/src/integration_tests.rs @@ -0,0 +1,249 @@ +use bech32::{decode, encode, FromBase32, ToBase32, Variant}; +use cosmwasm_std::{ + instantiate2_address, to_json_binary, Addr, Binary, CanonicalAddr, Coin, Decimal, +}; +use cw_utils::Duration; +use dao_interface::state::{Admin, ModuleInstantiateInfo}; +use dao_testing::test_tube::{ + cw4_group::Cw4Group, cw_admin_factory::CwAdminFactory, dao_dao_core::DaoCore, + dao_proposal_single::DaoProposalSingle, dao_voting_cw4::DaoVotingCw4, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{PercentageThreshold, Threshold}, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::{ + MsgExecuteContractResponse, QueryCodeRequest, QueryCodeResponse, QueryContractInfoRequest, + QueryContractInfoResponse, + }, + Account, ExecuteResponse, OsmosisTestApp, Runner, RunnerError, +}; + +use cw_admin_factory::msg::ExecuteMsg; + +use crate::ContractError; + +#[test] +fn test_set_self_admin_instantiate2() { + let app = OsmosisTestApp::new(); + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + // get bech32 prefix from created account + let prefix = decode(&accounts[0].address()).unwrap().0; + + let cw_admin_factory = CwAdminFactory::new(&app, None, &accounts[0], &[]).unwrap(); + let dao_dao_core_id = DaoCore::upload(&app, &accounts[0]).unwrap(); + let cw4_group_id = Cw4Group::upload(&app, &accounts[0]).unwrap(); + let dao_voting_cw4_id = DaoVotingCw4::upload(&app, &accounts[0]).unwrap(); + let proposal_single_id = DaoProposalSingle::upload(&app, &accounts[0]).unwrap(); + + // Get DAO core checksum + let dao_core_checksum = app + .query::( + "/cosmwasm.wasm.v1.Query/Code", + &QueryCodeRequest { + code_id: dao_dao_core_id, + }, + ) + .unwrap() + .code_info + .unwrap() + .data_hash; + + 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: dao_voting_cw4_id, + msg: to_json_binary(&dao_voting_cw4::msg::InstantiateMsg { + group_contract: dao_voting_cw4::msg::GroupContract::New { + cw4_group_code_id: cw4_group_id, + initial_members: vec![cw4::Member { + addr: accounts[0].address(), + weight: 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, + }; + + let salt = Binary::from("salt".as_bytes()); + let res: ExecuteResponse = cw_admin_factory + .execute( + &ExecuteMsg::Instantiate2ContractWithSelfAdmin { + instantiate_msg: to_json_binary(&msg).unwrap(), + code_id: dao_dao_core_id, + label: "first".to_string(), + salt: salt.clone(), + expect: None, + }, + &[], + &accounts[0], + ) + .unwrap(); + let core_addr = &res + .events + .iter() + .find(|e| { + e.ty == "instantiate" + && e.attributes + .iter() + .any(|a| a.key == "code_id" && a.value == dao_dao_core_id.to_string()) + }) + .unwrap() + .attributes + .iter() + .find(|a| a.key == "_contract_address") + .unwrap() + .value; + + // Check that admin of core address is itself + let core_admin = app + .query::( + "/cosmwasm.wasm.v1.Query/ContractInfo", + &QueryContractInfoRequest { + address: core_addr.to_string(), + }, + ) + .unwrap() + .contract_info + .unwrap() + .admin; + assert_eq!(&core_admin, core_addr); + + // Check that the address matches the predicted address + let canonical_factory = addr_canonicalize(&prefix, &cw_admin_factory.contract_addr); + let expected_addr = addr_humanize( + &prefix, + &instantiate2_address(&dao_core_checksum, &canonical_factory, salt.as_slice()).unwrap(), + ); + assert_eq!(core_addr, expected_addr.as_str()); + + // Check that it succeeds when expect matches. + let salt = Binary::from("salt_two".as_bytes()); + let expected_addr = addr_humanize( + &prefix, + &instantiate2_address(&dao_core_checksum, &canonical_factory, salt.as_slice()).unwrap(), + ); + let res: ExecuteResponse = cw_admin_factory + .execute( + &ExecuteMsg::Instantiate2ContractWithSelfAdmin { + instantiate_msg: to_json_binary(&msg).unwrap(), + code_id: dao_dao_core_id, + label: "second".to_string(), + salt: salt.clone(), + expect: Some(expected_addr.to_string()), + }, + &[], + &accounts[0], + ) + .unwrap(); + let core_addr = &res + .events + .iter() + .find(|e| { + e.ty == "instantiate" + && e.attributes + .iter() + .any(|a| a.key == "code_id" && a.value == dao_dao_core_id.to_string()) + }) + .unwrap() + .attributes + .iter() + .find(|a| a.key == "_contract_address") + .unwrap() + .value; + assert_eq!(core_addr, expected_addr.as_str()); + + // Check that admin of core address is itself + let core_admin = app + .query::( + "/cosmwasm.wasm.v1.Query/ContractInfo", + &QueryContractInfoRequest { + address: core_addr.clone(), + }, + ) + .unwrap() + .contract_info + .unwrap() + .admin; + assert_eq!(&core_admin, core_addr); + + // Check that it fails when expect does not match. + let salt = Binary::from("salt_mismatch".as_bytes()); + let actual_addr = addr_humanize( + &prefix, + &instantiate2_address(&dao_core_checksum, &canonical_factory, salt.as_slice()).unwrap(), + ); + let err = cw_admin_factory + .execute( + &ExecuteMsg::Instantiate2ContractWithSelfAdmin { + instantiate_msg: to_json_binary(&msg).unwrap(), + code_id: dao_dao_core_id, + label: "third".to_string(), + salt: salt.clone(), + expect: Some(cw_admin_factory.contract_addr.clone()), + }, + &[], + &accounts[0], + ) + .unwrap_err(); + assert_eq!( + err, + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: dispatch: submessages: reply: {}: execute wasm contract failed", + ContractError::UnexpectedContractAddress { + expected: cw_admin_factory.contract_addr.clone(), + actual: actual_addr.to_string(), + } + ) + }, + ); +} + +fn addr_canonicalize(prefix: &str, input: &str) -> CanonicalAddr { + let (p, decoded, variant) = decode(input).unwrap(); + if p == prefix && variant == Variant::Bech32 { + return Vec::::from_base32(&decoded).unwrap().into(); + } + panic!("Invalid address: {}", input); +} + +fn addr_humanize(prefix: &str, canonical: &CanonicalAddr) -> Addr { + let encoded = encode(prefix, canonical.as_slice().to_base32(), Variant::Bech32).unwrap(); + Addr::unchecked(encoded) +} diff --git a/contracts/external/cw-admin-factory/src/lib.rs b/contracts/external/cw-admin-factory/src/lib.rs index d1800adbc..6ea49d00c 100644 --- a/contracts/external/cw-admin-factory/src/lib.rs +++ b/contracts/external/cw-admin-factory/src/lib.rs @@ -8,4 +8,11 @@ pub mod state; #[cfg(test)] 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; + pub use crate::error::ContractError; diff --git a/contracts/external/cw-admin-factory/src/msg.rs b/contracts/external/cw-admin-factory/src/msg.rs index 35a472783..995f75899 100644 --- a/contracts/external/cw-admin-factory/src/msg.rs +++ b/contracts/external/cw-admin-factory/src/msg.rs @@ -9,18 +9,34 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { - /// Instantiates the target contract with the provided instantiate message and code id and - /// updates the contract's admin to be itself. + /// Instantiates the target contract with the provided instantiate message, + /// code ID, and label and updates the contract's admin to be itself. + #[cw_orch(payable)] InstantiateContractWithSelfAdmin { instantiate_msg: Binary, code_id: u64, label: String, }, + /// Instantiates the target contract with the provided instantiate message, + /// code ID, label, and salt, via instantiate2 to give a predictable + /// address, and updates the contract's admin to be itself. + #[cw_orch(payable)] + Instantiate2ContractWithSelfAdmin { + instantiate_msg: Binary, + code_id: u64, + label: String, + salt: Binary, + /// Optionally specify the expected address and fail if it doesn't match + /// the instantiated contract. This makes it easy for a consumer to + /// validate that they are using the correct address elsewhere. + expect: Option, + }, } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(AdminResponse)] Admin {}, diff --git a/contracts/external/cw-admin-factory/src/state.rs b/contracts/external/cw-admin-factory/src/state.rs index 218223e2c..9957a9623 100644 --- a/contracts/external/cw-admin-factory/src/state.rs +++ b/contracts/external/cw-admin-factory/src/state.rs @@ -3,3 +3,6 @@ use cw_storage_plus::Item; /// The account allowed to execute the contract. If None, anyone is allowed. pub const ADMIN: Item> = Item::new("admin"); + +/// The expected instantiate2 address to validate in the reply. +pub const EXPECT: Item = Item::new("expect"); diff --git a/contracts/external/cw-payroll-factory/Cargo.toml b/contracts/external/cw-payroll-factory/Cargo.toml index 9b89969a5..efd300919 100644 --- a/contracts/external/cw-payroll-factory/Cargo.toml +++ b/contracts/external/cw-payroll-factory/Cargo.toml @@ -24,6 +24,7 @@ cw-ownable = { workspace = true } cw-storage-plus = { workspace = true } cw2 = { workspace = true } cw20 = { workspace = true } +cw-orch = { workspace = true } thiserror = { workspace = true } cw-vesting = { workspace = true, features = ["library"] } cw-utils = { workspace = true } 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 964a41762..d9eb0eb32 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-payroll-factory/src/msg.rs b/contracts/external/cw-payroll-factory/src/msg.rs index eacc5357e..64f18ddee 100644 --- a/contracts/external/cw-payroll-factory/src/msg.rs +++ b/contracts/external/cw-payroll-factory/src/msg.rs @@ -11,10 +11,12 @@ pub struct InstantiateMsg { #[cw_ownable_execute] #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Instantiates a new vesting contract that is funded by a cw20 token. Receive(Cw20ReceiveMsg), /// Instantiates a new vesting contract that is funded by a native token. + #[cw_orch(payable)] InstantiateNativePayrollContract { instantiate_msg: PayrollInstantiateMsg, label: String, @@ -36,7 +38,7 @@ pub enum ReceiveMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { /// Returns list of all vesting payment contracts #[returns(Vec)] diff --git a/contracts/external/cw-token-swap/Cargo.toml b/contracts/external/cw-token-swap/Cargo.toml index e974d5453..d10660ac0 100644 --- a/contracts/external/cw-token-swap/Cargo.toml +++ b/contracts/external/cw-token-swap/Cargo.toml @@ -23,6 +23,7 @@ cw-storage-plus = { workspace = true } cw-utils = { workspace = true } cw2 = { workspace = true } cw20 = { workspace = true } +cw-orch = { workspace = true } thiserror = { workspace = true } [dev-dependencies] 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 4d5db2501..0875c942f 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-token-swap/src/msg.rs b/contracts/external/cw-token-swap/src/msg.rs index 1c591d111..a7530bde5 100644 --- a/contracts/external/cw-token-swap/src/msg.rs +++ b/contracts/external/cw-token-swap/src/msg.rs @@ -32,10 +32,12 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Used to provide cw20 tokens to satisfy a funds promise. Receive(cw20::Cw20ReceiveMsg), /// Provides native tokens to satisfy a funds promise. + #[cw_orch(payable)] Fund {}, /// Withdraws provided funds. Only allowed if the other /// counterparty has yet to provide their promised funds. @@ -43,7 +45,7 @@ pub enum ExecuteMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { // Gets the current status of the escrow transaction. #[returns(crate::msg::StatusResponse)] diff --git a/contracts/external/cw-tokenfactory-issuer/Cargo.toml b/contracts/external/cw-tokenfactory-issuer/Cargo.toml index 371f4a2e0..b7d14fc45 100644 --- a/contracts/external/cw-tokenfactory-issuer/Cargo.toml +++ b/contracts/external/cw-tokenfactory-issuer/Cargo.toml @@ -44,12 +44,14 @@ kujira_tokenfactory = ["cw-tokenfactory-types/kujira_tokenfactory"] cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw2 = { workspace = true } +cw-orch = { workspace = true } cw-ownable = { workspace = true } cw-storage-plus = { workspace = true } cw-tokenfactory-types = { workspace = true, default-features = false } dao-interface = { workspace = true } osmosis-std = { workspace = true } prost = { workspace = true } +prost-derive = { workspace = true } schemars = { workspace = true } serde = { workspace = true, default-features = false, features = ["derive"] } thiserror = { workspace = true } diff --git a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json index 2ff6b757c..6b5e40a2a 100644 --- a/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json +++ b/contracts/external/cw-tokenfactory-issuer/schema/cw-tokenfactory-issuer.json @@ -1,6 +1,6 @@ { "contract_name": "cw-tokenfactory-issuer", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-tokenfactory-issuer/src/msg.rs b/contracts/external/cw-tokenfactory-issuer/src/msg.rs index 0660ecd51..6170cddc8 100644 --- a/contracts/external/cw-tokenfactory-issuer/src/msg.rs +++ b/contracts/external/cw-tokenfactory-issuer/src/msg.rs @@ -23,6 +23,7 @@ pub enum InstantiateMsg { /// State changing methods available to this smart contract. #[cw_serde] +#[derive(cw_orch::ExecuteFns)] 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 @@ -108,7 +109,7 @@ pub struct MigrateMsg {} /// Queries supported by this smart contract. #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { /// Returns if token transfer is disabled. Response: IsFrozenResponse #[returns(IsFrozenResponse)] diff --git a/contracts/external/cw-tokenfactory-issuer/tests/mod.rs b/contracts/external/cw-tokenfactory-issuer/tests/mod.rs index 73b60899d..8ad6cfb23 100644 --- a/contracts/external/cw-tokenfactory-issuer/tests/mod.rs +++ b/contracts/external/cw-tokenfactory-issuer/tests/mod.rs @@ -1,7 +1,3 @@ -// 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")] diff --git a/contracts/external/cw-vesting/Cargo.toml b/contracts/external/cw-vesting/Cargo.toml index 2689de7de..2f5204551 100644 --- a/contracts/external/cw-vesting/Cargo.toml +++ b/contracts/external/cw-vesting/Cargo.toml @@ -24,6 +24,7 @@ staking = ["cosmwasm-std/staking"] cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } cw-denom = { workspace = true } +cw-orch = { workspace = true } cw-ownable = { workspace = true } cw-stake-tracker = { workspace = true } cw-storage-plus = { workspace = true } diff --git a/contracts/external/cw-vesting/schema/cw-vesting.json b/contracts/external/cw-vesting/schema/cw-vesting.json index d10586ec4..d26c50748 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw-vesting/src/msg.rs b/contracts/external/cw-vesting/src/msg.rs index 5aac6ca17..a9a89e0ae 100644 --- a/contracts/external/cw-vesting/src/msg.rs +++ b/contracts/external/cw-vesting/src/msg.rs @@ -62,6 +62,7 @@ pub struct InstantiateMsg { #[cw_ownable_execute] #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Fund the contract with a cw20 token. The `msg` field must have /// the shape `{"fund":{}}`, and the amount sent must be the same @@ -194,7 +195,7 @@ pub enum ReceiveMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { /// Get the current ownership. #[returns(::cw_ownable::Ownership<::cosmwasm_std::Addr>)] diff --git a/contracts/external/cw-vesting/src/suite_tests/suite.rs b/contracts/external/cw-vesting/src/suite_tests/suite.rs index 401aeb8f3..7558b9ad9 100644 --- a/contracts/external/cw-vesting/src/suite_tests/suite.rs +++ b/contracts/external/cw-vesting/src/suite_tests/suite.rs @@ -170,7 +170,7 @@ impl Suite { } pub fn process_unbonds(&mut self) { - self.app.sudo(StakingSudo::ProcessQueue {}.into()).unwrap(); + self.app.update_block(|_| {}); } } diff --git a/contracts/external/cw721-roles/Cargo.toml b/contracts/external/cw721-roles/Cargo.toml index 1a6935974..c5a4ee1d6 100644 --- a/contracts/external/cw721-roles/Cargo.toml +++ b/contracts/external/cw721-roles/Cargo.toml @@ -25,6 +25,7 @@ cw-storage-plus = { workspace = true } cw-utils = { workspace = true } cw2 = { workspace = true } cw4 = { workspace = true } +cw-orch = { workspace = true } cw721 = { workspace = true } cw721-base = { workspace = true, features = ["library"] } dao-cw721-extensions = { workspace = true } diff --git a/contracts/external/cw721-roles/schema/cw721-roles.json b/contracts/external/cw721-roles/schema/cw721-roles.json index 27d75ef6c..d9a254380 100644 --- a/contracts/external/cw721-roles/schema/cw721-roles.json +++ b/contracts/external/cw721-roles/schema/cw721-roles.json @@ -1,6 +1,6 @@ { "contract_name": "cw721-roles", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/external/cw721-roles/src/contract.rs b/contracts/external/cw721-roles/src/contract.rs index ea31936a1..e3f07190f 100644 --- a/contracts/external/cw721-roles/src/contract.rs +++ b/contracts/external/cw721-roles/src/contract.rs @@ -326,7 +326,7 @@ pub fn execute_update_token_role( .add_attribute("action", "update_token_role") .add_attribute("sender", info.sender) .add_attribute("token_id", token_id) - .add_attribute("role", role.unwrap_or_default())) + .add_attribute("role", role.unwrap_or("None".to_string()))) } pub fn execute_update_token_uri( @@ -348,7 +348,7 @@ pub fn execute_update_token_uri( .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())) + .add_attribute("token_uri", token_uri.unwrap_or("None".to_string()))) } pub fn execute_update_token_weight( diff --git a/contracts/external/cw721-roles/src/tests.rs b/contracts/external/cw721-roles/src/tests.rs index 005c2e61f..41c559413 100644 --- a/contracts/external/cw721-roles/src/tests.rs +++ b/contracts/external/cw721-roles/src/tests.rs @@ -3,7 +3,7 @@ use cw4::{HooksResponse, Member, MemberListResponse, MemberResponse, TotalWeight 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_testing::contracts::{cw721_roles_contract, cw721_staked_voting_contract}; use dao_voting_cw721_staked::msg::{InstantiateMsg as Cw721StakedInstantiateMsg, NftContract}; use crate::error::RolesContractError; @@ -254,7 +254,7 @@ fn test_send_permissions() { .unwrap(); // Instantiate an NFT staking voting contract for testing SendNft - let dao_voting_cw721_staked_id = app.store_code(voting_cw721_staked_contract()); + let dao_voting_cw721_staked_id = app.store_code(cw721_staked_voting_contract()); let cw721_staked_addr = app .instantiate_contract( dao_voting_cw721_staked_id, @@ -325,6 +325,20 @@ fn test_update_token_role() { // Token was updated successfully let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); assert_eq!(info.extension.role, Some("queen".to_string())); + + // Can set to None + app.execute_contract( + Addr::unchecked(DAO), + cw721_addr.clone(), + &ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenRole { + token_id: "1".to_string(), + role: None, + }, + }, + &[], + ) + .unwrap(); } #[test] diff --git a/contracts/external/dao-migrator/Cargo.toml b/contracts/external/dao-migrator/Cargo.toml index 4d2b48cb8..557a6a3a6 100644 --- a/contracts/external/dao-migrator/Cargo.toml +++ b/contracts/external/dao-migrator/Cargo.toml @@ -24,6 +24,7 @@ cw-utils = { workspace = true } thiserror = { workspace = true } cw2 = { workspace = true } cw20 = { workspace = true } +cw-orch = { workspace = true } dao-interface = { workspace = true } dao-dao-core = { workspace = true, features = ["library"] } @@ -31,8 +32,8 @@ dao-voting = { workspace = true } dao-proposal-single = { workspace = true, features = ["library"] } dao-voting-cw4 = { workspace = true, features = ["library"] } cw20-stake = { workspace = true, features = ["library"] } -dao-voting-cw20-staked = { workspace = true, features = ["library"] } cw20-base = { workspace = true, features = ["library"] } +dao-voting-cw20-staked = { workspace = true, features = ["library"] } cw-utils-v1 = { workspace = true } voting-v1 = { workspace = true } diff --git a/contracts/external/dao-migrator/README.md b/contracts/external/dao-migrator/README.md index f61a3bb52..ba51df374 100644 --- a/contracts/external/dao-migrator/README.md +++ b/contracts/external/dao-migrator/README.md @@ -5,7 +5,7 @@ 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 +A migrator module for a DAO DAO DAO which handles migration for DAO modules and test it went successfully. DAO core migration is handled by a proposal, which adds this module and do @@ -14,6 +14,7 @@ If custom module is found, this TX fails and migration is cancelled, custom module requires a custom migration to be done by the DAO. # General idea + 1. Proposal is made to migrate DAO core to V2, which also adds this module to the DAO. 2. On init of this contract, a callback is fired to do the migration. 3. Then we check to make sure the DAO doesn't have custom modules. @@ -23,9 +24,10 @@ module requires a custom migration to be done by the DAO. 7. In any case where 1 migration fails, we fail the whole TX. # Important notes -* custom modules cannot reliably be migrated by this contract, -because of that we fail the process to avoid any unwanted results. -* If any module migration fails we fail the whole thing, -this is to make sure that we either have a fully working V2, -or we do nothing and make sure the DAO is operational at any time. \ No newline at end of file +- custom modules cannot reliably be migrated by this contract, + because of that we fail the process to avoid any unwanted results. + +- If any module migration fails we fail the whole thing, + this is to make sure that we either have a fully working V2, + or we do nothing and make sure the DAO is operational at any time. diff --git a/contracts/external/dao-migrator/schema/dao-migrator.json b/contracts/external/dao-migrator/schema/dao-migrator.json index 828c241ef..62f9371fc 100644 --- a/contracts/external/dao-migrator/schema/dao-migrator.json +++ b/contracts/external/dao-migrator/schema/dao-migrator.json @@ -1,6 +1,6 @@ { "contract_name": "dao-migrator", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/gauges/gauge-adapter/Cargo.toml b/contracts/gauges/gauge-adapter/Cargo.toml index f8989d57d..bfde8bcfb 100644 --- a/contracts/gauges/gauge-adapter/Cargo.toml +++ b/contracts/gauges/gauge-adapter/Cargo.toml @@ -38,4 +38,4 @@ anyhow = { workspace = true } cw-multi-test = { workspace = true } cw20 = { workspace = true } cw20-base = { workspace = true } -cw-orch-core = { workspace = true } +scripts = { path = "../../../scripts" } diff --git a/contracts/gauges/gauge-adapter/src/lib.rs b/contracts/gauges/gauge-adapter/src/lib.rs index aae805d76..cf9083933 100644 --- a/contracts/gauges/gauge-adapter/src/lib.rs +++ b/contracts/gauges/gauge-adapter/src/lib.rs @@ -3,8 +3,4 @@ mod error; mod helpers; pub mod msg; pub mod state; - -#[cfg(test)] -mod multitest; - pub use crate::error::ContractError; diff --git a/contracts/gauges/gauge-adapter/src/multitest/mod.rs b/contracts/gauges/gauge-adapter/src/multitest/mod.rs deleted file mode 100644 index e5a2ee02d..000000000 --- a/contracts/gauges/gauge-adapter/src/multitest/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod suite; - -mod options; -mod submission; diff --git a/contracts/gauges/gauge-adapter/src/multitest/options.rs b/contracts/gauges/gauge-adapter/src/multitest/options.rs deleted file mode 100644 index 2d6343cff..000000000 --- a/contracts/gauges/gauge-adapter/src/multitest/options.rs +++ /dev/null @@ -1,80 +0,0 @@ -use cosmwasm_std::{coin, coins}; -use cw_denom::UncheckedDenom; -use cw_orch::{contract::interface_traits::CwOrchQuery, mock::MockBech32}; - -use crate::{ - msg::{ - AdapterQueryMsg, AllOptionsResponse, AllSubmissionsResponse, AssetUnchecked, - CheckOptionResponse, - }, - multitest::suite::{native_submission_helper, setup_gauge_adapter}, -}; - -#[test] -fn option_queries() { - let mock = MockBech32::new("mock"); - let adapter = setup_gauge_adapter( - mock.clone(), - Some(AssetUnchecked { - denom: UncheckedDenom::Native("juno".into()), - amount: 1_000u128.into(), - }), - ); - - let recipient = mock.addr_make("recipient"); - let newton = mock.addr_make("newton"); - let einstein = mock - .addr_make_with_balance("einstein", coins(1_000, "juno")) - .unwrap(); - - mock.add_balance(&mock.sender, coins(1_000, "juno")) - .unwrap(); - let options: AllSubmissionsResponse = - adapter.query(&AdapterQueryMsg::AllSubmissions {}).unwrap(); - // account for a default option - assert_eq!(options.submissions.len(), 1); - - // Valid submission. - native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - Some(coin(1_000u128, "juno")), - ) - .unwrap(); - - // Valid submission. - native_submission_helper( - adapter.clone(), - einstein.clone(), - einstein.clone(), - Some(coin(1_000u128, "juno")), - ) - .unwrap(); - - let options: AllOptionsResponse = adapter.query(&AdapterQueryMsg::AllOptions {}).unwrap(); - assert_eq!( - options, - AllOptionsResponse { - options: vec![ - einstein.to_string(), - mock.addr_make("community_pool").to_string(), - recipient.to_string() - ] - }, - ); - - let option: CheckOptionResponse = adapter - .query(&AdapterQueryMsg::CheckOption { - option: einstein.to_string(), - }) - .unwrap(); - assert!(option.valid); - - let option: CheckOptionResponse = adapter - .query(&AdapterQueryMsg::CheckOption { - option: newton.to_string(), - }) - .unwrap(); - assert!(!option.valid); -} diff --git a/contracts/gauges/gauge-adapter/src/multitest/submission.rs b/contracts/gauges/gauge-adapter/src/multitest/submission.rs deleted file mode 100644 index 41f8a88cb..000000000 --- a/contracts/gauges/gauge-adapter/src/multitest/submission.rs +++ /dev/null @@ -1,586 +0,0 @@ -use crate::{ - msg::{ - AdapterQueryMsg, AdapterQueryMsgFns, AllSubmissionsResponse, AssetUnchecked, ExecuteMsg, - ExecuteMsgFns, ReceiveMsg, SubmissionResponse, - }, - multitest::suite::{ - cw20_helper, native_submission_helper, setup_cw20_reward_gauge_adapter, setup_gauge_adapter, - }, - ContractError, -}; - -use abstract_cw20::{msg::Cw20ExecuteMsgFns, Cw20ExecuteMsg as AbsCw20ExecuteMsg}; -use abstract_cw20_base::msg::QueryMsgFns; -use cosmwasm_std::{coin, to_json_binary, Addr, CosmosMsg, Decimal, Uint128, WasmMsg}; -use cw_denom::UncheckedDenom; -use cw_orch::{contract::interface_traits::CwOrchExecute, mock::MockBech32, prelude::*}; - -#[test] -fn create_default_submission() { - let mock = MockBech32::new("mock"); - let treasury = &mock.addr_make("community_pool"); - - let adapter = setup_gauge_adapter(mock.clone(), None); - - // this one is created by default during instantiation - assert_eq!( - SubmissionResponse { - sender: adapter.address().unwrap(), - name: "Unimpressed".to_owned(), - url: "Those funds go back to the community pool".to_owned(), - address: treasury.clone(), - }, - adapter - .query(&crate::msg::AdapterQueryMsg::Submission { - address: treasury.to_string() - }) - .unwrap() - ) -} - -#[test] -fn create_submission_no_required_deposit() { - let mock = MockBech32::new("mock"); - let adapter = setup_gauge_adapter(mock.clone(), None); - - let recipient = mock.addr_make("recipient"); - mock.add_balance(&mock.sender, vec![coin(1_000, "juno")]) - .unwrap(); - - // Fails send funds along with the tx. - let err = native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - Some(coin(1_000, "juno")), - ) - .unwrap_err(); - - assert_eq!( - ContractError::InvalidDepositAmount { - correct_amount: Uint128::zero() - }, - err.downcast().unwrap() - ); - - // Valid submission. - let result = native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - None, - ); - assert!(result.is_ok()); - - assert_eq!( - SubmissionResponse { - sender: mock.sender, - name: "DAOers".to_owned(), - url: "https://daodao.zone".to_owned(), - address: recipient.clone(), - }, - adapter - .query(&crate::msg::AdapterQueryMsg::Submission { - address: recipient.to_string() - }) - .unwrap(), - ) -} - -#[test] -fn overwrite_existing_submission() { - let mock = MockBech32::new("mock"); - let adapter = setup_gauge_adapter(mock.clone(), None); - let recipient = mock.addr_make("recipient"); - native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - None, - ) - .unwrap(); - - assert_eq!( - SubmissionResponse { - sender: mock.sender.clone(), - name: "DAOers".to_owned(), - url: "https://daodao.zone".to_string(), - address: recipient.clone(), - }, - adapter.submission(recipient.to_string()).unwrap() - ); - - // Try to submit to the same address with different user - let err = native_submission_helper( - adapter.clone(), - Addr::unchecked("anotheruser"), - recipient.clone(), - None, - ) - .unwrap_err(); - - assert_eq!( - ContractError::UnauthorizedSubmission {}, - err.downcast().unwrap() - ); - - // Overwriting submission as same author works - native_submission_helper(adapter.clone(), mock.sender, recipient.clone(), None).unwrap(); - - let response = adapter.submission(recipient.to_string()).unwrap(); - assert_eq!(response.url, "https://daodao.zone".to_owned()); -} - -#[test] -fn create_submission_required_deposit() { - let mock = MockBech32::new("mock"); - let adapter = setup_gauge_adapter( - mock.clone(), - Some(AssetUnchecked { - denom: UncheckedDenom::Native("juno".into()), - amount: 1_000u128.into(), - }), - ); - - let recipient = mock.addr_make("recipient"); - mock.add_balance(&mock.sender.clone(), vec![coin(1_000, "wynd")]) - .unwrap(); - mock.add_balance(&mock.sender.clone(), vec![coin(1_000, "juno")]) - .unwrap(); - - // Fails if no funds sent. - let err = native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - None, - ) - .unwrap_err(); - - assert_eq!( - ContractError::PaymentError(cw_utils::PaymentError::NoFunds {}), - err.downcast().unwrap() - ); - - // Fails if correct denom but not enough amount. - // Fails if no funds sent. - let err = native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - Some(Coin { - denom: "juno".into(), - amount: 999u128.into(), - }), - ) - .unwrap_err(); - - assert_eq!( - ContractError::InvalidDepositAmount { - correct_amount: Uint128::new(1_000) - }, - err.downcast().unwrap() - ); - - // Fails if enough amount but incorrect denom. - let err = native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - Some(Coin { - denom: "wynd".into(), - amount: 1_000u128.into(), - }), - ) - .unwrap_err(); - - assert_eq!( - ContractError::InvalidDepositType {}, - err.downcast().unwrap() - ); - - // Valid submission. - native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - Some(Coin { - denom: "juno".into(), - amount: 1_000u128.into(), - }), - ) - .unwrap(); - - assert_eq!( - SubmissionResponse { - sender: mock.sender.clone(), - name: "DAOers".to_owned(), - url: "https://daodao.zone".to_owned(), - address: recipient.clone(), - }, - adapter - .query(&AdapterQueryMsg::Submission { - address: recipient.to_string() - }) - .unwrap() - ) -} - -#[test] -fn create_receive_required_deposit() { - let mock = MockBech32::new("mock"); - let cw20 = cw20_helper(mock.clone()); - let bad_cw20 = cw20_helper(mock.clone()); - let cw20_addr = cw20.address().unwrap(); - let bad_cw20_addr = bad_cw20.address().unwrap(); - println!("good cw20: {:#?}", cw20_addr); - println!("bad cw20: {:#?}", bad_cw20_addr); - let adapter = setup_gauge_adapter( - mock.clone(), - Some(AssetUnchecked { - denom: UncheckedDenom::Cw20(cw20_addr.to_string()), - amount: 1_000u128.into(), - }), - ); - - let recipient = mock.sender_addr().to_string(); - - let binary_msg = to_json_binary(&ReceiveMsg::CreateSubmission { - name: "DAOers".into(), - url: "https://daodao.zone".into(), - address: recipient.clone(), - }) - .unwrap(); - // Fails by sending wrong cw20. - let err = adapter - .call_as(&Addr::unchecked( - "mock1mzdhwvvh22wrt07w59wxyd58822qavwkx5lcej7aqfkpqqlhaqfsetqc4t", - )) - .execute( - &ExecuteMsg::Receive(cw20::Cw20ReceiveMsg { - sender: recipient.to_string(), - amount: Uint128::from(1_000u128), - msg: binary_msg.clone(), - }), - None, - ) - .unwrap_err(); - - assert_eq!( - ContractError::InvalidDepositType {}, - err.downcast().unwrap(), - ); - - // Fails by sending less tokens than required. - let err = adapter - .call_as(&cw20.address().unwrap()) - .execute( - &ExecuteMsg::Receive(cw20::Cw20ReceiveMsg { - sender: recipient.to_string(), - amount: Uint128::from(999u128), - msg: binary_msg.clone(), - }), - None, - ) - .unwrap_err(); - - assert_eq!( - ContractError::InvalidDepositAmount { - correct_amount: Uint128::new(1_000) - }, - err.downcast().unwrap() - ); - - // Valid submission. - adapter - .call_as(&cw20.address().unwrap()) - .execute( - &ExecuteMsg::Receive(cw20::Cw20ReceiveMsg { - sender: recipient.to_string(), - amount: Uint128::from(1_000u128), - msg: binary_msg, - }), - None, - ) - .unwrap(); - - assert_eq!( - SubmissionResponse { - sender: mock.sender.clone(), - name: "DAOers".to_owned(), - url: "https://daodao.zone".to_owned(), - address: Addr::unchecked(recipient.clone()), - }, - adapter - .query(&AdapterQueryMsg::Submission { - address: recipient.to_string() - }) - .unwrap() - ); - - assert_eq!( - 2, - adapter - .query::(&AdapterQueryMsg::AllSubmissions {}) - .unwrap() - .submissions - .len() - ) -} - -#[test] -fn return_deposits_no_required_deposit() { - let mock = MockBech32::new("mock"); - let adapter = setup_gauge_adapter(mock.clone(), None); - - let err = adapter - .execute(&ExecuteMsg::ReturnDeposits {}, None) - .unwrap_err(); - - assert_eq!(ContractError::NoDepositToRefund {}, err.downcast().unwrap()) -} - -#[test] -fn return_deposits_no_admin() { - let mock = MockBech32::new("mock"); - let bad_addr = mock.addr_make("einstien"); - let adapter = setup_gauge_adapter( - mock.clone(), - Some(AssetUnchecked { - denom: UncheckedDenom::Native("juno".into()), - amount: 1_000u128.into(), - }), - ); - - let err = adapter - .call_as(&bad_addr) - .execute(&ExecuteMsg::ReturnDeposits {}, None) - .unwrap_err(); - - assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()) -} - -#[test] -fn return_deposits_required_native_deposit() { - let mock = MockBech32::new("mock"); - let adapter = setup_gauge_adapter( - mock.clone(), - Some(AssetUnchecked { - denom: UncheckedDenom::Native("juno".into()), - amount: 1_000u128.into(), - }), - ); - mock.add_balance(&mock.sender, vec![coin(1_000u128, "juno")]) - .unwrap(); - let recipient = mock.addr_make("recipient"); - - // Valid submission. - native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - Some(coin(1_000u128, "juno")), - ) - .unwrap(); - - assert_eq!( - mock.query_balance(&mock.sender.clone(), "juno").unwrap(), - Uint128::zero() - ); - assert_eq!( - mock.query_balance(&recipient, "juno").unwrap(), - Uint128::zero() - ); - assert_eq!( - mock.query_balance(&adapter.address().unwrap(), "juno") - .unwrap(), - Uint128::from(1000u128) - ); - - adapter - .execute(&ExecuteMsg::ReturnDeposits {}, None) - .unwrap(); - assert_eq!( - mock.query_balance(&mock.sender.clone(), "juno").unwrap(), - Uint128::from(1000u128) - ); - assert_eq!( - mock.query_balance(&recipient, "juno").unwrap(), - Uint128::zero() - ); - assert_eq!( - mock.query_balance(&adapter.address().unwrap(), "juno") - .unwrap(), - Uint128::zero() - ); -} - -#[test] -fn return_deposits_required_native_deposit_multiple_deposits() { - let mock = MockBech32::new("mock"); - let adapter = setup_gauge_adapter( - mock.clone(), - Some(AssetUnchecked { - denom: UncheckedDenom::Native("juno".into()), - amount: 1_000u128.into(), - }), - ); - - let recipient = mock.addr_make("recipient"); - let einstien = mock - .addr_make_with_balance("einstien", vec![coin(1_000u128, "juno")]) - .unwrap(); - mock.add_balance(&mock.sender, vec![coin(1_000u128, "juno")]) - .unwrap(); - // Valid submission. - native_submission_helper( - adapter.clone(), - mock.sender.clone(), - recipient.clone(), - Some(coin(1_000u128, "juno")), - ) - .unwrap(); - // Valid submission. - native_submission_helper( - adapter.clone(), - einstien.clone(), - einstien.clone(), - Some(coin(1_000u128, "juno")), - ) - .unwrap(); - - adapter.return_deposits().unwrap(); - assert_eq!( - mock.query_balance(&mock.sender.clone(), "juno").unwrap(), - Uint128::from(1000u128) - ); - assert_eq!( - mock.query_balance(&einstien, "juno").unwrap(), - Uint128::from(1000u128) - ); - assert_eq!( - mock.query_balance(&recipient, "juno").unwrap(), - Uint128::zero() - ); - assert_eq!( - mock.query_balance(&adapter.address().unwrap(), "juno") - .unwrap(), - Uint128::zero() - ); -} - -#[test] -fn return_deposits_required_cw20_deposit() { - let mock = MockBech32::new("mock"); - let cw20 = cw20_helper(mock.clone()); - let recipient = mock.addr_make("recipient"); - let adapter = setup_gauge_adapter( - mock.clone(), - Some(AssetUnchecked { - denom: UncheckedDenom::Cw20(cw20.addr_str().unwrap()), - amount: 1_000u128.into(), - }), - ); - let binary_msg = to_json_binary(&ReceiveMsg::CreateSubmission { - name: "DAOers".into(), - url: "https://daodao.zone".into(), - address: recipient.to_string(), - }) - .unwrap(); - - // Valid submission. - cw20.send(1_000u128.into(), adapter.addr_str().unwrap(), binary_msg) - .unwrap(); - - assert_eq!( - cw20.balance(mock.sender.to_string()).unwrap().balance, - Uint128::from(999_000u128) - ); - assert_eq!( - cw20.balance(recipient.to_string()).unwrap().balance, - Uint128::zero() - ); - assert_eq!( - cw20.balance(adapter.address().unwrap().to_string()) - .unwrap() - .balance, - Uint128::from(1_000u128), - ); - - adapter.return_deposits().unwrap(); - - assert_eq!( - cw20.balance(mock.sender.to_string()).unwrap().balance, - Uint128::from(1_000_000u128), - ); - // Tokens are sent back to submission sender, not recipient. - assert_eq!( - cw20.balance(recipient.to_string()).unwrap().balance, - Uint128::zero(), - ); - assert_eq!( - cw20.balance(adapter.address().unwrap().to_string()) - .unwrap() - .balance, - Uint128::zero(), - ); -} - -#[test] -fn sample_gauge_msgs_cw20() { - let mock = MockBech32::new("mock"); - let addr_1 = mock.addr_make("addr1"); - let addr_2 = mock.addr_make("addr2"); - let addr_3 = mock.addr_make("addr3"); - let reward = Uint128::new(1_000_000); - let (adapter, cw20) = setup_cw20_reward_gauge_adapter(mock.clone(), None); - - adapter - .create_submission(addr_1.to_string(), "name".into(), "https://test.url".into()) - .unwrap(); - adapter - .create_submission(addr_2.to_string(), "name".into(), "https://test.url".into()) - .unwrap(); - - let selected = vec![ - (addr_1.to_string(), Decimal::percent(41)), - (addr_2.to_string(), Decimal::percent(33)), - (addr_3.to_string(), Decimal::percent(26)), - ]; - - let res: crate::msg::SampleGaugeMsgsResponse = - adapter.sample_gauge_msgs(selected.clone()).unwrap(); - assert_eq!(res.execute.len(), 3); - assert_eq!( - res.execute, - [ - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: cw20.addr_str().unwrap(), - msg: to_json_binary(&AbsCw20ExecuteMsg::Transfer { - recipient: addr_1.to_string(), - amount: reward * Decimal::percent(41) - }) - .unwrap(), - funds: vec![] - }), - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: cw20.addr_str().unwrap(), - msg: to_json_binary(&AbsCw20ExecuteMsg::Transfer { - recipient: addr_2.to_string(), - amount: reward * Decimal::percent(33) - }) - .unwrap(), - funds: vec![] - }), - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: cw20.addr_str().unwrap(), - msg: to_json_binary(&AbsCw20ExecuteMsg::Transfer { - recipient: addr_3.to_string(), - amount: reward * Decimal::percent(26) - }) - .unwrap(), - funds: vec![] - }), - ] - ); -} diff --git a/contracts/gauges/gauge/Cargo.toml b/contracts/gauges/gauge/Cargo.toml index 6cbc8a550..bc4034a8d 100644 --- a/contracts/gauges/gauge/Cargo.toml +++ b/contracts/gauges/gauge/Cargo.toml @@ -19,6 +19,7 @@ library = [] cosmwasm-schema = { workspace = true } cosmwasm-std = { workspace = true } dao-interface = { workspace = true } +cw-orch = { workspace = true } cw-storage-plus = { workspace = true } cw-utils = { workspace = true } cw2 = { workspace = true } diff --git a/contracts/gauges/gauge/README.md b/contracts/gauges/gauge/README.md index ef87daf0c..096779309 100644 --- a/contracts/gauges/gauge/README.md +++ b/contracts/gauges/gauge/README.md @@ -5,17 +5,15 @@ when we need to select a weighted group out of a larger group of options. ## Orchestrator -To work properly, the gauge must be informed every time that the voting power of a member changes. -It does so by listening to "update hooks" on the underlying staking contract and if an address's -voting power changes, updating their vote weight in the gauge, and the tally for the option they -had voted for (if any). +### Why is there an Gauge Orchestrator +To work properly, **a gauge must be informed every time that the voting power of a member changes.** It does so by listening for a "update hooks" msg from the underlying staking contract and if an address's voting power changes, updating their vote weight in the gauge, and the tally for the option they had voted for (if any) is done by the gauge. +#### Staking Hooks Gas Cost Every contract call has some overhead, which is silently added to the basic staking action. - If we have 5 gauges in a DAO, we would likely have a minimum of 5 x 65k or 325k gas per staking action, just to update gauges. This is a lot of overhead, and we want to avoid it. -To do so, we make one "Gauge Orchestrator", which can manage many different gauges. They all have the -same voting logic and rules to update when the voting power changes. The Orchestrator is the only +#### Gauge Orchestrator & Adapters +To do so, we make use of one "Gauge Orchestrator", which will manage many different "Gauge Adapters". Each gauge has its own gauge-adapter, but will use the same voting logic and rules to update when the voting power changes and ever. The Orchestrator is the only contract that must be called by the staking contract, and doing a few writes for each gauge is a lot cheaper gas-wise than calling a separate contract. @@ -31,17 +29,26 @@ that before adding to the new one. When an "update hook" is triggered, it update Every epoch (eg 1/week), the current tally of the gauge is sampled, and some cut-off applies (top 20, min 0.5% of votes, etc). The resulting set is the "selected set" and the options along with their relative vote counts (normalized to 1.0 = total votes within this set) is used to initiate some -action (eg. distribute reward tokens). +action (eg. distribute reward tokens). A Gauge may have a maximum number of epochs set to operator for until it no loger will operate. + -## Extensibility +Gauge Config | Description | Type | +--- | --- | --- | +Title | Title of gauge | String | +Adapter | Contract address of gauge adapter | String | +Epoch | Seconds between gauge processing messages | u64 | +Minimum % Selected | Optional, minimum percentages of votes needed for an option to be in the selected set| Decimal | +Max Options Selected | Maximum options able to make the selected set| u64 | +Max Available % | Optional,maximim % Threshold for each options| Decimal | +Reset Epoch | Optional, seconds between gauge being reset | u64| +Total Epochs | Optional, number of times gauge will run | u64 | -We will be using one Orchestrator for many different gauges that update many different contracts. -To make it more extensible, we define option as an arbitrary string that makes sense to that contract. +## Gauge Adapter Requirements +We will be using one Orchestrator for many different gauges, making use of many different `GaugeAdapters`. -We also store the integration logic in an external contract, called a `GaugeAdapter` that must provide -3 queries to the Orchestrator: +A `GaugeAdapter` must provide the following 3 queries in order to be supported with the Gauge Orchestrator: -* Provide set of all options: maybe expensive, iterate over all and return them. This is used for initialization. +* A set of all options: maybe expensive, iterate over all and return them. This is used for initialization. * Check an option: Allow anyone to propose one, and this confirms if it is valid (eg is this a valid address of a registered AMM pool?) * Create update messages: Accepts "selected set" as argument, returns `Vec` to be executed by the @@ -58,6 +65,8 @@ and instantiates a properly configured Adapter. Then, it votes to create a new Gauge that uses this adapter. Upon creating the gauge, it will query the adapter for the current set of options to initialize state. +The voting module of that DAO must be registered to reflect voting weight changes in the DAO. + After one epoch has passed, anyone can trigger `Execute` on this gauge ID, and the Orchestrator will apply the logic to determine the "selected set". It will then query the adapter for the messages needed to convert that selection into the appropriate action, and it will send those to the diff --git a/contracts/gauges/gauge/src/contract.rs b/contracts/gauges/gauge/src/contract.rs index 20ca4faec..41ad6d623 100644 --- a/contracts/gauges/gauge/src/contract.rs +++ b/contracts/gauges/gauge/src/contract.rs @@ -72,6 +72,7 @@ pub fn execute( ExecuteMsg::CreateGauge(options) => execute::create_gauge(deps, env, info.sender, options), ExecuteMsg::UpdateGauge { gauge_id, + epoch_limit, epoch_size, min_percent_selected, max_options_selected, @@ -81,6 +82,7 @@ pub fn execute( info.sender, gauge_id, epoch_size, + epoch_limit, min_percent_selected, max_options_selected, max_available_percentage, @@ -103,11 +105,15 @@ pub fn execute( } mod execute { + use cosmwasm_std::CosmosMsg; use cw4::MemberDiff; use dao_hooks::{nft_stake::NftStakeChangedHookMsg, stake::StakeChangedHookMsg}; use super::*; - use crate::state::{remove_tally, update_tallies, Reset, Vote}; + use crate::{ + msg::CreateGaugeReply, + state::{remove_tally, update_tallies, Reset, Vote}, + }; use std::collections::HashMap; pub fn member_changed( @@ -375,7 +381,8 @@ mod execute { Ok(Response::new() .add_attribute("action", "create_gauge") - .add_attribute("adapter", adapter)) + .add_attribute("gauge-id", adapter.id.to_string()) + .add_attribute("adapter", adapter.addr)) } pub fn attach_gauge( @@ -389,8 +396,9 @@ mod execute { max_options_selected, max_available_percentage, reset_epoch, + total_epochs, }: GaugeConfig, - ) -> Result { + ) -> Result { let adapter = deps.api.addr_validate(&adapter)?; // gauge parameter validation ensure!(epoch_size > 60u64, ContractError::EpochSizeTooShort {}); @@ -408,6 +416,7 @@ mod execute { title, adapter: adapter.clone(), epoch: epoch_size, + count: Some(0), min_percent_selected, max_options_selected, max_available_percentage, @@ -419,6 +428,7 @@ mod execute { reset_each: r, next: env.block.time.plus_seconds(r).seconds(), }), + total_epoch: total_epochs, }; let last_id: GaugeId = fetch_last_id(deps.storage)?; GAUGES.save(deps.storage, last_id, &gauge)?; @@ -434,13 +444,17 @@ mod execute { Ok::<_, ContractError>(()) })?; - Ok(adapter) + Ok(CreateGaugeReply { + id: last_id, + addr: adapter.to_string(), + }) } pub fn update_gauge( deps: DepsMut, sender: Addr, gauge_id: u64, + epoch_limit: Option, epoch_size: Option, min_percent_selected: Option, max_options_selected: Option, @@ -456,6 +470,10 @@ mod execute { ensure!(epoch_size > 60u64, ContractError::EpochSizeTooShort {}); gauge.epoch = epoch_size; } + if let Some(epoch_limit) = epoch_limit { + let e = gauge.gauge_epoch()?; + ensure!(e < epoch_limit, ContractError::EpochLimitTooShort {}) + } if let Some(min_percent_selected) = min_percent_selected { if min_percent_selected.is_zero() { gauge.min_percent_selected = None @@ -754,7 +772,8 @@ mod execute { } pub fn execute(deps: DepsMut, env: Env, gauge_id: u64) -> Result { - let mut gauge = GAUGES.load(deps.storage, gauge_id)?; + let mut gauge = GAUGES.load(deps.storage, gauge_id.clone())?; + let mut msgs = vec![]; if gauge.is_stopped { return Err(ContractError::GaugeStopped(gauge_id)); @@ -796,12 +815,24 @@ mod execute { &AdapterQueryMsg::SampleGaugeMsgs { selected }, )?; + msgs.extend(execute_messages.execute); + + + // increments epoch count + gauge.count = gauge.increment_gauge_count()?; + + if gauge.will_reach_epoch_limit() { + msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_json_binary(&ExecuteMsg::StopGauge { gauge: gauge_id })?, + funds: vec![], + })) + } + let config = CONFIG.load(deps.storage)?; let execute_msg = WasmMsg::Execute { contract_addr: config.dao_core.to_string(), - msg: to_json_binary(&DaoExecuteMsg::ExecuteProposalHook { - msgs: execute_messages.execute, - })?, + msg: to_json_binary(&DaoExecuteMsg::ExecuteProposalHook { msgs })?, funds: vec![], }; @@ -874,6 +905,7 @@ mod query { is_stopped: gauge.is_stopped, next_epoch: gauge.next_epoch, reset: gauge.reset, + total_epochs: gauge.total_epoch, } } diff --git a/contracts/gauges/gauge/src/error.rs b/contracts/gauges/gauge/src/error.rs index c320a4ec7..82b707cee 100644 --- a/contracts/gauges/gauge/src/error.rs +++ b/contracts/gauges/gauge/src/error.rs @@ -51,6 +51,8 @@ pub enum ContractError { #[error("Epoch size must be bigger then 60 seconds")] EpochSizeTooShort {}, + #[error("Epoch limit must be bigger then current epoch")] + EpochLimitTooShort {}, #[error("Minimum percent selected parameter needs to be smaller then 1.0")] MinPercentSelectedTooBig {}, diff --git a/contracts/gauges/gauge/src/msg.rs b/contracts/gauges/gauge/src/msg.rs index 3578e32ec..0e274d798 100644 --- a/contracts/gauges/gauge/src/msg.rs +++ b/contracts/gauges/gauge/src/msg.rs @@ -39,9 +39,12 @@ pub struct GaugeConfig { pub max_available_percentage: Option, /// If set, the gauge can be reset periodically, every `reset_epoch` seconds. pub reset_epoch: Option, + /// if set, the gauge will disable itself after this many epochs + pub total_epochs: Option, } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Updates gauge voting power in Token DAOs when a user stakes or unstakes StakeChangeHook(StakeChangedHookMsg), @@ -61,6 +64,7 @@ pub enum ExecuteMsg { min_percent_selected: Option, max_options_selected: Option, max_available_percentage: Option, + epoch_limit: Option, }, /// Stops a given gauge, meaning it will not execute any more messages, /// Or receive any more updates on MemberChangedHook. @@ -94,11 +98,12 @@ pub enum ExecuteMsg { pub struct CreateGaugeReply { /// Id of the gauge that was just created pub id: u64, + pub addr: String, } /// Queries the gauge exposes #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(dao_interface::voting::InfoResponse)] Info {}, @@ -139,6 +144,8 @@ pub struct GaugeResponse { pub adapter: String, /// Frequency (in seconds) the gauge executes messages, typically something like 7*86400 pub epoch_size: u64, + /// Total epoch duration + pub total_epochs: Option, /// Minimum percentage of votes needed by a given option to be in the selected set. /// If unset, there is no minimum percentage, just the `max_options_selected` limit. pub min_percent_selected: Option, diff --git a/contracts/gauges/gauge/src/multitest/gauge.rs b/contracts/gauges/gauge/src/multitest/gauge.rs index 1b9543f4f..e39918801 100644 --- a/contracts/gauges/gauge/src/multitest/gauge.rs +++ b/contracts/gauges/gauge/src/multitest/gauge.rs @@ -25,6 +25,7 @@ fn create_gauge() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -42,6 +43,7 @@ fn create_gauge() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None } ); } @@ -62,6 +64,7 @@ fn gauge_can_upgrade_from_self() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -82,6 +85,7 @@ fn gauge_can_upgrade_from_self() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None } ); } @@ -102,6 +106,7 @@ fn gauge_migrate_with_next_epochs() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -120,6 +125,7 @@ fn gauge_migrate_with_next_epochs() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None } ); @@ -153,6 +159,7 @@ fn gauge_migrate_with_next_epochs() { is_stopped: false, next_epoch: suite.current_time() + 14 * 86400, reset: None, + total_epochs: None } ); @@ -186,7 +193,13 @@ fn execute_gauge() { suite.next_block(); let gauge_config = suite - .instantiate_adapter_and_return_config(&[voter1, voter2], reward_to_distribute, None, None) + .instantiate_adapter_and_return_config( + &[voter1, voter2], + reward_to_distribute, + None, + None, + None, + ) .unwrap(); suite .propose_update_proposal_module(voter1.to_string(), vec![gauge_config]) @@ -289,6 +302,7 @@ fn query_last_execution() { (1000, "ujuno"), None, None, + None, ) .unwrap(); let gauge_id = 0; @@ -381,7 +395,7 @@ fn execute_gauge_twice_same_epoch() { suite.next_block(); let gauge_config = suite - .instantiate_adapter_and_return_config(&[voter1, voter2], (1000, "ujuno"), None, None) // reward per + .instantiate_adapter_and_return_config(&[voter1, voter2], (1000, "ujuno"), None, None, None) // reward per // epoch .unwrap(); suite @@ -492,7 +506,13 @@ fn execute_stopped_gauge() { suite.next_block(); let gauge_config = suite - .instantiate_adapter_and_return_config(&[voter1, voter2], reward_to_distribute, None, None) + .instantiate_adapter_and_return_config( + &[voter1, voter2], + reward_to_distribute, + None, + None, + None, + ) .unwrap(); suite .propose_update_proposal_module(voter1.to_string(), vec![gauge_config]) @@ -578,6 +598,7 @@ fn update_gauge() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -588,6 +609,7 @@ fn update_gauge() { (1000, "uusdc"), None, None, + None, ) .unwrap(); @@ -606,6 +628,7 @@ fn update_gauge() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None }, GaugeResponse { id: 1, @@ -618,6 +641,7 @@ fn update_gauge() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None } ] ); @@ -625,6 +649,7 @@ fn update_gauge() { // update parameters on the first gauge let owner = suite.owner.clone(); let new_epoch = EPOCH * 2; + let epoch_limit = 8u64; let new_min_percent = Some(Decimal::percent(10)); let new_max_options = 15; let new_max_available_percentage = Some(Decimal::percent(5)); @@ -634,6 +659,7 @@ fn update_gauge() { gauge_contract.clone(), 0, new_epoch, + epoch_limit, new_min_percent, new_max_options, new_max_available_percentage, @@ -655,6 +681,7 @@ fn update_gauge() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None }, GaugeResponse { id: 1, @@ -667,6 +694,7 @@ fn update_gauge() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None } ] ); @@ -678,6 +706,7 @@ fn update_gauge() { gauge_contract.clone(), 1, None, + epoch_limit, Some(Decimal::zero()), None, None, @@ -699,6 +728,7 @@ fn update_gauge() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None }, GaugeResponse { id: 1, @@ -711,6 +741,7 @@ fn update_gauge() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None } ] ); @@ -722,6 +753,7 @@ fn update_gauge() { gauge_contract.clone(), 0, new_epoch, + epoch_limit, new_min_percent, new_max_options, None, @@ -735,6 +767,7 @@ fn update_gauge() { gauge_contract.clone(), 0, 50, + epoch_limit, new_min_percent, new_max_options, None, @@ -748,6 +781,7 @@ fn update_gauge() { gauge_contract.clone(), 0, new_epoch, + epoch_limit, Some(Decimal::one()), new_max_options, None, @@ -764,6 +798,7 @@ fn update_gauge() { gauge_contract.clone(), 0, new_epoch, + epoch_limit, new_min_percent, 0, None, @@ -780,6 +815,7 @@ fn update_gauge() { gauge_contract, 1, None, + epoch_limit, Some(Decimal::zero()), None, Some(Decimal::percent(101)), diff --git a/contracts/gauges/gauge/src/multitest/reset.rs b/contracts/gauges/gauge/src/multitest/reset.rs index ffe572bd4..1047470f6 100644 --- a/contracts/gauges/gauge/src/multitest/reset.rs +++ b/contracts/gauges/gauge/src/multitest/reset.rs @@ -27,6 +27,7 @@ fn basic_gauge_reset() { reward_to_distribute, None, RESET_EPOCH, + None, ) .unwrap(); suite @@ -48,7 +49,6 @@ fn basic_gauge_reset() { .unwrap(); let proposal_modules = suite.query_proposal_modules().unwrap(); let gauge_contract = proposal_modules[1].clone(); - let gauge_id = 0; // vote for one of the options in gauge @@ -179,6 +179,7 @@ fn gauge_migrate_with_reset() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -197,6 +198,7 @@ fn gauge_migrate_with_reset() { is_stopped: false, next_epoch: suite.current_time() + 7 * 86400, reset: None, + total_epochs: None, } ); @@ -262,6 +264,7 @@ fn gauge_migrate_with_reset() { reset_each: RESET_EPOCH, next: suite.current_time() + 100, }), + total_epochs: None, } ); } @@ -295,6 +298,7 @@ fn gauge_migrate_keeps_last_reset() { (1000, "ujuno"), None, Some(RESET_EPOCH), + None, ) .unwrap(); let gauge_id = 0; @@ -346,6 +350,7 @@ fn partial_reset() { reward_to_distribute, None, RESET_EPOCH, + None, ) .unwrap(); suite diff --git a/contracts/gauges/gauge/src/multitest/suite.rs b/contracts/gauges/gauge/src/multitest/suite.rs index de16b1d32..4735866b0 100644 --- a/contracts/gauges/gauge/src/multitest/suite.rs +++ b/contracts/gauges/gauge/src/multitest/suite.rs @@ -616,12 +616,14 @@ impl Suite { to_distribute: (u128, &str), max_available_percentage: impl Into>, reset_epoch: impl Into>, + epoch_limit: impl Into>, ) -> AnyResult { let option = self.instantiate_adapter_and_return_config( options, to_distribute, max_available_percentage, reset_epoch, + epoch_limit, )?; let gauge_adapter = option.adapter.clone(); self.app.execute_contract( @@ -639,6 +641,7 @@ impl Suite { to_distribute: (u128, &str), max_available_percentage: impl Into>, reset_epoch: impl Into>, + epoch_limit: impl Into>, ) -> AnyResult { let gauge_adapter = self.app.instantiate_contract( self.gauge_adapter_code_id, @@ -660,6 +663,7 @@ impl Suite { max_options_selected: 10, max_available_percentage: max_available_percentage.into(), reset_epoch: reset_epoch.into(), + total_epochs: epoch_limit.into(), }) } @@ -669,6 +673,7 @@ impl Suite { sender: &str, gauge_contract: Addr, gauge_id: u64, + epoch_limit: impl Into>, epoch_size: impl Into>, min_percent_selected: Option, max_options_selected: impl Into>, @@ -683,6 +688,7 @@ impl Suite { min_percent_selected, max_options_selected: max_options_selected.into(), max_available_percentage: max_available_percentage.into(), + epoch_limit: epoch_limit.into(), }, &[], ) diff --git a/contracts/gauges/gauge/src/multitest/tally.rs b/contracts/gauges/gauge/src/multitest/tally.rs index 9de07ecb4..ab00ad1c7 100644 --- a/contracts/gauges/gauge/src/multitest/tally.rs +++ b/contracts/gauges/gauge/src/multitest/tally.rs @@ -47,6 +47,7 @@ fn multiple_options_one_gauge() { reward_to_distribute, None, None, + None, ) .unwrap(); let gauge_id = 0; @@ -152,6 +153,7 @@ fn multiple_options_two_gauges() { reward_to_distribute, None, None, + None, ) .unwrap(); suite @@ -179,6 +181,7 @@ fn multiple_options_two_gauges() { reward_to_distribute, None, None, + None, ) .unwrap(); let second_gauge_id = 1; @@ -282,6 +285,7 @@ fn not_voted_options_are_not_selected() { reward_to_distribute, None, None, + None, ) .unwrap(); let first_gauge_id = 0; diff --git a/contracts/gauges/gauge/src/multitest/voting.rs b/contracts/gauges/gauge/src/multitest/voting.rs index 962daa1ac..05ed8ef5c 100644 --- a/contracts/gauges/gauge/src/multitest/voting.rs +++ b/contracts/gauges/gauge/src/multitest/voting.rs @@ -48,6 +48,7 @@ fn add_option() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -135,6 +136,7 @@ fn remove_option() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -282,6 +284,7 @@ fn vote_for_option() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -423,6 +426,7 @@ fn remove_vote() { (1000, "ujuno"), None, None, + None, ) .unwrap(); @@ -493,7 +497,7 @@ fn votes_stays_the_same_after_execution() { suite.next_block(); let gauge_config = suite - .instantiate_adapter_and_return_config(&[voter1, voter2], reward_to_distribute, None, None) + .instantiate_adapter_and_return_config(&[voter1, voter2], reward_to_distribute, None, None,None) .unwrap(); suite .propose_update_proposal_module(voter1.to_string(), vec![gauge_config]) @@ -616,7 +620,7 @@ fn vote_for_max_capped_option() { &[voter1, voter2], (1000, "ujuno"), Some(Decimal::percent(10)), - None, + None,None, ) .unwrap(); @@ -732,6 +736,7 @@ fn membership_voting_power_change() { (1000, "ujuno"), None, None, + None, ) .unwrap(); let gauge_id = 0; // first created gauge @@ -913,6 +918,7 @@ fn token_staking_voting_power_change() { (1000, "ujuno"), None, None, + None, ) .unwrap(); let gauge_id = 0; // first created gauge @@ -1111,6 +1117,7 @@ fn nft_staking_voting_power_change() { (1000, "ujuno"), None, None, + None, ) .unwrap(); let gauge_id = 0; // first created gauge diff --git a/contracts/gauges/gauge/src/state.rs b/contracts/gauges/gauge/src/state.rs index a48c4fd20..1eba039d8 100644 --- a/contracts/gauges/gauge/src/state.rs +++ b/contracts/gauges/gauge/src/state.rs @@ -51,6 +51,10 @@ pub struct Gauge { pub adapter: Addr, /// Frequency (in seconds) the gauge executes messages, typically something like 7*86400 pub epoch: u64, + /// Epoch count. + pub count: Option, + /// total possible count for a gauge to run. will automatially disable itself when reaching this epoch count. + pub total_epoch: Option, /// Minimum percentage of votes needed by a given option to be in the selected set pub min_percent_selected: Option, /// Maximum number of Options to make the selected set. Needed even with @@ -86,6 +90,19 @@ impl Gauge { .map(|r| r.last == Some(r.next)) .unwrap_or_default() } + pub fn will_reach_epoch_limit(&self) -> bool { + if let Some(total) = self.total_epoch { + total == self.count.unwrap_or_default() + } else { + false + } + } + pub fn increment_gauge_count(&self) -> StdResult> { + Ok(self.count.map_or(Some(0), |o| Some(o + 1))) + } + pub fn gauge_epoch(&self) -> StdResult { + Ok(self.count.map_or(Some(0), |o| Some(o)).unwrap_or_default()) + } } #[cw_serde] @@ -451,6 +468,8 @@ mod tests { next_epoch: env.block.time.seconds(), last_executed_set: None, reset: None, + count: Some(0), + total_epoch: None, }, ) .unwrap(); @@ -578,6 +597,8 @@ mod tests { next_epoch: env.block.time.seconds(), last_executed_set: None, reset: None, + count: Some(0), + total_epoch: None, }, ) .unwrap(); 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 c38ce7f64..0b44a0621 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/Cargo.toml +++ b/contracts/pre-propose/dao-pre-propose-approval-single/Cargo.toml @@ -31,6 +31,7 @@ dao-interface = { workspace = true } cw-denom = { workspace = true } cw-multi-test = { workspace = true } cw-utils = { workspace = true } +cw4 = { workspace = true } cw4-group = { workspace = true } cw20 = { workspace = true } cw20-base = { workspace = true } @@ -41,3 +42,11 @@ dao-voting = { workspace = true } dao-voting-cw4 = { workspace = true } dao-voting-cw20-staked = { workspace = true } dao-proposal-single = { workspace = true } + +# v2.4.1 migration +dao-dao-core-v241 = { workspace = true } +dao-interface-v241 = { workspace = true } +dao-pre-propose-approval-single-v241 = { workspace = true } +dao-proposal-single-v241 = { workspace = true } +dao-voting-cw4-v241 = { workspace = true } +dao-voting-v241 = { workspace = true } diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/examples/schema.rs b/contracts/pre-propose/dao-pre-propose-approval-single/examples/schema.rs index 514f8152e..122a90a08 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/examples/schema.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/examples/schema.rs @@ -1,10 +1,11 @@ use cosmwasm_schema::write_api; -use dao_pre_propose_approval_single::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use dao_pre_propose_approval_single::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; fn main() { write_api! { instantiate: InstantiateMsg, query: QueryMsg, execute: ExecuteMsg, + migrate: MigrateMsg, } } 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 d253aeb3a..9bb6f1213 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,7 @@ "type": "object", "required": [ "extension", - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -30,13 +30,21 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "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" + }, "DepositRefundPolicy": { "oneOf": [ { @@ -123,6 +131,76 @@ }, "additionalProperties": false }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "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" @@ -238,11 +316,9 @@ "properties": { "update_config": { "type": "object", - "required": [ - "open_proposal_submission" - ], "properties": { "deposit_info": { + "description": "If None, will remove the deposit. Backwards compatible.", "anyOf": [ { "$ref": "#/definitions/UncheckedDepositInfo" @@ -252,8 +328,79 @@ } ] }, - "open_proposal_submission": { - "type": "boolean" + "submission_policy": { + "description": "If None, will leave the submission policy in the config as-is.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Perform more granular submission policy updates to allow for atomic operations that don't override others.", + "type": "object", + "required": [ + "update_submission_policy" + ], + "properties": { + "update_submission_policy": { + "type": "object", + "properties": { + "allowlist_add": { + "description": "If using specific policy, optionally add to the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "allowlist_remove": { + "description": "If using specific policy, optionally remove from the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_add": { + "description": "Optionally add to the denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_remove": { + "description": "Optionally remove from denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "set_dao_members": { + "description": "If using specific policy, optionally update the `dao_members` flag.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -384,6 +531,10 @@ } ], "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": [ @@ -573,6 +724,10 @@ } ] }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "DepositRefundPolicy": { "oneOf": [ { @@ -693,6 +848,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -855,6 +1035,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -1011,6 +1221,76 @@ } } }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "ProposeMessage": { "oneOf": [ { @@ -1454,6 +1734,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -1538,6 +1872,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -1573,6 +1922,20 @@ }, "additionalProperties": false }, + { + "description": "Returns contract version info.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the module's configuration.", "type": "object", @@ -1611,6 +1974,28 @@ }, "additionalProperties": false }, + { + "description": "Returns whether or not the address can submit proposals.", + "type": "object", + "required": [ + "can_propose" + ], + "properties": { + "can_propose": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns list of proposal submitted hooks.", "type": "object", @@ -1915,15 +2300,152 @@ } } }, - "migrate": null, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "from_under_v250" + ], + "properties": { + "from_under_v250": { + "type": "object", + "properties": { + "policy": { + "description": "Optionally set a new submission policy with more granular controls. If not set, the current policy will remain.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "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" + }, + "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" + }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, "sudo": null, "responses": { + "can_propose": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", "required": [ - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -1937,9 +2459,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -2040,6 +2566,76 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "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" @@ -2184,6 +2780,40 @@ } } }, + "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 + } + } + }, "proposal_module": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Addr", 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 06714deee..091e91e2e 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 @@ -13,8 +13,8 @@ use dao_voting::deposit::DepositRefundPolicy; use dao_voting::proposal::SingleChoiceProposeMsg as ProposeMsg; use crate::msg::{ - ApproverProposeMessage, ExecuteExt, ExecuteMsg, InstantiateExt, InstantiateMsg, ProposeMessage, - ProposeMessageInternal, QueryExt, QueryMsg, + ApproverProposeMessage, ExecuteExt, ExecuteMsg, InstantiateExt, InstantiateMsg, MigrateMsg, + ProposeMessage, ProposeMessageInternal, QueryExt, QueryMsg, }; use crate::state::{ advance_approval_id, Proposal, ProposalStatus, APPROVER, COMPLETED_PROPOSALS, @@ -24,7 +24,7 @@ use crate::state::{ pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-pre-propose-approval-single"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -type PrePropose = PreProposeContract; +type PrePropose = PreProposeContract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -403,3 +403,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { _ => PrePropose::default().query(deps, env, msg), } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(mut deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + let res = PrePropose::default().migrate(deps.branch(), msg); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + res +} 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 25bc7ea84..381606d1d 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 @@ -1,7 +1,8 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{CosmosMsg, Empty}; use dao_pre_propose_base::msg::{ - ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, QueryMsg as QueryBase, + ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, MigrateMsg as MigrateBase, + QueryMsg as QueryBase, }; use dao_voting::{proposal::SingleChoiceProposeMsg as ProposeMsg, voting::SingleChoiceAutoVote}; @@ -87,6 +88,7 @@ pub enum QueryExt { pub type InstantiateMsg = InstantiateBase; pub type ExecuteMsg = ExecuteBase; pub type QueryMsg = QueryBase; +pub type MigrateMsg = MigrateBase; /// Internal version of the propose message that includes the /// `proposer` field. The module will fill this in based on the sender 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 99b8c0369..2e3f3e324 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,26 +1,38 @@ -use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Coin, Empty, Uint128}; +use cosmwasm_std::{ + coins, from_json, to_json_binary, Addr, Coin, CosmosMsg, Empty, Uint128, WasmMsg, +}; use cw2::ContractVersion; use cw20::Cw20Coin; use cw_denom::UncheckedDenom; use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor}; use cw_utils::Duration; +use dao_interface::proposal::InfoResponse; 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::query::ProposalResponse; -use dao_testing::helpers::instantiate_with_cw4_groups_governance; +use dao_testing::{contracts::cw4_group_contract, helpers::instantiate_with_cw4_groups_governance}; +use dao_voting::pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, pre_propose::{PreProposeInfo, ProposalCreationPolicy}, status::Status, threshold::{PercentageThreshold, Threshold}, - voting::Vote, + voting::{SingleChoiceAutoVote, Vote}, }; +// test v2.4.1 migration +use dao_dao_core_v241 as core_v241; +use dao_interface_v241 as di_v241; +use dao_pre_propose_approval_single_v241 as dppas_v241; +use dao_proposal_single_v241 as dps_v241; +use dao_voting_cw4_v241 as dvcw4_v241; +use dao_voting_v241 as dv_v241; + use crate::state::{Proposal, ProposalStatus}; use crate::{contract::*, msg::*}; -fn cw_dao_proposal_single_contract() -> Box> { +fn dao_proposal_single_contract() -> Box> { let contract = ContractWrapper::new( dao_proposal_single::contract::execute, dao_proposal_single::contract::instantiate, @@ -31,8 +43,8 @@ fn cw_dao_proposal_single_contract() -> Box> { Box::new(contract) } -fn cw_pre_propose_base_proposal_single() -> Box> { - let contract = ContractWrapper::new(execute, instantiate, query); +fn dao_pre_propose_approval_single_contract() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query).with_migrate(migrate); Box::new(contract) } @@ -50,7 +62,17 @@ fn get_default_proposal_module_instantiate( deposit_info: Option, open_proposal_submission: bool, ) -> dao_proposal_single::msg::InstantiateMsg { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_approval_single_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + } + }; dao_proposal_single::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { @@ -65,7 +87,7 @@ fn get_default_proposal_module_instantiate( code_id: pre_propose_id, msg: to_json_binary(&InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: InstantiateExt { approver: "approver".to_string(), }, @@ -116,7 +138,7 @@ fn setup_default_test( deposit_info: Option, open_proposal_submission: bool, ) -> DefaultTestSetup { - let dao_proposal_single_id = app.store_code(cw_dao_proposal_single_contract()); + let dao_proposal_single_id = app.store_code(dao_proposal_single_contract()); let proposal_module_instantiate = get_default_proposal_module_instantiate(app, deposit_info, open_proposal_submission); @@ -168,6 +190,15 @@ fn setup_default_test( get_proposal_module(app, pre_propose.clone()) ); assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + assert_eq!( + InfoResponse { + info: ContractVersion { + contract: "crates.io:dao-pre-propose-approval-single".to_string(), + version: env!("CARGO_PKG_VERSION").to_string() + } + }, + get_info(app, pre_propose.clone()) + ); DefaultTestSetup { core_addr, @@ -286,6 +317,12 @@ fn get_dao(app: &App, module: Addr) -> Addr { .unwrap() } +fn get_info(app: &App, module: Addr) -> InfoResponse { + app.wrap() + .query_wasm_smart(module, &QueryMsg::Info {}) + .unwrap() +} + fn get_proposal_module(app: &App, module: Addr) -> Addr { app.wrap() .query_wasm_smart(module, &QueryMsg::ProposalModule {}) @@ -298,19 +335,30 @@ fn get_deposit_info(app: &App, module: Addr, id: u64) -> DepositInfoResponse { .unwrap() } +fn query_can_propose(app: &App, module: Addr, address: impl Into) -> bool { + app.wrap() + .query_wasm_smart( + module, + &QueryMsg::CanPropose { + address: address.into(), + }, + ) + .unwrap() +} + fn update_config( app: &mut App, module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> Config { app.execute_contract( Addr::unchecked(sender), module.clone(), &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -324,14 +372,14 @@ fn update_config_should_fail( module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> PreProposeError { app.execute_contract( Addr::unchecked(sender), module, &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -1165,7 +1213,10 @@ fn test_permissions() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); } #[test] @@ -1314,7 +1365,10 @@ fn test_no_deposit_required_members_submission() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); let pre_propose_id = make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &[]); @@ -1325,15 +1379,221 @@ fn test_no_deposit_required_members_submission() { assert_eq!(Status::Passed, new_status) } +#[test] +fn test_anyone_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + ); + + let rando = "rando"; + + // Proposal succeeds when anyone can propose. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_pre_proposal(&mut app, pre_propose.clone(), rando, &[]); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked(rando)], + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposing succeeds if not on denylist. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_pre_proposal(&mut app, pre_propose, "ekez", &[]); +} + +#[test] +fn test_specific_allowlist_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + // Proposal succeeds for member. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &[]); + + let rando = "rando"; + + // Proposing fails for non-member. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![], + }, + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_pre_proposal(&mut app, pre_propose.clone(), rando, &[]); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![Addr::unchecked("ekez")], + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![], + }, + ); + + // Proposing fails if members not allowed. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_pre_proposal(&mut app, pre_propose.clone(), rando, &[]); +} + #[test] #[should_panic(expected = "invalid zero deposit. set the deposit to `None` to have no deposit")] fn test_instantiate_with_zero_native_deposit() { let mut app = App::default(); - let dao_proposal_single_id = app.store_code(cw_dao_proposal_single_contract()); + let dao_proposal_single_id = app.store_code(dao_proposal_single_contract()); let proposal_module_instantiate = { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_approval_single_contract()); dao_proposal_single::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { @@ -1354,7 +1614,11 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: InstantiateExt { approver: "approver".to_string(), }, @@ -1395,10 +1659,10 @@ fn test_instantiate_with_zero_cw20_deposit() { let cw20_addr = instantiate_cw20_base_default(&mut app); - let dao_proposal_single_id = app.store_code(cw_dao_proposal_single_contract()); + let dao_proposal_single_id = app.store_code(dao_proposal_single_contract()); let proposal_module_instantiate = { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_approval_single_contract()); dao_proposal_single::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { @@ -1419,7 +1683,11 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: InstantiateExt { approver: "approver".to_string(), }, @@ -1467,7 +1735,11 @@ fn test_update_config() { config, Config { deposit_info: None, - open_proposal_submission: false + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + } } ); @@ -1487,7 +1759,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never, }), - true, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, ); let config = get_config(&app, pre_propose.clone()); @@ -1499,7 +1771,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never }), - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, } ); @@ -1549,92 +1821,590 @@ fn test_update_config() { assert_eq!(balance, Uint128::new(0)); // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); - assert_eq!(err, PreProposeError::NotDao {}); -} - -#[test] -fn test_withdraw() { - let mut app = App::default(); - - let DefaultTestSetup { - core_addr, - proposal_single, - pre_propose, - } = setup_default_test(&mut app, None, false); - - let err = withdraw_should_fail( + let err = update_config_should_fail( &mut app, pre_propose.clone(), proposal_single.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + None, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, ); assert_eq!(err, PreProposeError::NotDao {}); - let err = withdraw_should_fail( + // Errors when no one is authorized to create proposals. + let err = update_config_should_fail( &mut app, pre_propose.clone(), core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![], + denylist: vec![], + }, ); - assert_eq!(err, PreProposeError::NothingToWithdraw {}); - - let err = withdraw_should_fail(&mut app, pre_propose.clone(), core_addr.as_str(), None); - assert_eq!(err, PreProposeError::NoWithdrawalDenom {}); - - // Turn on native deposits. - update_config( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) ); - // Withdraw with no specified denom - should fall back to the one - // in the config. - mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); - withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(10)); - - // Withdraw again, this time specifying a native denomination. - mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); - withdraw( + // Errors when allowlist and denylist overlap. + let err = update_config_should_fail( &mut app, - pre_propose.clone(), + pre_propose, core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![Addr::unchecked("ekez")], + }, ); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(20)); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} - // Make a proposal with the native tokens to put some in the system. - mint_natives(&mut app, "ekez", coins(10, "ujuno")); - let native_pre_propose_id = - make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &coins(10, "ujuno")); +#[test] +fn test_update_submission_policy() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, true); - // Approver approves - let native_id = approve_proposal( - &mut app, - pre_propose.clone(), - "approver", - native_pre_propose_id, + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + } ); - // Update the config to use a cw20 token. - let cw20_address = instantiate_cw20_base_default(&mut app); - update_config( - &mut app, - pre_propose.clone(), - core_addr.as_str(), + // Only the core module can update the submission policy. + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, PreProposeError::NotDao {}); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked("ekez")], + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + } + ); + + // Error if try to change Specific fields when set to Anyone. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(true), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + + // Change to Specific policy. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateConfig { + deposit_info: None, + submission_policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + } + ); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![Addr::unchecked("ekez")], + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + }, + } + ); + + // Append to allowlist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![], + }, + } + ); + + // Add and remove to/from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["someone".to_string(), "else".to_string()]), + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + denylist: vec![], + }, + } + ); + + // Remove from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + }, + } + ); + + // Setting dao_members to false fails if allowlist is empty. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Set dao_members to false and add allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![] + }, + } + ); + + // Errors when allowlist and denylist overlap. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} + +#[test] +fn test_withdraw() { + let mut app = App::default(); + + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + let err = withdraw_should_fail( + &mut app, + pre_propose.clone(), + proposal_single.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + assert_eq!(err, PreProposeError::NotDao {}); + + let err = withdraw_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + assert_eq!(err, PreProposeError::NothingToWithdraw {}); + + let err = withdraw_should_fail(&mut app, pre_propose.clone(), core_addr.as_str(), None); + assert_eq!(err, PreProposeError::NoWithdrawalDenom {}); + + // Turn on native deposits. + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDepositInfo { + denom: DepositToken::Token { + denom: UncheckedDenom::Native("ujuno".to_string()), + }, + amount: Uint128::new(10), + refund_policy: DepositRefundPolicy::Always, + }), + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + // Withdraw with no specified denom - should fall back to the one + // in the config. + mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); + withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(10)); + + // Withdraw again, this time specifying a native denomination. + mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); + withdraw( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(20)); + + // Make a proposal with the native tokens to put some in the system. + mint_natives(&mut app, "ekez", coins(10, "ujuno")); + let native_pre_propose_id = + make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &coins(10, "ujuno")); + + // Approver approves + let native_id = approve_proposal( + &mut app, + pre_propose.clone(), + "approver", + native_pre_propose_id, + ); + + // Update the config to use a cw20 token. + let cw20_address = instantiate_cw20_base_default(&mut app); + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), Some(UncheckedDepositInfo { denom: DepositToken::Token { denom: UncheckedDenom::Cw20(cw20_address.to_string()), @@ -1642,74 +2412,827 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + increase_allowance( + &mut app, + "ekez", + &pre_propose, + cw20_address.clone(), + Uint128::new(10), + ); + let cw20_pre_propose_id = make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &[]); + + // Approver approves + let cw20_id = approve_proposal( + &mut app, + pre_propose.clone(), + "approver", + cw20_pre_propose_id, + ); + + // There is now a pending proposal and cw20 tokens in the + // pre-propose module that should be returned on that proposal's + // completion. To make things interesting, we withdraw those + // tokens which should cause the status change hook on the + // proposal's execution to fail as we don't have sufficent balance + // to return the deposit. + withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); + let balance = get_balance_cw20(&app, &cw20_address, core_addr.as_str()); + assert_eq!(balance, Uint128::new(10)); + + // Proposal should still be executable! We just get removed from + // the proposal module's hook receiver list. + vote( + &mut app, + proposal_single.clone(), + "ekez", + cw20_id, + Vote::Yes, + ); + execute_proposal(&mut app, proposal_single.clone(), "ekez", cw20_id); + + // Make sure the proposal module has fallen back to anyone can + // propose becuase of our malfunction. + let proposal_creation_policy: ProposalCreationPolicy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dao_proposal_single::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + assert_eq!(proposal_creation_policy, ProposalCreationPolicy::Anyone {}); + + // Close out the native proposal and it's deposit as well. + vote( + &mut app, + proposal_single.clone(), + "ekez", + native_id, + Vote::No, + ); + close_proposal(&mut app, proposal_single.clone(), "ekez", native_id); + withdraw( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(30)); +} + +#[test] +fn test_migrate_from_v241() { + let app = &mut App::default(); + + let core_v241_contract = Box::new( + ContractWrapper::new( + core_v241::contract::execute, + core_v241::contract::instantiate, + core_v241::contract::query, + ) + .with_reply(core_v241::contract::reply), + ); + let dvcw4_v241_contract = Box::new( + ContractWrapper::new( + dvcw4_v241::contract::execute, + dvcw4_v241::contract::instantiate, + dvcw4_v241::contract::query, + ) + .with_reply(dvcw4_v241::contract::reply), + ); + let dpps_v241_contract = Box::new(ContractWrapper::new( + dppas_v241::contract::execute, + dppas_v241::contract::instantiate, + dppas_v241::contract::query, + )); + let dps_v241_contract = Box::new( + ContractWrapper::new( + dps_v241::contract::execute, + dps_v241::contract::instantiate, + dps_v241::contract::query, + ) + .with_reply(dps_v241::contract::reply), + ); + + let core_id = app.store_code(core_v241_contract); + let cw4_id = app.store_code(cw4_group_contract()); + let dvcw4_v241_id = app.store_code(dvcw4_v241_contract); + let dpps_v241_id = app.store_code(dpps_v241_contract); + let dps_v241_id = app.store_code(dps_v241_contract); + + let governance_instantiate = di_v241::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: di_v241::state::ModuleInstantiateInfo { + code_id: dvcw4_v241_id, + msg: to_json_binary(&dvcw4_v241::msg::InstantiateMsg { + group_contract: dvcw4_v241::msg::GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: "ekez".to_string(), + weight: 9, + }, + cw4::Member { + addr: "keze".to_string(), + weight: 8, + }, + ], + }, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![di_v241::state::ModuleInstantiateInfo { + code_id: dps_v241_id, + msg: to_json_binary(&dps_v241::msg::InstantiateMsg { + threshold: dv_v241::threshold::Threshold::AbsolutePercentage { + percentage: dv_v241::threshold::PercentageThreshold::Majority {}, + }, + max_voting_period: cw_utils::Duration::Time(86400), + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + pre_propose_info: dv_v241::pre_propose::PreProposeInfo::ModuleMayPropose { + info: di_v241::state::ModuleInstantiateInfo { + code_id: dpps_v241_id, + msg: to_json_binary(&dppas_v241::msg::InstantiateMsg { + deposit_info: None, + open_proposal_submission: false, + extension: dppas_v241::msg::InstantiateExt { + approver: "approver".to_string(), + }, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "baby's first pre-propose module".to_string(), + }, + }, + close_proposal_on_execution_failure: false, + veto: None, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let core_addr = app + .instantiate_contract( + core_id, + Addr::unchecked("ekez"), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + app.update_block(|block| block.height += 1); + + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &di_v241::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposal_modules.len(), 1); + let proposal_single = proposal_modules.into_iter().next().unwrap().address; + let proposal_creation_policy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + let pre_propose = match proposal_creation_policy { + dv_v241::pre_propose::ProposalCreationPolicy::Module { addr } => addr, + _ => panic!("expected a module for the proposal creation policy"), + }; + + // Make sure things were set up correctly. + assert_eq!( + proposal_single, + get_proposal_module(app, pre_propose.clone()) + ); + assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: "crates.io:dao-pre-propose-approval-single".to_string(), + version: "2.4.1".to_string() + }, + info, + ); + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Propose { + msg: dppas_v241::msg::ProposeMessage::Propose { + title: "title1".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: Some(dv_v241::voting::SingleChoiceAutoVote { + vote: dv_v241::voting::Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("approver"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Extension { + msg: dppas_v241::msg::ExecuteExt::Approve { id: 1 }, + }, + &[], + ) + .unwrap(); + + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Passed); + assert_eq!(proposal.proposal.proposer, Addr::unchecked("ekez")); + assert_eq!(proposal.proposal.title, "title1".to_string()); + assert_eq!(proposal.proposal.description, "d".to_string()); + assert_eq!(proposal.proposal.msgs, vec![]); + + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dps_v241::msg::ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); + + // UPGRADE ONLY PRE-PROPOSE TO LATEST VIA DAO PROPOSAL + + let dppas_latest_id = app.store_code(dao_pre_propose_approval_single_contract()); + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Propose { + msg: dppas_v241::msg::ProposeMessage::Propose { + title: "upgrade pre-propose-single from v2.4.1".to_string(), + description: "d".to_string(), + msgs: vec![CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: pre_propose.to_string(), + new_code_id: dppas_latest_id, + msg: to_json_binary(&MigrateMsg::FromUnderV250 { policy: None }).unwrap(), + })], + vote: Some(dv_v241::voting::SingleChoiceAutoVote { + vote: dv_v241::voting::Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("approver"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Extension { + msg: dppas_v241::msg::ExecuteExt::Approve { id: 2 }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dps_v241::msg::ExecuteMsg::Execute { proposal_id: 2 }, + &[], + ) + .unwrap(); + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 2 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); + + // MAKE SURE PRE PROPOSE INFO CHANGED + + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string() + }, + info, + ); + + // MAKE SURE PRE PROPOSE CONFIG WAS UPDATED + + let config: Config = app + .wrap() + .query_wasm_smart(pre_propose.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + } + }, + config + ); + + // NOW MAKE SURE WE CAN MAKE AND VOTE ON NEW PROPOSALS + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: Some(SingleChoiceAutoVote { + vote: Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("approver"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Extension { + msg: dppas_v241::msg::ExecuteExt::Approve { id: 3 }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_single::msg::ExecuteMsg::Execute { proposal_id: 3 }, + &[], + ) + .unwrap(); + let proposal: ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dao_proposal_single::msg::QueryMsg::Proposal { proposal_id: 3 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, Status::Executed); +} + +#[test] +fn test_migrate_from_v241_with_policy_update() { + let app = &mut App::default(); + + let core_v241_contract = Box::new( + ContractWrapper::new( + core_v241::contract::execute, + core_v241::contract::instantiate, + core_v241::contract::query, + ) + .with_reply(core_v241::contract::reply), + ); + let dvcw4_v241_contract = Box::new( + ContractWrapper::new( + dvcw4_v241::contract::execute, + dvcw4_v241::contract::instantiate, + dvcw4_v241::contract::query, + ) + .with_reply(dvcw4_v241::contract::reply), + ); + let dpps_v241_contract = Box::new(ContractWrapper::new( + dppas_v241::contract::execute, + dppas_v241::contract::instantiate, + dppas_v241::contract::query, + )); + let dps_v241_contract = Box::new( + ContractWrapper::new( + dps_v241::contract::execute, + dps_v241::contract::instantiate, + dps_v241::contract::query, + ) + .with_reply(dps_v241::contract::reply), ); - increase_allowance( - &mut app, - "ekez", - &pre_propose, - cw20_address.clone(), - Uint128::new(10), + let core_id = app.store_code(core_v241_contract); + let cw4_id = app.store_code(cw4_group_contract()); + let dvcw4_v241_id = app.store_code(dvcw4_v241_contract); + let dpps_v241_id = app.store_code(dpps_v241_contract); + let dps_v241_id = app.store_code(dps_v241_contract); + + let governance_instantiate = di_v241::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: di_v241::state::ModuleInstantiateInfo { + code_id: dvcw4_v241_id, + msg: to_json_binary(&dvcw4_v241::msg::InstantiateMsg { + group_contract: dvcw4_v241::msg::GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: "ekez".to_string(), + weight: 9, + }, + cw4::Member { + addr: "keze".to_string(), + weight: 8, + }, + ], + }, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![di_v241::state::ModuleInstantiateInfo { + code_id: dps_v241_id, + msg: to_json_binary(&dps_v241::msg::InstantiateMsg { + threshold: dv_v241::threshold::Threshold::AbsolutePercentage { + percentage: dv_v241::threshold::PercentageThreshold::Majority {}, + }, + max_voting_period: cw_utils::Duration::Time(86400), + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + pre_propose_info: dv_v241::pre_propose::PreProposeInfo::ModuleMayPropose { + info: di_v241::state::ModuleInstantiateInfo { + code_id: dpps_v241_id, + msg: to_json_binary(&dppas_v241::msg::InstantiateMsg { + deposit_info: None, + open_proposal_submission: false, + extension: dppas_v241::msg::InstantiateExt { + approver: "approver".to_string(), + }, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "baby's first pre-propose module".to_string(), + }, + }, + close_proposal_on_execution_failure: false, + veto: None, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let core_addr = app + .instantiate_contract( + core_id, + Addr::unchecked("ekez"), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + app.update_block(|block| block.height += 1); + + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &di_v241::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposal_modules.len(), 1); + let proposal_single = proposal_modules.into_iter().next().unwrap().address; + let proposal_creation_policy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + let pre_propose = match proposal_creation_policy { + dv_v241::pre_propose::ProposalCreationPolicy::Module { addr } => addr, + _ => panic!("expected a module for the proposal creation policy"), + }; + + // Make sure things were set up correctly. + assert_eq!( + proposal_single, + get_proposal_module(app, pre_propose.clone()) + ); + assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: "crates.io:dao-pre-propose-approval-single".to_string(), + version: "2.4.1".to_string() + }, + info, ); - let cw20_pre_propose_id = make_pre_proposal(&mut app, pre_propose.clone(), "ekez", &[]); - // Approver approves - let cw20_id = approve_proposal( - &mut app, + app.execute_contract( + Addr::unchecked("ekez"), pre_propose.clone(), - "approver", - cw20_pre_propose_id, - ); + &dppas_v241::msg::ExecuteMsg::Propose { + msg: dppas_v241::msg::ProposeMessage::Propose { + title: "title1".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: Some(dv_v241::voting::SingleChoiceAutoVote { + vote: dv_v241::voting::Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("approver"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Extension { + msg: dppas_v241::msg::ExecuteExt::Approve { id: 1 }, + }, + &[], + ) + .unwrap(); - // There is now a pending proposal and cw20 tokens in the - // pre-propose module that should be returned on that proposal's - // completion. To make things interesting, we withdraw those - // tokens which should cause the status change hook on the - // proposal's execution to fail as we don't have sufficent balance - // to return the deposit. - withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); - let balance = get_balance_cw20(&app, &cw20_address, core_addr.as_str()); - assert_eq!(balance, Uint128::new(10)); + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); - // Proposal should still be executable! We just get removed from - // the proposal module's hook receiver list. - vote( - &mut app, + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Passed); + assert_eq!(proposal.proposal.proposer, Addr::unchecked("ekez")); + assert_eq!(proposal.proposal.title, "title1".to_string()); + assert_eq!(proposal.proposal.description, "d".to_string()); + assert_eq!(proposal.proposal.msgs, vec![]); + + app.execute_contract( + Addr::unchecked("ekez"), proposal_single.clone(), - "ekez", - cw20_id, - Vote::Yes, - ); - execute_proposal(&mut app, proposal_single.clone(), "ekez", cw20_id); + &dps_v241::msg::ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap(); - // Make sure the proposal module has fallen back to anyone can - // propose becuase of our malfunction. - let proposal_creation_policy: ProposalCreationPolicy = app + let proposal: dps_v241::query::ProposalResponse = app .wrap() .query_wasm_smart( proposal_single.clone(), - &dao_proposal_single::msg::QueryMsg::ProposalCreationPolicy {}, + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, ) .unwrap(); - assert_eq!(proposal_creation_policy, ProposalCreationPolicy::Anyone {}); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); - // Close out the native proposal and it's deposit as well. - vote( - &mut app, + // UPGRADE ONLY PRE-PROPOSE TO LATEST VIA DAO PROPOSAL WITH POLICY UPDATE + + let dppas_latest_id = app.store_code(dao_pre_propose_approval_single_contract()); + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Propose { + msg: dppas_v241::msg::ProposeMessage::Propose { + title: "upgrade pre-propose-single from v2.4.1".to_string(), + description: "d".to_string(), + msgs: vec![CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: pre_propose.to_string(), + new_code_id: dppas_latest_id, + msg: to_json_binary(&MigrateMsg::FromUnderV250 { + policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("noob")], + denylist: vec![], + }), + }) + .unwrap(), + })], + vote: Some(dv_v241::voting::SingleChoiceAutoVote { + vote: dv_v241::voting::Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("approver"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Extension { + msg: dppas_v241::msg::ExecuteExt::Approve { id: 2 }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), proposal_single.clone(), - "ekez", - native_id, - Vote::No, + &dps_v241::msg::ExecuteMsg::Execute { proposal_id: 2 }, + &[], + ) + .unwrap(); + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 2 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); + + // MAKE SURE PRE PROPOSE INFO CHANGED + + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string() + }, + info, ); - close_proposal(&mut app, proposal_single.clone(), "ekez", native_id); - withdraw( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + + // MAKE SURE PRE PROPOSE CONFIG WAS UPDATED + + let config: Config = app + .wrap() + .query_wasm_smart(pre_propose.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("noob")], + denylist: vec![] + } + }, + config ); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(30)); + + // NOW MAKE SURE ONLY NOOB CAN MAKE PROPOSALS + + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + app.execute_contract( + Addr::unchecked("noob"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("approver"), + pre_propose.clone(), + &dppas_v241::msg::ExecuteMsg::Extension { + msg: dppas_v241::msg::ExecuteExt::Approve { id: 3 }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_single::msg::ExecuteMsg::Vote { + proposal_id: 3, + vote: Vote::Yes, + rationale: None, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_single::msg::ExecuteMsg::Execute { proposal_id: 3 }, + &[], + ) + .unwrap(); + let proposal: ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dao_proposal_single::msg::QueryMsg::Proposal { proposal_id: 3 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, Status::Executed); } diff --git a/contracts/pre-propose/dao-pre-propose-approver/examples/schema.rs b/contracts/pre-propose/dao-pre-propose-approver/examples/schema.rs index 51cb0cbf3..fd26dfa01 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/examples/schema.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/examples/schema.rs @@ -1,10 +1,11 @@ use cosmwasm_schema::write_api; -use dao_pre_propose_approver::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use dao_pre_propose_approver::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; fn main() { write_api! { instantiate: InstantiateMsg, query: QueryMsg, execute: ExecuteMsg, + migrate: MigrateMsg, } } 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 ef387da81..8dc9a227c 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -51,11 +51,9 @@ "properties": { "update_config": { "type": "object", - "required": [ - "open_proposal_submission" - ], "properties": { "deposit_info": { + "description": "If None, will remove the deposit. Backwards compatible.", "anyOf": [ { "$ref": "#/definitions/UncheckedDepositInfo" @@ -65,8 +63,79 @@ } ] }, - "open_proposal_submission": { - "type": "boolean" + "submission_policy": { + "description": "If None, will leave the submission policy in the config as-is.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Perform more granular submission policy updates to allow for atomic operations that don't override others.", + "type": "object", + "required": [ + "update_submission_policy" + ], + "properties": { + "update_submission_policy": { + "type": "object", + "properties": { + "allowlist_add": { + "description": "If using specific policy, optionally add to the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "allowlist_remove": { + "description": "If using specific policy, optionally remove from the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_add": { + "description": "Optionally add to the denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_remove": { + "description": "Optionally remove from denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "set_dao_members": { + "description": "If using specific policy, optionally update the `dao_members` flag.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -197,6 +266,10 @@ } ], "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" + }, "ApproverProposeMessage": { "oneOf": [ { @@ -370,6 +443,76 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Status": { "oneOf": [ { @@ -569,6 +712,20 @@ }, "additionalProperties": false }, + { + "description": "Returns contract version info.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the module's configuration.", "type": "object", @@ -607,6 +764,28 @@ }, "additionalProperties": false }, + { + "description": "Returns whether or not the address can submit proposals.", + "type": "object", + "required": [ + "can_propose" + ], + "properties": { + "can_propose": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns list of proposal submitted hooks.", "type": "object", @@ -710,15 +889,152 @@ } } }, - "migrate": null, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "from_under_v250" + ], + "properties": { + "from_under_v250": { + "type": "object", + "properties": { + "policy": { + "description": "Optionally set a new submission policy with more granular controls. If not set, the current policy will remain.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "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" + }, + "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" + }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, "sudo": null, "responses": { + "can_propose": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", "required": [ - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -732,9 +1048,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -835,6 +1155,76 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "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" @@ -979,6 +1369,40 @@ } } }, + "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 + } + } + }, "proposal_module": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Addr", 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 119928320..268d3bae8 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs @@ -11,11 +11,12 @@ use dao_pre_propose_approval_single::msg::{ ApproverProposeMessage, ExecuteExt as ApprovalExt, ExecuteMsg as PreProposeApprovalExecuteMsg, }; use dao_pre_propose_base::{error::PreProposeError, state::PreProposeContract}; +use dao_voting::pre_propose::PreProposeSubmissionPolicy; use dao_voting::status::Status; use crate::msg::{ - BaseInstantiateMsg, ExecuteExt, ExecuteMsg, InstantiateMsg, ProposeMessageInternal, QueryExt, - QueryMsg, + BaseInstantiateMsg, ExecuteExt, ExecuteMsg, InstantiateMsg, MigrateMsg, ProposeMessageInternal, + QueryExt, QueryMsg, }; use crate::state::{ PRE_PROPOSE_APPROVAL_CONTRACT, PRE_PROPOSE_ID_TO_PROPOSAL_ID, PROPOSAL_ID_TO_PRE_PROPOSE_ID, @@ -24,7 +25,7 @@ use crate::state::{ pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-pre-propose-approver"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -type PrePropose = PreProposeContract; +type PrePropose = PreProposeContract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -33,11 +34,16 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - // This contract does not handle deposits or have open submissions - // Here we hardcode the pre-propose-base instantiate message + // This contract does not handle deposits or allow submission permissions + // since only the approval-single contract can create proposals. Just + // hardcode the pre-propose-base instantiate message. let base_instantiate_msg = BaseInstantiateMsg { deposit_info: None, - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: Empty {}, }; // Default pre-propose-base instantiation @@ -92,6 +98,9 @@ pub fn execute( ExecuteMsg::Extension { msg } => match msg { ExecuteExt::ResetApprover {} => execute_reset_approver(deps, env, info), }, + // Override config updates since they don't apply. + ExecuteMsg::UpdateConfig { .. } => Err(PreProposeError::Unsupported {}), + ExecuteMsg::UpdateSubmissionPolicy { .. } => Err(PreProposeError::Unsupported {}), _ => PrePropose::default().execute(deps, env, info, msg), } } @@ -107,7 +116,7 @@ pub fn execute_propose( return Err(PreProposeError::Unauthorized {}); } - // Get pre_prospose_id, transform proposal for the approver + // Get pre_propose_id, transform proposal for the approver // Here we make sure that there are no messages that can be executed let (pre_propose_id, sanitized_msg) = match msg { ApproverProposeMessage::Propose { @@ -228,6 +237,11 @@ pub fn execute_reset_approver( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { + QueryMsg::CanPropose { address } => { + let approval_contract = PRE_PROPOSE_APPROVAL_CONTRACT.load(deps.storage)?; + let can_propose = address == approval_contract; + to_json_binary(&can_propose) + } QueryMsg::QueryExtension { msg } => match msg { QueryExt::PreProposeApprovalContract {} => { to_json_binary(&PRE_PROPOSE_APPROVAL_CONTRACT.load(deps.storage)?) @@ -242,3 +256,10 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { _ => PrePropose::default().query(deps, env, msg), } } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(mut deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + let res = PrePropose::default().migrate(deps.branch(), msg); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + res +} 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 7d4529856..5482fad8c 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/msg.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/msg.rs @@ -2,7 +2,8 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{CosmosMsg, Empty}; use dao_pre_propose_approval_single::msg::ApproverProposeMessage; use dao_pre_propose_base::msg::{ - ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, QueryMsg as QueryBase, + ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, MigrateMsg as MigrateBase, + QueryMsg as QueryBase, }; #[cw_serde] @@ -31,6 +32,7 @@ pub enum QueryExt { pub type BaseInstantiateMsg = InstantiateBase; pub type ExecuteMsg = ExecuteBase; pub type QueryMsg = QueryBase; +pub type MigrateMsg = MigrateBase; /// Internal version of the propose message that includes the /// `proposer` field. The module will fill this in based on the sender 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 fbef73e87..c1475939a 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs @@ -3,6 +3,8 @@ use cw2::ContractVersion; use cw20::Cw20Coin; use cw_denom::UncheckedDenom; use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor}; +use dao_interface::proposal::InfoResponse; +use dao_voting::pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}; use dps::query::{ProposalListResponse, ProposalResponse}; use dao_interface::state::ProposalModule; @@ -79,6 +81,16 @@ fn get_proposal_module_approval_single_instantiate( ) -> dps::msg::InstantiateMsg { let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + } + }; + dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { percentage: PercentageThreshold::Majority {}, @@ -92,7 +104,7 @@ fn get_proposal_module_approval_single_instantiate( code_id: pre_propose_id, msg: to_json_binary(&InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: InstantiateExt { approver: APPROVER.to_string(), }, @@ -289,6 +301,15 @@ fn setup_default_test( approver_core_addr, get_dao(app, pre_propose_approver.clone()) ); + assert_eq!( + InfoResponse { + info: ContractVersion { + contract: "crates.io:dao-pre-propose-approver".to_string(), + version: env!("CARGO_PKG_VERSION").to_string() + } + }, + get_info(app, pre_propose_approver.clone()) + ); DefaultTestSetup { core_addr, @@ -407,6 +428,12 @@ fn get_dao(app: &App, module: Addr) -> Addr { .unwrap() } +fn get_info(app: &App, module: Addr) -> InfoResponse { + app.wrap() + .query_wasm_smart(module, &QueryMsg::Info {}) + .unwrap() +} + fn get_proposal_module(app: &App, module: Addr) -> Addr { app.wrap() .query_wasm_smart(module, &QueryMsg::ProposalModule {}) @@ -446,19 +473,30 @@ fn get_latest_proposal_id(app: &App, module: Addr) -> u64 { props.proposals[props.proposals.len() - 1].id } +fn query_can_propose(app: &App, module: Addr, address: impl Into) -> bool { + app.wrap() + .query_wasm_smart( + module, + &QueryMsg::CanPropose { + address: address.into(), + }, + ) + .unwrap() +} + fn update_config( app: &mut App, module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> Config { app.execute_contract( Addr::unchecked(sender), module.clone(), &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -472,14 +510,14 @@ fn update_config_should_fail( module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> PreProposeError { app.execute_contract( Addr::unchecked(sender), module, &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -1160,7 +1198,10 @@ fn test_permissions() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); } #[test] @@ -1316,7 +1357,11 @@ fn test_update_config() { config, Config { deposit_info: None, - open_proposal_submission: false + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + } } ); @@ -1343,7 +1388,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never, }), - true, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, ); let config = get_config(&app, pre_propose.clone()); @@ -1355,7 +1400,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never }), - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, } ); @@ -1407,9 +1452,139 @@ fn test_update_config() { assert_eq!(balance, Uint128::new(0)); // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + proposal_single.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + ); assert_eq!(err, PreProposeError::NotDao {}); + + // Errors when no one is authorized to create proposals. + let err = update_config_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![], + denylist: vec![], + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Errors when allowlist and denylist overlap. + let err = update_config_should_fail( + &mut app, + pre_propose, + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![Addr::unchecked("ekez")], + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} + +#[test] +fn test_approver_unsupported_update_config() { + let mut app = App::default(); + + // Need to instantiate this so contract addresses match with cw20 test cases + let _ = instantiate_cw20_base_default(&mut app); + + let DefaultTestSetup { + core_addr, + pre_propose_approver, + .. + } = setup_default_test(&mut app, None, true); + + // Should fail because config is not supported for the approver pre-propose + // contract. + let err = update_config_should_fail( + &mut app, + pre_propose_approver, + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![], + }, + ); + assert_eq!(err, PreProposeError::Unsupported {}); +} + +#[test] +fn test_approver_unsupported_update_submission_policy() { + let mut app = App::default(); + + // Need to instantiate this so contract addresses match with cw20 test cases + let _ = instantiate_cw20_base_default(&mut app); + + let DefaultTestSetup { + core_addr, + pre_propose_approver, + .. + } = setup_default_test(&mut app, None, true); + + // Should fail because submission policy is not supported for the approver + // pre-propose contract. + let err: PreProposeError = app + .execute_contract( + core_addr, + pre_propose_approver, + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, PreProposeError::Unsupported {}); +} + +#[test] +fn test_approver_can_propose() { + let mut app = App::default(); + + // Need to instantiate this so contract addresses match with cw20 test cases + let _ = instantiate_cw20_base_default(&mut app); + + let DefaultTestSetup { + pre_propose, + pre_propose_approver, + .. + } = setup_default_test(&mut app, None, true); + + // Only the pre-propose-approval-single contract can propose. + assert!(query_can_propose( + &app, + pre_propose_approver.clone(), + pre_propose + )); + assert!(!query_can_propose( + &app, + pre_propose_approver, + "someone_else" + )); } #[test] @@ -1459,7 +1634,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, ); // Withdraw with no specified denom - should fall back to the one @@ -1508,7 +1687,11 @@ fn test_withdraw() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Always, }), - false, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, ); increase_allowance( diff --git a/contracts/pre-propose/dao-pre-propose-multiple/Cargo.toml b/contracts/pre-propose/dao-pre-propose-multiple/Cargo.toml index 670c0f281..0aa8ba140 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/Cargo.toml +++ b/contracts/pre-propose/dao-pre-propose-multiple/Cargo.toml @@ -26,6 +26,7 @@ dao-voting = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } cw-utils = { workspace = true } +cw4 = { workspace = true } cw4-group = { workspace = true } cw20 = { workspace = true } cw20-base = { workspace = true } @@ -38,3 +39,11 @@ cw-denom = { workspace = true } dao-interface = { workspace = true } dao-testing = { workspace = true } dao-hooks = { workspace = true } + +# v2.4.1 migration +dao-dao-core-v241 = { workspace = true } +dao-interface-v241 = { workspace = true } +dao-pre-propose-multiple-v241 = { workspace = true } +dao-proposal-multiple-v241 = { workspace = true } +dao-voting-cw4-v241 = { workspace = true } +dao-voting-v241 = { workspace = true } diff --git a/contracts/pre-propose/dao-pre-propose-multiple/examples/schema.rs b/contracts/pre-propose/dao-pre-propose-multiple/examples/schema.rs index e75470342..e03b233b7 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/examples/schema.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/examples/schema.rs @@ -1,12 +1,11 @@ use cosmwasm_schema::write_api; -use cosmwasm_std::Empty; -use dao_pre_propose_base::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use dao_pre_propose_multiple::ProposeMessage; +use dao_pre_propose_multiple::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; fn main() { write_api! { - instantiate: InstantiateMsg, - query: QueryMsg, - execute: ExecuteMsg, + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg } } 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 577862b00..7e2958b4d 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,7 @@ "type": "object", "required": [ "extension", - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -30,13 +30,21 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "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" + }, "DepositRefundPolicy": { "oneOf": [ { @@ -115,6 +123,76 @@ "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" }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "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" @@ -230,11 +308,9 @@ "properties": { "update_config": { "type": "object", - "required": [ - "open_proposal_submission" - ], "properties": { "deposit_info": { + "description": "If None, will remove the deposit. Backwards compatible.", "anyOf": [ { "$ref": "#/definitions/UncheckedDepositInfo" @@ -244,8 +320,79 @@ } ] }, - "open_proposal_submission": { - "type": "boolean" + "submission_policy": { + "description": "If None, will leave the submission policy in the config as-is.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Perform more granular submission policy updates to allow for atomic operations that don't override others.", + "type": "object", + "required": [ + "update_submission_policy" + ], + "properties": { + "update_submission_policy": { + "type": "object", + "properties": { + "allowlist_add": { + "description": "If using specific policy, optionally add to the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "allowlist_remove": { + "description": "If using specific policy, optionally remove from the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_add": { + "description": "Optionally add to the denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_remove": { + "description": "Optionally remove from denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "set_dao_members": { + "description": "If using specific policy, optionally update the `dao_members` flag.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -376,6 +523,10 @@ } ], "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": [ @@ -565,6 +716,10 @@ } ] }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "DepositRefundPolicy": { "oneOf": [ { @@ -685,6 +840,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -773,6 +953,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -1008,6 +1218,76 @@ }, "additionalProperties": false }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "ProposeMessage": { "oneOf": [ { @@ -1399,6 +1679,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -1483,6 +1817,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -1518,6 +1867,20 @@ }, "additionalProperties": false }, + { + "description": "Returns contract version info.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the module's configuration.", "type": "object", @@ -1556,6 +1919,28 @@ }, "additionalProperties": false }, + { + "description": "Returns whether or not the address can submit proposals.", + "type": "object", + "required": [ + "can_propose" + ], + "properties": { + "can_propose": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns list of proposal submitted hooks.", "type": "object", @@ -1600,15 +1985,152 @@ } } }, - "migrate": null, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "from_under_v250" + ], + "properties": { + "from_under_v250": { + "type": "object", + "properties": { + "policy": { + "description": "Optionally set a new submission policy with more granular controls. If not set, the current policy will remain.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "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" + }, + "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" + }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, "sudo": null, "responses": { + "can_propose": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", "required": [ - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -1622,9 +2144,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -1725,6 +2251,76 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "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" @@ -1869,6 +2465,40 @@ } } }, + "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 + } + } + }, "proposal_module": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Addr", diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs index 30e119fbc..082d129e0 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs @@ -6,7 +6,10 @@ use cw2::set_contract_version; use dao_pre_propose_base::{ error::PreProposeError, - msg::{ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, QueryMsg as QueryBase}, + msg::{ + ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, MigrateMsg as MigrateBase, + QueryMsg as QueryBase, + }, state::PreProposeContract, }; use dao_voting::{ @@ -30,6 +33,7 @@ pub enum ProposeMessage { pub type InstantiateMsg = InstantiateBase; pub type ExecuteMsg = ExecuteBase; pub type QueryMsg = QueryBase; +pub type MigrateMsg = MigrateBase; /// Internal version of the propose message that includes the /// `proposer` field. The module will fill this in based on the sender @@ -39,7 +43,7 @@ enum ProposeMessageInternal { Propose(ProposeMsg), } -type PrePropose = PreProposeContract; +type PrePropose = PreProposeContract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -87,10 +91,23 @@ pub fn execute( ExecuteMsg::Withdraw { denom } => ExecuteInternal::Withdraw { denom }, ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy, } => ExecuteInternal::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy, + }, + ExecuteMsg::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, + } => ExecuteInternal::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, }, ExecuteMsg::AddProposalSubmittedHook { address } => { ExecuteInternal::AddProposalSubmittedHook { address } @@ -114,3 +131,10 @@ pub fn execute( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { PrePropose::default().query(deps, env, msg) } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(mut deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + let res = PrePropose::default().migrate(deps.branch(), msg); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + res +} diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/lib.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/lib.rs index 16d6ba5ae..33df46b46 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/lib.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/lib.rs @@ -5,7 +5,7 @@ pub mod contract; #[cfg(test)] mod tests; -pub use contract::{ExecuteMsg, InstantiateMsg, ProposeMessage, QueryMsg}; +pub use contract::{ExecuteMsg, InstantiateMsg, MigrateMsg, ProposeMessage, QueryMsg}; // Exporting these means that contracts interacting with this one don't // need an explicit dependency on the base contract to read queries. 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 56f310ee2..f949702c2 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs @@ -1,15 +1,20 @@ -use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Coin, Decimal, Empty, Uint128}; +use cosmwasm_std::{ + coins, from_json, to_json_binary, Addr, Coin, CosmosMsg, Decimal, Empty, Uint128, WasmMsg, +}; use cpm::query::ProposalResponse; use cw2::ContractVersion; use cw20::Cw20Coin; use cw_denom::UncheckedDenom; use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor}; use cw_utils::Duration; +use dao_interface::proposal::InfoResponse; 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_multiple as cpm; -use dao_testing::helpers::instantiate_with_cw4_groups_governance; +use dao_testing::{contracts::cw4_group_contract, helpers::instantiate_with_cw4_groups_governance}; +use dao_voting::multiple_choice::MultipleChoiceAutoVote; +use dao_voting::pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, multiple_choice::{ @@ -21,9 +26,17 @@ use dao_voting::{ threshold::PercentageThreshold, }; +// test v2.4.1 migration +use dao_dao_core_v241 as core_v241; +use dao_interface_v241 as di_v241; +use dao_pre_propose_multiple_v241 as dppm_v241; +use dao_proposal_multiple_v241 as dpm_v241; +use dao_voting_cw4_v241 as dvcw4_v241; +use dao_voting_v241 as dv_v241; + use crate::contract::*; -fn cw_dao_proposal_multiple_contract() -> Box> { +fn dao_proposal_multiple_contract() -> Box> { let contract = ContractWrapper::new( cpm::contract::execute, cpm::contract::instantiate, @@ -33,8 +46,8 @@ fn cw_dao_proposal_multiple_contract() -> Box> { Box::new(contract) } -fn cw_pre_propose_base_proposal_single() -> Box> { - let contract = ContractWrapper::new(execute, instantiate, query); +fn dao_pre_propose_multiple_contract() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query).with_migrate(migrate); Box::new(contract) } @@ -52,7 +65,17 @@ fn get_default_proposal_module_instantiate( deposit_info: Option, open_proposal_submission: bool, ) -> cpm::msg::InstantiateMsg { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_multiple_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + } + }; cpm::msg::InstantiateMsg { voting_strategy: VotingStrategy::SingleChoice { @@ -67,7 +90,7 @@ fn get_default_proposal_module_instantiate( code_id: pre_propose_id, msg: to_json_binary(&InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), @@ -115,7 +138,7 @@ fn setup_default_test( deposit_info: Option, open_proposal_submission: bool, ) -> DefaultTestSetup { - let cpm_id = app.store_code(cw_dao_proposal_multiple_contract()); + let cpm_id = app.store_code(dao_proposal_multiple_contract()); let proposal_module_instantiate = get_default_proposal_module_instantiate(app, deposit_info, open_proposal_submission); @@ -167,6 +190,15 @@ fn setup_default_test( get_proposal_module(app, pre_propose.clone()) ); assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + assert_eq!( + InfoResponse { + info: ContractVersion { + contract: "crates.io:dao-pre-propose-multiple".to_string(), + version: env!("CARGO_PKG_VERSION").to_string() + } + }, + get_info(app, pre_propose.clone()) + ); DefaultTestSetup { core_addr, @@ -339,6 +371,12 @@ fn get_dao(app: &App, module: Addr) -> Addr { .unwrap() } +fn get_info(app: &App, module: Addr) -> InfoResponse { + app.wrap() + .query_wasm_smart(module, &QueryMsg::Info {}) + .unwrap() +} + fn get_proposal_module(app: &App, module: Addr) -> Addr { app.wrap() .query_wasm_smart(module, &QueryMsg::ProposalModule {}) @@ -351,19 +389,30 @@ fn get_deposit_info(app: &App, module: Addr, id: u64) -> DepositInfoResponse { .unwrap() } +fn query_can_propose(app: &App, module: Addr, address: impl Into) -> bool { + app.wrap() + .query_wasm_smart( + module, + &QueryMsg::CanPropose { + address: address.into(), + }, + ) + .unwrap() +} + fn update_config( app: &mut App, module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> Config { app.execute_contract( Addr::unchecked(sender), module.clone(), &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -377,14 +426,14 @@ fn update_config_should_fail( module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> PreProposeError { app.execute_contract( Addr::unchecked(sender), module, &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -878,7 +927,10 @@ fn test_permissions() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}) + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ) } #[test] @@ -985,7 +1037,10 @@ fn test_no_deposit_required_members_submission() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); let id = make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); let new_status = vote( @@ -998,6 +1053,260 @@ fn test_no_deposit_required_members_submission() { assert_eq!(Status::Passed, new_status) } +#[test] +fn test_anyone_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + ); + + let rando = "rando"; + + // Proposal succeeds when anyone can propose. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked(rando)], + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + choices: MultipleChoiceOptions { + options: vec![MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposing succeeds if not on denylist. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); +} + +#[test] +fn test_specific_allowlist_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + // Proposal succeeds for member. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + "ekez", + &[], + ); + + let rando = "rando"; + + // Proposing fails for non-member. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + choices: MultipleChoiceOptions { + options: vec![MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![], + }, + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![Addr::unchecked("ekez")], + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + choices: MultipleChoiceOptions { + options: vec![MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![], + }, + ); + + // Proposing fails if members not allowed. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + choices: MultipleChoiceOptions { + options: vec![MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); +} + #[test] fn test_execute_extension_does_nothing() { let mut app = App::default(); @@ -1035,10 +1344,10 @@ fn test_execute_extension_does_nothing() { fn test_instantiate_with_zero_native_deposit() { let mut app = App::default(); - let cpm_id = app.store_code(cw_dao_proposal_multiple_contract()); + let cpm_id = app.store_code(dao_proposal_multiple_contract()); let proposal_module_instantiate = { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_multiple_contract()); cpm::msg::InstantiateMsg { voting_strategy: VotingStrategy::SingleChoice { @@ -1059,7 +1368,11 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: Empty::default(), }) .unwrap(), @@ -1098,10 +1411,10 @@ fn test_instantiate_with_zero_cw20_deposit() { let cw20_addr = instantiate_cw20_base_default(&mut app); - let cpm_id = app.store_code(cw_dao_proposal_multiple_contract()); + let cpm_id = app.store_code(dao_proposal_multiple_contract()); let proposal_module_instantiate = { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_multiple_contract()); cpm::msg::InstantiateMsg { voting_strategy: VotingStrategy::SingleChoice { @@ -1122,7 +1435,11 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: Empty::default(), }) .unwrap(), @@ -1168,7 +1485,11 @@ fn test_update_config() { config, Config { deposit_info: None, - open_proposal_submission: false + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + } } ); @@ -1191,7 +1512,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never, }), - true, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, ); let config = get_config(&app, pre_propose.clone()); @@ -1203,7 +1524,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never }), - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, } ); @@ -1261,161 +1582,1485 @@ fn test_update_config() { assert_eq!(balance, Uint128::new(0)); // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); - assert_eq!(err, PreProposeError::NotDao {}); -} - -#[test] -fn test_withdraw() { - let mut app = App::default(); - - let DefaultTestSetup { - core_addr, - proposal_single, - pre_propose, - } = setup_default_test(&mut app, None, false); - - let err = withdraw_should_fail( + let err = update_config_should_fail( &mut app, pre_propose.clone(), proposal_single.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + None, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, ); assert_eq!(err, PreProposeError::NotDao {}); - let err = withdraw_should_fail( + // Errors when no one is authorized to create proposals. + let err = update_config_should_fail( &mut app, pre_propose.clone(), core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![], + denylist: vec![], + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) ); - assert_eq!(err, PreProposeError::NothingToWithdraw {}); - - let err = withdraw_should_fail(&mut app, pre_propose.clone(), core_addr.as_str(), None); - assert_eq!(err, PreProposeError::NoWithdrawalDenom {}); - // Turn on native deposits. - update_config( + // Errors when allowlist and denylist overlap. + let err = update_config_should_fail( &mut app, - pre_propose.clone(), + pre_propose, core_addr.as_str(), - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, - ); + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![Addr::unchecked("ekez")], + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} + +#[test] +fn test_update_submission_policy() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, true); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + } + ); + + // Only the core module can update the submission policy. + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, PreProposeError::NotDao {}); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked("ekez")], + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + } + ); + + // Error if try to change Specific fields when set to Anyone. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(true), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + + // Change to Specific policy. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateConfig { + deposit_info: None, + submission_policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + } + ); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![Addr::unchecked("ekez")], + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + }, + } + ); + + // Append to allowlist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![], + }, + } + ); + + // Add and remove to/from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["someone".to_string(), "else".to_string()]), + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + denylist: vec![], + }, + } + ); + + // Remove from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + }, + } + ); + + // Setting dao_members to false fails if allowlist is empty. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Set dao_members to false and add allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![] + }, + } + ); + + // Errors when allowlist and denylist overlap. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} + +#[test] +fn test_withdraw() { + let mut app = App::default(); + + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + let err = withdraw_should_fail( + &mut app, + pre_propose.clone(), + proposal_single.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + assert_eq!(err, PreProposeError::NotDao {}); + + let err = withdraw_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + assert_eq!(err, PreProposeError::NothingToWithdraw {}); + + let err = withdraw_should_fail(&mut app, pre_propose.clone(), core_addr.as_str(), None); + assert_eq!(err, PreProposeError::NoWithdrawalDenom {}); + + // Turn on native deposits. + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDepositInfo { + denom: DepositToken::Token { + denom: UncheckedDenom::Native("ujuno".to_string()), + }, + amount: Uint128::new(10), + refund_policy: DepositRefundPolicy::Always, + }), + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + // Withdraw with no specified denom - should fall back to the one + // in the config. + mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); + withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(10)); + + // Withdraw again, this time specifying a native denomination. + mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); + withdraw( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(20)); + + // Make a proposal with the native tokens to put some in the system. + mint_natives(&mut app, "ekez", coins(10, "ujuno")); + let native_id = make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + "ekez", + &coins(10, "ujuno"), + ); + + // Update the config to use a cw20 token. + let cw20_address = instantiate_cw20_base_default(&mut app); + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDepositInfo { + denom: DepositToken::Token { + denom: UncheckedDenom::Cw20(cw20_address.to_string()), + }, + amount: Uint128::new(10), + refund_policy: DepositRefundPolicy::Always, + }), + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + increase_allowance( + &mut app, + "ekez", + &pre_propose, + cw20_address.clone(), + Uint128::new(10), + ); + let cw20_id = make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + "ekez", + &[], + ); + + // There is now a pending proposal and cw20 tokens in the + // pre-propose module that should be returned on that proposal's + // completion. Execute an early withdraw and make sure things play + // out correctly. + + withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); + let balance = get_balance_cw20(&app, &cw20_address, core_addr.as_str()); + assert_eq!(balance, Uint128::new(10)); + + // Proposal should still be executable! We just get removed from + // the proposal module's hook receiver list. + vote( + &mut app, + proposal_single.clone(), + "ekez", + cw20_id, + MultipleChoiceVote { option_id: 0 }, + ); + execute_proposal(&mut app, proposal_single.clone(), "ekez", cw20_id); + + // Make sure the proposal module has fallen back to anyone can + // propose becuase of our malfunction. + let proposal_creation_policy: ProposalCreationPolicy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &cpm::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + assert_eq!(proposal_creation_policy, ProposalCreationPolicy::Anyone {}); + + // Close out the native proposal and it's deposit as well. + vote( + &mut app, + proposal_single.clone(), + "ekez", + native_id, + MultipleChoiceVote { option_id: 2 }, + ); + close_proposal(&mut app, proposal_single.clone(), "ekez", native_id); + withdraw( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(30)); +} + +#[test] +fn test_migrate_from_v241() { + let app = &mut App::default(); + + let core_v241_contract = Box::new( + ContractWrapper::new( + core_v241::contract::execute, + core_v241::contract::instantiate, + core_v241::contract::query, + ) + .with_reply(core_v241::contract::reply), + ); + let dvcw4_v241_contract = Box::new( + ContractWrapper::new( + dvcw4_v241::contract::execute, + dvcw4_v241::contract::instantiate, + dvcw4_v241::contract::query, + ) + .with_reply(dvcw4_v241::contract::reply), + ); + let dppm_v241_contract = Box::new(ContractWrapper::new( + dppm_v241::contract::execute, + dppm_v241::contract::instantiate, + dppm_v241::contract::query, + )); + let dpm_v241_contract = Box::new( + ContractWrapper::new( + dpm_v241::contract::execute, + dpm_v241::contract::instantiate, + dpm_v241::contract::query, + ) + .with_reply(dpm_v241::contract::reply), + ); + + let core_id = app.store_code(core_v241_contract); + let cw4_id = app.store_code(cw4_group_contract()); + let dvcw4_v241_id = app.store_code(dvcw4_v241_contract); + let dppm_v241_id = app.store_code(dppm_v241_contract); + let dpm_v241_id = app.store_code(dpm_v241_contract); + + let governance_instantiate = di_v241::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: di_v241::state::ModuleInstantiateInfo { + code_id: dvcw4_v241_id, + msg: to_json_binary(&dvcw4_v241::msg::InstantiateMsg { + group_contract: dvcw4_v241::msg::GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: "ekez".to_string(), + weight: 9, + }, + cw4::Member { + addr: "keze".to_string(), + weight: 8, + }, + ], + }, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![di_v241::state::ModuleInstantiateInfo { + code_id: dpm_v241_id, + msg: to_json_binary(&dpm_v241::msg::InstantiateMsg { + voting_strategy: dv_v241::multiple_choice::VotingStrategy::SingleChoice { + quorum: dv_v241::threshold::PercentageThreshold::Majority {}, + }, + max_voting_period: cw_utils::Duration::Time(86400), + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + pre_propose_info: dv_v241::pre_propose::PreProposeInfo::ModuleMayPropose { + info: di_v241::state::ModuleInstantiateInfo { + code_id: dppm_v241_id, + msg: to_json_binary(&dppm_v241::InstantiateMsg { + deposit_info: None, + open_proposal_submission: true, + extension: Empty::default(), + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "baby's first pre-propose module".to_string(), + }, + }, + close_proposal_on_execution_failure: false, + veto: None, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let core_addr = app + .instantiate_contract( + core_id, + Addr::unchecked("ekez"), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + app.update_block(|block| block.height += 1); + + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &di_v241::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposal_modules.len(), 1); + let proposal_single = proposal_modules.into_iter().next().unwrap().address; + let proposal_creation_policy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dpm_v241::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + let pre_propose = match proposal_creation_policy { + dv_v241::pre_propose::ProposalCreationPolicy::Module { addr } => addr, + _ => panic!("expected a module for the proposal creation policy"), + }; + + // Make sure things were set up correctly. + assert_eq!( + proposal_single, + get_proposal_module(app, pre_propose.clone()) + ); + assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: "crates.io:dao-pre-propose-multiple".to_string(), + version: "2.4.1".to_string() + }, + info, + ); + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &dppm_v241::ExecuteMsg::Propose { + msg: dppm_v241::ProposeMessage::Propose { + title: "title1".to_string(), + description: "d".to_string(), + choices: dv_v241::multiple_choice::MultipleChoiceOptions { + options: vec![ + dv_v241::multiple_choice::MultipleChoiceOption { + title: "first".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + dv_v241::multiple_choice::MultipleChoiceOption { + title: "second".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + dv_v241::multiple_choice::MultipleChoiceOption { + title: "third".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + ], + }, + vote: Some(dv_v241::multiple_choice::MultipleChoiceAutoVote { + vote: dv_v241::multiple_choice::MultipleChoiceVote { option_id: 1 }, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + + let proposal: dpm_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dpm_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Passed); + assert_eq!(proposal.proposal.proposer, Addr::unchecked("ekez")); + assert_eq!(proposal.proposal.title, "title1".to_string()); + assert_eq!(proposal.proposal.description, "d".to_string()); + assert_eq!( + proposal.proposal.choices.len(), + // 3 + none of the above + 4 + ); + + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dpm_v241::msg::ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: dpm_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dpm_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); + + // UPGRADE ONLY PRE-PROPOSE TO LATEST VIA DAO PROPOSAL + + let dppm_latest_id = app.store_code(dao_pre_propose_multiple_contract()); + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &dppm_v241::ExecuteMsg::Propose { + msg: dppm_v241::ProposeMessage::Propose { + title: "upgrade pre-propose-multiple from v2.4.1".to_string(), + description: "d".to_string(), + choices: dv_v241::multiple_choice::MultipleChoiceOptions { + options: vec![ + dv_v241::multiple_choice::MultipleChoiceOption { + title: "first".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + dv_v241::multiple_choice::MultipleChoiceOption { + title: "second".to_string(), + description: "d".to_string(), + msgs: vec![CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: pre_propose.to_string(), + new_code_id: dppm_latest_id, + msg: to_json_binary(&MigrateMsg::FromUnderV250 { policy: None }) + .unwrap(), + })], + }, + dv_v241::multiple_choice::MultipleChoiceOption { + title: "third".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + ], + }, + vote: Some(dv_v241::multiple_choice::MultipleChoiceAutoVote { + vote: dv_v241::multiple_choice::MultipleChoiceVote { option_id: 1 }, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dpm_v241::msg::ExecuteMsg::Execute { proposal_id: 2 }, + &[], + ) + .unwrap(); + let proposal: dpm_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dpm_v241::msg::QueryMsg::Proposal { proposal_id: 2 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); + + // MAKE SURE PRE PROPOSE INFO CHANGED + + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string() + }, + info, + ); + + // MAKE SURE PRE PROPOSE CONFIG WAS UPDATED + + let config: Config = app + .wrap() + .query_wasm_smart(pre_propose.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + }, + config + ); + + // NOW MAKE SURE WE CAN MAKE AND VOTE ON NEW PROPOSALS + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + choices: MultipleChoiceOptions { + options: vec![ + MultipleChoiceOption { + title: "first".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + MultipleChoiceOption { + title: "second".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + MultipleChoiceOption { + title: "third".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + ], + }, + vote: Some(MultipleChoiceAutoVote { + vote: MultipleChoiceVote { option_id: 1 }, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_multiple::msg::ExecuteMsg::Execute { proposal_id: 3 }, + &[], + ) + .unwrap(); + let proposal: ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dao_proposal_multiple::msg::QueryMsg::Proposal { proposal_id: 3 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, Status::Executed); +} + +#[test] +fn test_migrate_from_v241_with_policy_update() { + let app = &mut App::default(); + + let core_v241_contract = Box::new( + ContractWrapper::new( + core_v241::contract::execute, + core_v241::contract::instantiate, + core_v241::contract::query, + ) + .with_reply(core_v241::contract::reply), + ); + let dvcw4_v241_contract = Box::new( + ContractWrapper::new( + dvcw4_v241::contract::execute, + dvcw4_v241::contract::instantiate, + dvcw4_v241::contract::query, + ) + .with_reply(dvcw4_v241::contract::reply), + ); + let dppm_v241_contract = Box::new(ContractWrapper::new( + dppm_v241::contract::execute, + dppm_v241::contract::instantiate, + dppm_v241::contract::query, + )); + let dpm_v241_contract = Box::new( + ContractWrapper::new( + dpm_v241::contract::execute, + dpm_v241::contract::instantiate, + dpm_v241::contract::query, + ) + .with_reply(dpm_v241::contract::reply), + ); + + let core_id = app.store_code(core_v241_contract); + let cw4_id = app.store_code(cw4_group_contract()); + let dvcw4_v241_id = app.store_code(dvcw4_v241_contract); + let dppm_v241_id = app.store_code(dppm_v241_contract); + let dpm_v241_id = app.store_code(dpm_v241_contract); + + let governance_instantiate = di_v241::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: di_v241::state::ModuleInstantiateInfo { + code_id: dvcw4_v241_id, + msg: to_json_binary(&dvcw4_v241::msg::InstantiateMsg { + group_contract: dvcw4_v241::msg::GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: "ekez".to_string(), + weight: 9, + }, + cw4::Member { + addr: "keze".to_string(), + weight: 8, + }, + ], + }, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![di_v241::state::ModuleInstantiateInfo { + code_id: dpm_v241_id, + msg: to_json_binary(&dpm_v241::msg::InstantiateMsg { + voting_strategy: dv_v241::multiple_choice::VotingStrategy::SingleChoice { + quorum: dv_v241::threshold::PercentageThreshold::Majority {}, + }, + max_voting_period: cw_utils::Duration::Time(86400), + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + pre_propose_info: dv_v241::pre_propose::PreProposeInfo::ModuleMayPropose { + info: di_v241::state::ModuleInstantiateInfo { + code_id: dppm_v241_id, + msg: to_json_binary(&dppm_v241::InstantiateMsg { + deposit_info: None, + open_proposal_submission: false, + extension: Empty::default(), + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "baby's first pre-propose module".to_string(), + }, + }, + close_proposal_on_execution_failure: false, + veto: None, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let core_addr = app + .instantiate_contract( + core_id, + Addr::unchecked("ekez"), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + app.update_block(|block| block.height += 1); + + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &di_v241::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); - // Withdraw with no specified denom - should fall back to the one - // in the config. - mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); - withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(10)); + assert_eq!(proposal_modules.len(), 1); + let proposal_single = proposal_modules.into_iter().next().unwrap().address; + let proposal_creation_policy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dpm_v241::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); - // Withdraw again, this time specifying a native denomination. - mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); - withdraw( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), - ); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(20)); + let pre_propose = match proposal_creation_policy { + dv_v241::pre_propose::ProposalCreationPolicy::Module { addr } => addr, + _ => panic!("expected a module for the proposal creation policy"), + }; - // Make a proposal with the native tokens to put some in the system. - mint_natives(&mut app, "ekez", coins(10, "ujuno")); - let native_id = make_proposal( - &mut app, - pre_propose.clone(), - proposal_single.clone(), - "ekez", - &coins(10, "ujuno"), + // Make sure things were set up correctly. + assert_eq!( + proposal_single, + get_proposal_module(app, pre_propose.clone()) + ); + assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: "crates.io:dao-pre-propose-multiple".to_string(), + version: "2.4.1".to_string() + }, + info, ); - // Update the config to use a cw20 token. - let cw20_address = instantiate_cw20_base_default(&mut app); - update_config( - &mut app, + app.execute_contract( + Addr::unchecked("ekez"), pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Cw20(cw20_address.to_string()), + &dppm_v241::ExecuteMsg::Propose { + msg: dppm_v241::ProposeMessage::Propose { + title: "title1".to_string(), + description: "d".to_string(), + choices: dv_v241::multiple_choice::MultipleChoiceOptions { + options: vec![ + dv_v241::multiple_choice::MultipleChoiceOption { + title: "first".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + dv_v241::multiple_choice::MultipleChoiceOption { + title: "second".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + dv_v241::multiple_choice::MultipleChoiceOption { + title: "third".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + ], + }, + vote: Some(dv_v241::multiple_choice::MultipleChoiceAutoVote { + vote: dv_v241::multiple_choice::MultipleChoiceVote { option_id: 1 }, + rationale: None, + }), }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, - ); + }, + &[], + ) + .unwrap(); - increase_allowance( - &mut app, - "ekez", - &pre_propose, - cw20_address.clone(), - Uint128::new(10), + let proposal: dpm_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dpm_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Passed); + assert_eq!(proposal.proposal.proposer, Addr::unchecked("ekez")); + assert_eq!(proposal.proposal.title, "title1".to_string()); + assert_eq!(proposal.proposal.description, "d".to_string()); + assert_eq!( + proposal.proposal.choices.len(), + // 3 + none of the above + 4 ); - let cw20_id = make_proposal( - &mut app, - pre_propose.clone(), + + app.execute_contract( + Addr::unchecked("ekez"), proposal_single.clone(), - "ekez", + &dpm_v241::msg::ExecuteMsg::Execute { proposal_id: 1 }, &[], - ); + ) + .unwrap(); - // There is now a pending proposal and cw20 tokens in the - // pre-propose module that should be returned on that proposal's - // completion. Execute an early withdraw and make sure things play - // out correctly. + let proposal: dpm_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dpm_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); - withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); - let balance = get_balance_cw20(&app, &cw20_address, core_addr.as_str()); - assert_eq!(balance, Uint128::new(10)); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); - // Proposal should still be executable! We just get removed from - // the proposal module's hook receiver list. - vote( - &mut app, - proposal_single.clone(), - "ekez", - cw20_id, - MultipleChoiceVote { option_id: 0 }, - ); - execute_proposal(&mut app, proposal_single.clone(), "ekez", cw20_id); + // UPGRADE ONLY PRE-PROPOSE TO LATEST VIA DAO PROPOSAL WITH POLICY UPDATE - // Make sure the proposal module has fallen back to anyone can - // propose becuase of our malfunction. - let proposal_creation_policy: ProposalCreationPolicy = app + let dppm_latest_id = app.store_code(dao_pre_propose_multiple_contract()); + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &dppm_v241::ExecuteMsg::Propose { + msg: dppm_v241::ProposeMessage::Propose { + title: "upgrade pre-propose-multiple from v2.4.1".to_string(), + description: "d".to_string(), + choices: dv_v241::multiple_choice::MultipleChoiceOptions { + options: vec![ + dv_v241::multiple_choice::MultipleChoiceOption { + title: "first".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + dv_v241::multiple_choice::MultipleChoiceOption { + title: "second".to_string(), + description: "d".to_string(), + msgs: vec![CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: pre_propose.to_string(), + new_code_id: dppm_latest_id, + msg: to_json_binary(&MigrateMsg::FromUnderV250 { + policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("noob")], + denylist: vec![], + }), + }) + .unwrap(), + })], + }, + dv_v241::multiple_choice::MultipleChoiceOption { + title: "third".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + ], + }, + vote: Some(dv_v241::multiple_choice::MultipleChoiceAutoVote { + vote: dv_v241::multiple_choice::MultipleChoiceVote { option_id: 1 }, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dpm_v241::msg::ExecuteMsg::Execute { proposal_id: 2 }, + &[], + ) + .unwrap(); + let proposal: dpm_v241::query::ProposalResponse = app .wrap() .query_wasm_smart( proposal_single.clone(), - &cpm::msg::QueryMsg::ProposalCreationPolicy {}, + &dpm_v241::msg::QueryMsg::Proposal { proposal_id: 2 }, ) .unwrap(); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); - assert_eq!(proposal_creation_policy, ProposalCreationPolicy::Anyone {}); + // MAKE SURE PRE PROPOSE INFO CHANGED - // Close out the native proposal and it's deposit as well. - vote( - &mut app, - proposal_single.clone(), - "ekez", - native_id, - MultipleChoiceVote { option_id: 2 }, + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string() + }, + info, ); - close_proposal(&mut app, proposal_single.clone(), "ekez", native_id); - withdraw( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + + // MAKE SURE PRE PROPOSE CONFIG WAS UPDATED + + let config: Config = app + .wrap() + .query_wasm_smart(pre_propose.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("noob")], + denylist: vec![] + } + }, + config ); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(30)); + + // NOW MAKE SURE ONLY NOOB CAN MAKE PROPOSALS + + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + choices: MultipleChoiceOptions { + options: vec![ + MultipleChoiceOption { + title: "first".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + MultipleChoiceOption { + title: "second".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + MultipleChoiceOption { + title: "third".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + ], + }, + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + app.execute_contract( + Addr::unchecked("noob"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + choices: MultipleChoiceOptions { + options: vec![ + MultipleChoiceOption { + title: "first".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + MultipleChoiceOption { + title: "second".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + MultipleChoiceOption { + title: "third".to_string(), + description: "d".to_string(), + msgs: vec![], + }, + ], + }, + vote: None, + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_multiple::msg::ExecuteMsg::Vote { + proposal_id: 3, + vote: MultipleChoiceVote { option_id: 1 }, + rationale: None, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_multiple::msg::ExecuteMsg::Execute { proposal_id: 3 }, + &[], + ) + .unwrap(); + let proposal: ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dao_proposal_multiple::msg::QueryMsg::Proposal { proposal_id: 3 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, Status::Executed); } diff --git a/contracts/pre-propose/dao-pre-propose-single/Cargo.toml b/contracts/pre-propose/dao-pre-propose-single/Cargo.toml index ac1fea9d4..ce70721c6 100644 --- a/contracts/pre-propose/dao-pre-propose-single/Cargo.toml +++ b/contracts/pre-propose/dao-pre-propose-single/Cargo.toml @@ -26,6 +26,7 @@ dao-voting = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } cw-utils = { workspace = true } +cw4 = { workspace = true } cw4-group = { workspace = true } cw20 = { workspace = true } cw20-base = { workspace = true } @@ -39,3 +40,11 @@ dao-testing = { workspace = true } dao-hooks = { workspace = true } dao-proposal-single = { workspace = true } cw-hooks = { workspace = true } + +# v2.4.1 migration +dao-dao-core-v241 = { workspace = true } +dao-interface-v241 = { workspace = true } +dao-pre-propose-single-v241 = { workspace = true } +dao-proposal-single-v241 = { workspace = true } +dao-voting-cw4-v241 = { workspace = true } +dao-voting-v241 = { workspace = true } diff --git a/contracts/pre-propose/dao-pre-propose-single/examples/schema.rs b/contracts/pre-propose/dao-pre-propose-single/examples/schema.rs index 8841e17bd..908c4c2d5 100644 --- a/contracts/pre-propose/dao-pre-propose-single/examples/schema.rs +++ b/contracts/pre-propose/dao-pre-propose-single/examples/schema.rs @@ -1,12 +1,11 @@ use cosmwasm_schema::write_api; -use cosmwasm_std::Empty; -use dao_pre_propose_base::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use dao_pre_propose_single::ProposeMessage; +use dao_pre_propose_single::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; fn main() { write_api! { - instantiate: InstantiateMsg, - query: QueryMsg, - execute: ExecuteMsg, + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg } } 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 bf590180a..6de3d4a04 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8,7 +8,7 @@ "type": "object", "required": [ "extension", - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -30,13 +30,21 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "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" + }, "DepositRefundPolicy": { "oneOf": [ { @@ -115,6 +123,76 @@ "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" }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "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" @@ -230,11 +308,9 @@ "properties": { "update_config": { "type": "object", - "required": [ - "open_proposal_submission" - ], "properties": { "deposit_info": { + "description": "If None, will remove the deposit. Backwards compatible.", "anyOf": [ { "$ref": "#/definitions/UncheckedDepositInfo" @@ -244,8 +320,79 @@ } ] }, - "open_proposal_submission": { - "type": "boolean" + "submission_policy": { + "description": "If None, will leave the submission policy in the config as-is.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Perform more granular submission policy updates to allow for atomic operations that don't override others.", + "type": "object", + "required": [ + "update_submission_policy" + ], + "properties": { + "update_submission_policy": { + "type": "object", + "properties": { + "allowlist_add": { + "description": "If using specific policy, optionally add to the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "allowlist_remove": { + "description": "If using specific policy, optionally remove from the allowlist.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_add": { + "description": "Optionally add to the denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "denylist_remove": { + "description": "Optionally remove from denylist. Works for any submission policy.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "set_dao_members": { + "description": "If using specific policy, optionally update the `dao_members` flag.", + "type": [ + "boolean", + "null" + ] } }, "additionalProperties": false @@ -376,6 +523,10 @@ } ], "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": [ @@ -565,6 +716,10 @@ } ] }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "DepositRefundPolicy": { "oneOf": [ { @@ -685,6 +840,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -773,6 +953,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -929,6 +1139,76 @@ } } }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "ProposeMessage": { "oneOf": [ { @@ -1373,6 +1653,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -1457,6 +1791,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -1492,6 +1841,20 @@ }, "additionalProperties": false }, + { + "description": "Returns contract version info.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the module's configuration.", "type": "object", @@ -1530,6 +1893,28 @@ }, "additionalProperties": false }, + { + "description": "Returns whether or not the address can submit proposals.", + "type": "object", + "required": [ + "can_propose" + ], + "properties": { + "can_propose": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns list of proposal submitted hooks.", "type": "object", @@ -1574,15 +1959,152 @@ } } }, - "migrate": null, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "from_under_v250" + ], + "properties": { + "from_under_v250": { + "type": "object", + "properties": { + "policy": { + "description": "Optionally set a new submission policy with more granular controls. If not set, the current policy will remain.", + "anyOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + }, + "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" + }, + "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" + }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, "sudo": null, "responses": { + "can_propose": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", "type": "object", "required": [ - "open_proposal_submission" + "submission_policy" ], "properties": { "deposit_info": { @@ -1596,9 +2118,13 @@ } ] }, - "open_proposal_submission": { - "description": "If false, only members (addresses with voting power) may create proposals in the DAO. Otherwise, any address may create a proposal so long as they pay the deposit.", - "type": "boolean" + "submission_policy": { + "description": "The policy dictating who is allowed to submit proposals.", + "allOf": [ + { + "$ref": "#/definitions/PreProposeSubmissionPolicy" + } + ] } }, "additionalProperties": false, @@ -1699,6 +2225,76 @@ } ] }, + "PreProposeSubmissionPolicy": { + "description": "The policy configured in a pre-propose module that determines who can submit proposals. This is the preferred way to restrict proposal creation (as opposed to the ProposalCreationPolicy above) since pre-propose modules support other features, such as proposal deposits.", + "oneOf": [ + { + "description": "Anyone may create proposals, except for those in the denylist.", + "type": "object", + "required": [ + "anyone" + ], + "properties": { + "anyone": { + "type": "object", + "required": [ + "denylist" + ], + "properties": { + "denylist": { + "description": "Addresses that may not create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Specific people may create proposals.", + "type": "object", + "required": [ + "specific" + ], + "properties": { + "specific": { + "type": "object", + "required": [ + "allowlist", + "dao_members", + "denylist" + ], + "properties": { + "allowlist": { + "description": "Addresses that may create proposals.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + }, + "dao_members": { + "description": "Whether or not DAO members may create proposals.", + "type": "boolean" + }, + "denylist": { + "description": "Addresses that may not create proposals, overriding other settings.", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + } + } + }, + "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" @@ -1843,6 +2439,40 @@ } } }, + "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 + } + } + }, "proposal_module": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Addr", diff --git a/contracts/pre-propose/dao-pre-propose-single/src/contract.rs b/contracts/pre-propose/dao-pre-propose-single/src/contract.rs index 73b283cac..4eb840793 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/contract.rs @@ -8,7 +8,10 @@ use cw2::set_contract_version; use dao_pre_propose_base::{ error::PreProposeError, - msg::{ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, QueryMsg as QueryBase}, + msg::{ + ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, MigrateMsg as MigrateBase, + QueryMsg as QueryBase, + }, state::PreProposeContract, }; use dao_voting::{proposal::SingleChoiceProposeMsg as ProposeMsg, voting::SingleChoiceAutoVote}; @@ -33,6 +36,7 @@ pub enum ProposeMessage { pub type InstantiateMsg = InstantiateBase; pub type ExecuteMsg = ExecuteBase; pub type QueryMsg = QueryBase; +pub type MigrateMsg = MigrateBase; /// Internal version of the propose message that includes the /// `proposer` field. The module will fill this in based on the sender @@ -42,7 +46,7 @@ enum ProposeMessageInternal { Propose(ProposeMsg), } -type PrePropose = PreProposeContract; +type PrePropose = PreProposeContract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -91,10 +95,23 @@ pub fn execute( ExecuteMsg::Withdraw { denom } => ExecuteInternal::Withdraw { denom }, ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy, } => ExecuteInternal::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy, + }, + ExecuteMsg::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, + } => ExecuteInternal::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, }, ExecuteMsg::AddProposalSubmittedHook { address } => { ExecuteInternal::AddProposalSubmittedHook { address } @@ -118,3 +135,10 @@ pub fn execute( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { PrePropose::default().query(deps, env, msg) } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(mut deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + let res = PrePropose::default().migrate(deps.branch(), msg); + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + res +} diff --git a/contracts/pre-propose/dao-pre-propose-single/src/lib.rs b/contracts/pre-propose/dao-pre-propose-single/src/lib.rs index 16d6ba5ae..33df46b46 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/lib.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/lib.rs @@ -5,7 +5,7 @@ pub mod contract; #[cfg(test)] mod tests; -pub use contract::{ExecuteMsg, InstantiateMsg, ProposeMessage, QueryMsg}; +pub use contract::{ExecuteMsg, InstantiateMsg, MigrateMsg, ProposeMessage, QueryMsg}; // Exporting these means that contracts interacting with this one don't // need an explicit dependency on the base contract to read queries. 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 0475d13b6..a8eef5e3e 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs @@ -1,26 +1,38 @@ -use cosmwasm_std::{coins, from_json, to_json_binary, Addr, Coin, Empty, Uint128}; +use cosmwasm_std::{ + coins, from_json, to_json_binary, Addr, Coin, CosmosMsg, Empty, Uint128, WasmMsg, +}; use cw2::ContractVersion; use cw20::Cw20Coin; use cw_denom::UncheckedDenom; use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor}; use cw_utils::Duration; +use dao_interface::proposal::InfoResponse; 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 dps; -use dao_testing::helpers::instantiate_with_cw4_groups_governance; +use dao_testing::{contracts::cw4_group_contract, helpers::instantiate_with_cw4_groups_governance}; +use dao_voting::pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}; use dao_voting::{ deposit::{CheckedDepositInfo, DepositRefundPolicy, DepositToken, UncheckedDepositInfo}, pre_propose::{PreProposeInfo, ProposalCreationPolicy}, status::Status, threshold::{PercentageThreshold, Threshold}, - voting::Vote, + voting::{SingleChoiceAutoVote, Vote}, }; use dps::query::ProposalResponse; +// test v2.4.1 migration +use dao_dao_core_v241 as core_v241; +use dao_interface_v241 as di_v241; +use dao_pre_propose_single_v241 as dpps_v241; +use dao_proposal_single_v241 as dps_v241; +use dao_voting_cw4_v241 as dvcw4_v241; +use dao_voting_v241 as dv_v241; + use crate::contract::*; -fn cw_dao_proposal_single_contract() -> Box> { +fn dao_proposal_single_contract() -> Box> { let contract = ContractWrapper::new( dps::contract::execute, dps::contract::instantiate, @@ -31,8 +43,8 @@ fn cw_dao_proposal_single_contract() -> Box> { Box::new(contract) } -fn cw_pre_propose_base_proposal_single() -> Box> { - let contract = ContractWrapper::new(execute, instantiate, query); +fn dao_pre_propose_single_contract() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query).with_migrate(migrate); Box::new(contract) } @@ -50,7 +62,17 @@ fn get_default_proposal_module_instantiate( deposit_info: Option, open_proposal_submission: bool, ) -> dps::msg::InstantiateMsg { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_single_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + } + }; dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { @@ -65,7 +87,7 @@ fn get_default_proposal_module_instantiate( code_id: pre_propose_id, msg: to_json_binary(&InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), @@ -113,7 +135,7 @@ fn setup_default_test( deposit_info: Option, open_proposal_submission: bool, ) -> DefaultTestSetup { - let dps_id = app.store_code(cw_dao_proposal_single_contract()); + let dps_id = app.store_code(dao_proposal_single_contract()); let proposal_module_instantiate = get_default_proposal_module_instantiate(app, deposit_info, open_proposal_submission); @@ -165,6 +187,15 @@ fn setup_default_test( get_proposal_module(app, pre_propose.clone()) ); assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + assert_eq!( + InfoResponse { + info: ContractVersion { + contract: "crates.io:dao-pre-propose-single".to_string(), + version: env!("CARGO_PKG_VERSION").to_string() + } + }, + get_info(app, pre_propose.clone()) + ); DefaultTestSetup { core_addr, @@ -314,6 +345,12 @@ fn get_dao(app: &App, module: Addr) -> Addr { .unwrap() } +fn get_info(app: &App, module: Addr) -> InfoResponse { + app.wrap() + .query_wasm_smart(module, &QueryMsg::Info {}) + .unwrap() +} + fn query_hooks(app: &App, module: Addr) -> cw_hooks::HooksResponse { app.wrap() .query_wasm_smart(module, &QueryMsg::ProposalSubmittedHooks {}) @@ -332,19 +369,30 @@ fn get_deposit_info(app: &App, module: Addr, id: u64) -> DepositInfoResponse { .unwrap() } +fn query_can_propose(app: &App, module: Addr, address: impl Into) -> bool { + app.wrap() + .query_wasm_smart( + module, + &QueryMsg::CanPropose { + address: address.into(), + }, + ) + .unwrap() +} + fn update_config( app: &mut App, module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> Config { app.execute_contract( Addr::unchecked(sender), module.clone(), &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -358,14 +406,14 @@ fn update_config_should_fail( module: Addr, sender: &str, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: PreProposeSubmissionPolicy, ) -> PreProposeError { app.execute_contract( Addr::unchecked(sender), module, &ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, + submission_policy: Some(submission_policy), }, &[], ) @@ -838,7 +886,10 @@ fn test_permissions() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}) + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ) } #[test] @@ -927,13 +978,246 @@ fn test_no_deposit_required_members_submission() { .unwrap_err() .downcast() .unwrap(); - assert_eq!(err, PreProposeError::NotMember {}); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); let id = make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); let new_status = vote(&mut app, proposal_single, "ekez", id, Vote::Yes); assert_eq!(Status::Passed, new_status) } +#[test] +fn test_anyone_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + ); + + let rando = "rando"; + + // Proposal succeeds when anyone can propose. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked(rando)], + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposing succeeds if not on denylist. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_proposal(&mut app, pre_propose, proposal_single.clone(), "ekez", &[]); +} + +#[test] +fn test_specific_allowlist_denylist() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + // Proposal succeeds for member. + assert!(query_can_propose(&app, pre_propose.clone(), "ekez")); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + "ekez", + &[], + ); + + let rando = "rando"; + + // Proposing fails for non-member. + assert!(!query_can_propose(&app, pre_propose.clone(), rando)); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked(rando), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "I would like to join the DAO".to_string(), + description: "though, I am currently not a member.".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![], + }, + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![Addr::unchecked("ekez")], + }, + ); + + // Proposing fails if on denylist. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked(rando)], + denylist: vec![], + }, + ); + + // Proposing fails if members not allowed. + assert!(!query_can_propose(&app, pre_propose.clone(), "ekez")); + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "Let me propose!".to_string(), + description: "I am a member!!!".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + // Proposal succeeds if on allowlist. + assert!(query_can_propose(&app, pre_propose.clone(), rando)); + make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + rando, + &[], + ); +} + #[test] fn test_execute_extension_does_nothing() { let mut app = App::default(); @@ -971,10 +1255,10 @@ fn test_execute_extension_does_nothing() { fn test_instantiate_with_zero_native_deposit() { let mut app = App::default(); - let dps_id = app.store_code(cw_dao_proposal_single_contract()); + let dps_id = app.store_code(dao_proposal_single_contract()); let proposal_module_instantiate = { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_single_contract()); dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { @@ -995,7 +1279,11 @@ fn test_instantiate_with_zero_native_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: Empty::default(), }) .unwrap(), @@ -1034,10 +1322,10 @@ fn test_instantiate_with_zero_cw20_deposit() { let cw20_addr = instantiate_cw20_base_default(&mut app); - let dps_id = app.store_code(cw_dao_proposal_single_contract()); + let dps_id = app.store_code(dao_proposal_single_contract()); let proposal_module_instantiate = { - let pre_propose_id = app.store_code(cw_pre_propose_base_proposal_single()); + let pre_propose_id = app.store_code(dao_pre_propose_single_contract()); dps::msg::InstantiateMsg { threshold: Threshold::AbsolutePercentage { @@ -1058,7 +1346,11 @@ fn test_instantiate_with_zero_cw20_deposit() { amount: Uint128::zero(), refund_policy: DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: Empty::default(), }) .unwrap(), @@ -1104,7 +1396,11 @@ fn test_update_config() { config, Config { deposit_info: None, - open_proposal_submission: false + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + }, } ); @@ -1127,7 +1423,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never, }), - true, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, ); let config = get_config(&app, pre_propose.clone()); @@ -1139,7 +1435,7 @@ fn test_update_config() { amount: Uint128::new(10), refund_policy: DepositRefundPolicy::Never }), - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, } ); @@ -1185,180 +1481,1394 @@ fn test_update_config() { assert_eq!(balance, Uint128::new(0)); // Only the core module can update the config. - let err = - update_config_should_fail(&mut app, pre_propose, proposal_single.as_str(), None, true); - assert_eq!(err, PreProposeError::NotDao {}); -} - -#[test] -fn test_withdraw() { - let mut app = App::default(); - - let DefaultTestSetup { - core_addr, - proposal_single, - pre_propose, - } = setup_default_test(&mut app, None, false); - - let err = withdraw_should_fail( + let err = update_config_should_fail( &mut app, pre_propose.clone(), proposal_single.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + None, + PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, ); assert_eq!(err, PreProposeError::NotDao {}); - let err = withdraw_should_fail( + // Errors when no one is authorized to create proposals. + let err = update_config_should_fail( &mut app, pre_propose.clone(), core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![], + denylist: vec![], + }, ); - assert_eq!(err, PreProposeError::NothingToWithdraw {}); - - let err = withdraw_should_fail(&mut app, pre_propose.clone(), core_addr.as_str(), None); - assert_eq!(err, PreProposeError::NoWithdrawalDenom {}); - - // Turn on native deposits. - update_config( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Native("ujuno".to_string()), - }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) ); - // Withdraw with no specified denom - should fall back to the one - // in the config. - mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); - withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(10)); - - // Withdraw again, this time specifying a native denomination. - mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); - withdraw( + // Errors when allowlist and denylist overlap. + let err = update_config_should_fail( &mut app, pre_propose.clone(), core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + None, + PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![Addr::unchecked("ekez")], + }, + ); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) ); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(20)); - // Make a proposal with the native tokens to put some in the system. - mint_natives(&mut app, "ekez", coins(10, "ujuno")); - let native_id = make_proposal( - &mut app, + // Doesn't change submission policy if omitted. + app.execute_contract( + core_addr, pre_propose.clone(), - proposal_single.clone(), + &ExecuteMsg::UpdateConfig { + deposit_info: None, + submission_policy: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + } + ); +} + +#[test] +fn test_update_submission_policy() { + let mut app = App::default(); + let DefaultTestSetup { + core_addr, + pre_propose, + .. + } = setup_default_test(&mut app, None, true); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + } + ); + + // Only the core module can update the submission policy. + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, PreProposeError::NotDao {}); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked("ekez")], + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { + denylist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, + } + ); + + // Error if try to change Specific fields when set to Anyone. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(true), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {} + ) + ); + + // Change to Specific policy. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateConfig { + deposit_info: None, + submission_policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + } + ); + + // Append to denylist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![Addr::unchecked("ekez")], + }, + } + ); + + // Add and remove to/from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["someone".to_string(), "else".to_string()]), + denylist_remove: Some(vec!["ekez".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + }, + } + ); + + // Remove from denylist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + }, + } + ); + + // Append to allowlist, with auto de-dupe. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["ekez".to_string(), "ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![], + }, + } + ); + + // Add and remove to/from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: Some(vec!["someone".to_string(), "else".to_string()]), + allowlist_remove: Some(vec!["ekez".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![Addr::unchecked("else"), Addr::unchecked("someone")], + denylist: vec![], + }, + } + ); + + // Remove from allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: Some(vec!["someone".to_string(), "else".to_string()]), + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + }, + } + ); + + // Setting dao_members to false fails if allowlist is empty. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::NoOneAllowed {}) + ); + + // Set dao_members to false and add allowlist. + app.execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: None, + denylist_remove: None, + set_dao_members: Some(false), + allowlist_add: Some(vec!["ekez".to_string()]), + allowlist_remove: None, + }, + &[], + ) + .unwrap(); + + let config = get_config(&app, pre_propose.clone()); + assert_eq!( + config, + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("ekez")], + denylist: vec![] + }, + } + ); + + // Errors when allowlist and denylist overlap. + let err: PreProposeError = app + .execute_contract( + core_addr.clone(), + pre_propose.clone(), + &ExecuteMsg::UpdateSubmissionPolicy { + denylist_add: Some(vec!["ekez".to_string()]), + denylist_remove: None, + set_dao_members: None, + allowlist_add: None, + allowlist_remove: None, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {} + ) + ); +} + +#[test] +fn test_withdraw() { + let mut app = App::default(); + + let DefaultTestSetup { + core_addr, + proposal_single, + pre_propose, + } = setup_default_test(&mut app, None, false); + + let err = withdraw_should_fail( + &mut app, + pre_propose.clone(), + proposal_single.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + assert_eq!(err, PreProposeError::NotDao {}); + + let err = withdraw_should_fail( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + assert_eq!(err, PreProposeError::NothingToWithdraw {}); + + let err = withdraw_should_fail(&mut app, pre_propose.clone(), core_addr.as_str(), None); + assert_eq!(err, PreProposeError::NoWithdrawalDenom {}); + + // Turn on native deposits. + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDepositInfo { + denom: DepositToken::Token { + denom: UncheckedDenom::Native("ujuno".to_string()), + }, + amount: Uint128::new(10), + refund_policy: DepositRefundPolicy::Always, + }), + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + // Withdraw with no specified denom - should fall back to the one + // in the config. + mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); + withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(10)); + + // Withdraw again, this time specifying a native denomination. + mint_natives(&mut app, pre_propose.as_str(), coins(10, "ujuno")); + withdraw( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(20)); + + // Make a proposal with the native tokens to put some in the system. + mint_natives(&mut app, "ekez", coins(10, "ujuno")); + let native_id = make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + "ekez", + &coins(10, "ujuno"), + ); + + // Update the config to use a cw20 token. + let cw20_address = instantiate_cw20_base_default(&mut app); + update_config( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDepositInfo { + denom: DepositToken::Token { + denom: UncheckedDenom::Cw20(cw20_address.to_string()), + }, + amount: Uint128::new(10), + refund_policy: DepositRefundPolicy::Always, + }), + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, + ); + + increase_allowance( + &mut app, "ekez", - &coins(10, "ujuno"), + &pre_propose, + cw20_address.clone(), + Uint128::new(10), + ); + let cw20_id = make_proposal( + &mut app, + pre_propose.clone(), + proposal_single.clone(), + "ekez", + &[], + ); + + // There is now a pending proposal and cw20 tokens in the + // pre-propose module that should be returned on that proposal's + // completion. To make things interesting, we withdraw those + // tokens which should cause the status change hook on the + // proposal's execution to fail as we don't have sufficent balance + // to return the deposit. + withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); + let balance = get_balance_cw20(&app, &cw20_address, core_addr.as_str()); + assert_eq!(balance, Uint128::new(10)); + + // Proposal should still be executable! We just get removed from + // the proposal module's hook receiver list. + vote( + &mut app, + proposal_single.clone(), + "ekez", + cw20_id, + Vote::Yes, + ); + execute_proposal(&mut app, proposal_single.clone(), "ekez", cw20_id); + + // Make sure the proposal module has fallen back to anyone can + // propose becuase of our malfunction. + let proposal_creation_policy: ProposalCreationPolicy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + assert_eq!(proposal_creation_policy, ProposalCreationPolicy::Anyone {}); + + // Close out the native proposal and it's deposit as well. + vote( + &mut app, + proposal_single.clone(), + "ekez", + native_id, + Vote::No, + ); + close_proposal(&mut app, proposal_single.clone(), "ekez", native_id); + withdraw( + &mut app, + pre_propose.clone(), + core_addr.as_str(), + Some(UncheckedDenom::Native("ujuno".to_string())), + ); + let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); + assert_eq!(balance, Uint128::new(30)); +} + +#[test] +fn test_hook_management() { + let app = &mut App::default(); + let DefaultTestSetup { + core_addr, + proposal_single: _, + pre_propose, + } = setup_default_test(app, None, true); + + add_hook(app, core_addr.as_str(), &pre_propose, "one"); + add_hook(app, core_addr.as_str(), &pre_propose, "two"); + + remove_hook(app, core_addr.as_str(), &pre_propose, "one"); + + let hooks = query_hooks(app, pre_propose).hooks; + assert_eq!(hooks, vec!["two".to_string()]) +} + +#[test] +fn test_migrate_from_v241() { + let app = &mut App::default(); + + let core_v241_contract = Box::new( + ContractWrapper::new( + core_v241::contract::execute, + core_v241::contract::instantiate, + core_v241::contract::query, + ) + .with_reply(core_v241::contract::reply), + ); + let dvcw4_v241_contract = Box::new( + ContractWrapper::new( + dvcw4_v241::contract::execute, + dvcw4_v241::contract::instantiate, + dvcw4_v241::contract::query, + ) + .with_reply(dvcw4_v241::contract::reply), + ); + let dpps_v241_contract = Box::new(ContractWrapper::new( + dpps_v241::contract::execute, + dpps_v241::contract::instantiate, + dpps_v241::contract::query, + )); + let dps_v241_contract = Box::new( + ContractWrapper::new( + dps_v241::contract::execute, + dps_v241::contract::instantiate, + dps_v241::contract::query, + ) + .with_reply(dps_v241::contract::reply), + ); + + let core_id = app.store_code(core_v241_contract); + let cw4_id = app.store_code(cw4_group_contract()); + let dvcw4_v241_id = app.store_code(dvcw4_v241_contract); + let dpps_v241_id = app.store_code(dpps_v241_contract); + let dps_v241_id = app.store_code(dps_v241_contract); + + let governance_instantiate = di_v241::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: di_v241::state::ModuleInstantiateInfo { + code_id: dvcw4_v241_id, + msg: to_json_binary(&dvcw4_v241::msg::InstantiateMsg { + group_contract: dvcw4_v241::msg::GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: "ekez".to_string(), + weight: 9, + }, + cw4::Member { + addr: "keze".to_string(), + weight: 8, + }, + ], + }, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![di_v241::state::ModuleInstantiateInfo { + code_id: dps_v241_id, + msg: to_json_binary(&dps_v241::msg::InstantiateMsg { + threshold: dv_v241::threshold::Threshold::AbsolutePercentage { + percentage: dv_v241::threshold::PercentageThreshold::Majority {}, + }, + max_voting_period: cw_utils::Duration::Time(86400), + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + pre_propose_info: dv_v241::pre_propose::PreProposeInfo::ModuleMayPropose { + info: di_v241::state::ModuleInstantiateInfo { + code_id: dpps_v241_id, + msg: to_json_binary(&dpps_v241::InstantiateMsg { + deposit_info: None, + open_proposal_submission: false, + extension: Empty::default(), + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "baby's first pre-propose module".to_string(), + }, + }, + close_proposal_on_execution_failure: false, + veto: None, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let core_addr = app + .instantiate_contract( + core_id, + Addr::unchecked("ekez"), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + app.update_block(|block| block.height += 1); + + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &di_v241::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + + assert_eq!(proposal_modules.len(), 1); + let proposal_single = proposal_modules.into_iter().next().unwrap().address; + let proposal_creation_policy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + let pre_propose = match proposal_creation_policy { + dv_v241::pre_propose::ProposalCreationPolicy::Module { addr } => addr, + _ => panic!("expected a module for the proposal creation policy"), + }; + + // Make sure things were set up correctly. + assert_eq!( + proposal_single, + get_proposal_module(app, pre_propose.clone()) + ); + assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: "crates.io:dao-pre-propose-single".to_string(), + version: "2.4.1".to_string() + }, + info, + ); + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &dpps_v241::ExecuteMsg::Propose { + msg: dpps_v241::ProposeMessage::Propose { + title: "title1".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: Some(dv_v241::voting::SingleChoiceAutoVote { + vote: dv_v241::voting::Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Passed); + assert_eq!(proposal.proposal.proposer, Addr::unchecked("ekez")); + assert_eq!(proposal.proposal.title, "title1".to_string()); + assert_eq!(proposal.proposal.description, "d".to_string()); + assert_eq!(proposal.proposal.msgs, vec![]); + + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dps_v241::msg::ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap(); + + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); + + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); + + // UPGRADE ONLY PRE-PROPOSE TO LATEST VIA DAO PROPOSAL + + let dpps_latest_id = app.store_code(dao_pre_propose_single_contract()); + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &dpps_v241::ExecuteMsg::Propose { + msg: dpps_v241::ProposeMessage::Propose { + title: "upgrade pre-propose-single from v2.4.1".to_string(), + description: "d".to_string(), + msgs: vec![CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: pre_propose.to_string(), + new_code_id: dpps_latest_id, + msg: to_json_binary(&MigrateMsg::FromUnderV250 { policy: None }).unwrap(), + })], + vote: Some(dv_v241::voting::SingleChoiceAutoVote { + vote: dv_v241::voting::Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dps_v241::msg::ExecuteMsg::Execute { proposal_id: 2 }, + &[], + ) + .unwrap(); + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 2 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); + + // MAKE SURE PRE PROPOSE INFO CHANGED + + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string() + }, + info, + ); + + // MAKE SURE PRE PROPOSE CONFIG WAS UPDATED + + let config: Config = app + .wrap() + .query_wasm_smart(pre_propose.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + } + }, + config + ); + + // NOW MAKE SURE WE CAN MAKE AND VOTE ON NEW PROPOSALS + + app.execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: Some(SingleChoiceAutoVote { + vote: Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_single::msg::ExecuteMsg::Execute { proposal_id: 3 }, + &[], + ) + .unwrap(); + let proposal: ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dao_proposal_single::msg::QueryMsg::Proposal { proposal_id: 3 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, Status::Executed); +} + +#[test] +fn test_migrate_from_v241_with_policy_update() { + let app = &mut App::default(); + + let core_v241_contract = Box::new( + ContractWrapper::new( + core_v241::contract::execute, + core_v241::contract::instantiate, + core_v241::contract::query, + ) + .with_reply(core_v241::contract::reply), + ); + let dvcw4_v241_contract = Box::new( + ContractWrapper::new( + dvcw4_v241::contract::execute, + dvcw4_v241::contract::instantiate, + dvcw4_v241::contract::query, + ) + .with_reply(dvcw4_v241::contract::reply), + ); + let dpps_v241_contract = Box::new(ContractWrapper::new( + dpps_v241::contract::execute, + dpps_v241::contract::instantiate, + dpps_v241::contract::query, + )); + let dps_v241_contract = Box::new( + ContractWrapper::new( + dps_v241::contract::execute, + dps_v241::contract::instantiate, + dps_v241::contract::query, + ) + .with_reply(dps_v241::contract::reply), ); - // Update the config to use a cw20 token. - let cw20_address = instantiate_cw20_base_default(&mut app); - update_config( - &mut app, - pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDepositInfo { - denom: DepositToken::Token { - denom: UncheckedDenom::Cw20(cw20_address.to_string()), + let core_id = app.store_code(core_v241_contract); + let cw4_id = app.store_code(cw4_group_contract()); + let dvcw4_v241_id = app.store_code(dvcw4_v241_contract); + let dpps_v241_id = app.store_code(dpps_v241_contract); + let dps_v241_id = app.store_code(dps_v241_contract); + + let governance_instantiate = di_v241::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: di_v241::state::ModuleInstantiateInfo { + code_id: dvcw4_v241_id, + msg: to_json_binary(&dvcw4_v241::msg::InstantiateMsg { + group_contract: dvcw4_v241::msg::GroupContract::New { + cw4_group_code_id: cw4_id, + initial_members: vec![ + cw4::Member { + addr: "ekez".to_string(), + weight: 9, + }, + cw4::Member { + addr: "keze".to_string(), + weight: 8, + }, + ], + }, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![di_v241::state::ModuleInstantiateInfo { + code_id: dps_v241_id, + msg: to_json_binary(&dps_v241::msg::InstantiateMsg { + threshold: dv_v241::threshold::Threshold::AbsolutePercentage { + percentage: dv_v241::threshold::PercentageThreshold::Majority {}, + }, + max_voting_period: cw_utils::Duration::Time(86400), + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + pre_propose_info: dv_v241::pre_propose::PreProposeInfo::ModuleMayPropose { + info: di_v241::state::ModuleInstantiateInfo { + code_id: dpps_v241_id, + msg: to_json_binary(&dpps_v241::InstantiateMsg { + deposit_info: None, + open_proposal_submission: false, + extension: Empty::default(), + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "baby's first pre-propose module".to_string(), + }, + }, + close_proposal_on_execution_failure: false, + veto: None, + }) + .unwrap(), + admin: Some(di_v241::state::Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module".to_string(), + }], + initial_items: None, + }; + + let core_addr = app + .instantiate_contract( + core_id, + Addr::unchecked("ekez"), + &governance_instantiate, + &[], + "DAO DAO", + None, + ) + .unwrap(); + + app.update_block(|block| block.height += 1); + + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &di_v241::msg::QueryMsg::ProposalModules { + start_after: None, + limit: None, }, - amount: Uint128::new(10), - refund_policy: DepositRefundPolicy::Always, - }), - false, - ); + ) + .unwrap(); - increase_allowance( - &mut app, - "ekez", - &pre_propose, - cw20_address.clone(), - Uint128::new(10), + assert_eq!(proposal_modules.len(), 1); + let proposal_single = proposal_modules.into_iter().next().unwrap().address; + let proposal_creation_policy = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::ProposalCreationPolicy {}, + ) + .unwrap(); + + let pre_propose = match proposal_creation_policy { + dv_v241::pre_propose::ProposalCreationPolicy::Module { addr } => addr, + _ => panic!("expected a module for the proposal creation policy"), + }; + + // Make sure things were set up correctly. + assert_eq!( + proposal_single, + get_proposal_module(app, pre_propose.clone()) ); - let cw20_id = make_proposal( - &mut app, + assert_eq!(core_addr, get_dao(app, pre_propose.clone())); + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: "crates.io:dao-pre-propose-single".to_string(), + version: "2.4.1".to_string() + }, + info, + ); + + app.execute_contract( + Addr::unchecked("ekez"), pre_propose.clone(), - proposal_single.clone(), - "ekez", + &dpps_v241::ExecuteMsg::Propose { + msg: dpps_v241::ProposeMessage::Propose { + title: "title1".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: Some(dv_v241::voting::SingleChoiceAutoVote { + vote: dv_v241::voting::Vote::Yes, + rationale: None, + }), + }, + }, &[], - ); + ) + .unwrap(); - // There is now a pending proposal and cw20 tokens in the - // pre-propose module that should be returned on that proposal's - // completion. To make things interesting, we withdraw those - // tokens which should cause the status change hook on the - // proposal's execution to fail as we don't have sufficent balance - // to return the deposit. - withdraw(&mut app, pre_propose.clone(), core_addr.as_str(), None); - let balance = get_balance_cw20(&app, &cw20_address, core_addr.as_str()); - assert_eq!(balance, Uint128::new(10)); + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, + ) + .unwrap(); - // Proposal should still be executable! We just get removed from - // the proposal module's hook receiver list. - vote( - &mut app, + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Passed); + assert_eq!(proposal.proposal.proposer, Addr::unchecked("ekez")); + assert_eq!(proposal.proposal.title, "title1".to_string()); + assert_eq!(proposal.proposal.description, "d".to_string()); + assert_eq!(proposal.proposal.msgs, vec![]); + + app.execute_contract( + Addr::unchecked("ekez"), proposal_single.clone(), - "ekez", - cw20_id, - Vote::Yes, - ); - execute_proposal(&mut app, proposal_single.clone(), "ekez", cw20_id); + &dps_v241::msg::ExecuteMsg::Execute { proposal_id: 1 }, + &[], + ) + .unwrap(); - // Make sure the proposal module has fallen back to anyone can - // propose becuase of our malfunction. - let proposal_creation_policy: ProposalCreationPolicy = app + let proposal: dps_v241::query::ProposalResponse = app .wrap() .query_wasm_smart( proposal_single.clone(), - &dps::msg::QueryMsg::ProposalCreationPolicy {}, + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 1 }, ) .unwrap(); - assert_eq!(proposal_creation_policy, ProposalCreationPolicy::Anyone {}); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); - // Close out the native proposal and it's deposit as well. - vote( - &mut app, - proposal_single.clone(), - "ekez", - native_id, - Vote::No, - ); - close_proposal(&mut app, proposal_single.clone(), "ekez", native_id); - withdraw( - &mut app, + // UPGRADE ONLY PRE-PROPOSE TO LATEST VIA DAO PROPOSAL WITH POLICY UPDATE + + let dpps_latest_id = app.store_code(dao_pre_propose_single_contract()); + + app.execute_contract( + Addr::unchecked("ekez"), pre_propose.clone(), - core_addr.as_str(), - Some(UncheckedDenom::Native("ujuno".to_string())), + &dpps_v241::ExecuteMsg::Propose { + msg: dpps_v241::ProposeMessage::Propose { + title: "upgrade pre-propose-single from v2.4.1".to_string(), + description: "d".to_string(), + msgs: vec![CosmosMsg::Wasm(WasmMsg::Migrate { + contract_addr: pre_propose.to_string(), + new_code_id: dpps_latest_id, + msg: to_json_binary(&MigrateMsg::FromUnderV250 { + policy: Some(PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("noob")], + denylist: vec![], + }), + }) + .unwrap(), + })], + vote: Some(dv_v241::voting::SingleChoiceAutoVote { + vote: dv_v241::voting::Vote::Yes, + rationale: None, + }), + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dps_v241::msg::ExecuteMsg::Execute { proposal_id: 2 }, + &[], + ) + .unwrap(); + let proposal: dps_v241::query::ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dps_v241::msg::QueryMsg::Proposal { proposal_id: 2 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, dv_v241::status::Status::Executed); + + // MAKE SURE PRE PROPOSE INFO CHANGED + + let info: ContractVersion = from_json( + app.wrap() + .query_wasm_raw(pre_propose.clone(), "contract_info".as_bytes()) + .unwrap() + .unwrap(), + ) + .unwrap(); + assert_eq!( + ContractVersion { + contract: CONTRACT_NAME.to_string(), + version: CONTRACT_VERSION.to_string() + }, + info, ); - let balance = get_balance_native(&app, core_addr.as_str(), "ujuno"); - assert_eq!(balance, Uint128::new(30)); -} -#[test] -fn test_hook_management() { - let app = &mut App::default(); - let DefaultTestSetup { - core_addr, - proposal_single: _, - pre_propose, - } = setup_default_test(app, None, true); + // MAKE SURE PRE PROPOSE CONFIG WAS UPDATED - add_hook(app, core_addr.as_str(), &pre_propose, "one"); - add_hook(app, core_addr.as_str(), &pre_propose, "two"); + let config: Config = app + .wrap() + .query_wasm_smart(pre_propose.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + Config { + deposit_info: None, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: false, + allowlist: vec![Addr::unchecked("noob")], + denylist: vec![] + } + }, + config + ); - remove_hook(app, core_addr.as_str(), &pre_propose, "one"); + // NOW MAKE SURE ONLY NOOB CAN MAKE PROPOSALS - let hooks = query_hooks(app, pre_propose).hooks; - assert_eq!(hooks, vec!["two".to_string()]) + let err: PreProposeError = app + .execute_contract( + Addr::unchecked("ekez"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!( + err, + PreProposeError::SubmissionPolicy(PreProposeSubmissionPolicyError::Unauthorized {}) + ); + + app.execute_contract( + Addr::unchecked("noob"), + pre_propose.clone(), + &ExecuteMsg::Propose { + msg: ProposeMessage::Propose { + title: "title2 on latest version".to_string(), + description: "d".to_string(), + msgs: vec![], + vote: None, + }, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_single::msg::ExecuteMsg::Vote { + proposal_id: 3, + vote: Vote::Yes, + rationale: None, + }, + &[], + ) + .unwrap(); + app.execute_contract( + Addr::unchecked("ekez"), + proposal_single.clone(), + &dao_proposal_single::msg::ExecuteMsg::Execute { proposal_id: 3 }, + &[], + ) + .unwrap(); + let proposal: ProposalResponse = app + .wrap() + .query_wasm_smart( + proposal_single.clone(), + &dao_proposal_single::msg::QueryMsg::Proposal { proposal_id: 3 }, + ) + .unwrap(); + assert_eq!(proposal.proposal.status, Status::Executed); } diff --git a/contracts/proposal/dao-proposal-condorcet/Cargo.toml b/contracts/proposal/dao-proposal-condorcet/Cargo.toml index a53910b27..a9326a4c6 100644 --- a/contracts/proposal/dao-proposal-condorcet/Cargo.toml +++ b/contracts/proposal/dao-proposal-condorcet/Cargo.toml @@ -1,5 +1,5 @@ [package] -name ="dao-proposal-condorcet" +name = "dao-proposal-condorcet" authors = ["ekez "] description = "A DAO DAO proposal module with ranked-choice, Condorcet voting." edition = { workspace = true } @@ -26,6 +26,7 @@ dao-voting = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } thiserror = { workspace = true } +cw-orch = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } 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 1ffbbb163..73bc73dd9 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -480,6 +480,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -555,6 +580,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -972,6 +1027,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -1056,6 +1165,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -1608,6 +1732,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -1696,6 +1845,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -2266,6 +2445,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -2351,6 +2584,21 @@ } ] }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } + }, "Winner": { "oneOf": [ { diff --git a/contracts/proposal/dao-proposal-condorcet/src/contract.rs b/contracts/proposal/dao-proposal-condorcet/src/contract.rs index f6b5406f3..3d3785002 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/contract.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/contract.rs @@ -274,7 +274,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/msg.rs b/contracts/proposal/dao-proposal-condorcet/src/msg.rs index ab6f5aa12..684108444 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/msg.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/msg.rs @@ -13,6 +13,7 @@ pub struct Choice { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { Propose { choices: Vec }, Vote { proposal_id: u32, vote: Vec }, @@ -23,7 +24,7 @@ pub enum ExecuteMsg { #[proposal_module_query] #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(crate::proposal::ProposalResponse)] Proposal { id: u32 }, diff --git a/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs b/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs index 79d18154f..c268bc5c1 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs @@ -146,7 +146,7 @@ impl SuiteBuilder { if let Some(candidates) = self.with_proposal { suite .propose( - &suite.sender(), + suite.sender(), (0..candidates) .map(|_| vec![unimportant_message()]) .collect(), diff --git a/contracts/proposal/dao-proposal-multiple/Cargo.toml b/contracts/proposal/dao-proposal-multiple/Cargo.toml index 5eca7e2df..e9d1f90c2 100644 --- a/contracts/proposal/dao-proposal-multiple/Cargo.toml +++ b/contracts/proposal/dao-proposal-multiple/Cargo.toml @@ -39,6 +39,7 @@ cw-hooks = { workspace = true } dao-hooks = { workspace = true } dao-pre-propose-multiple = { workspace = true } voting-v1 = { workspace = true } +cw-orch = { workspace = true } [dev-dependencies] anyhow = { workspace = true } 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 415412ea2..51bc3ffa2 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -988,6 +988,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -1063,6 +1088,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -1722,6 +1777,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -1806,6 +1915,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -2998,6 +3122,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -3120,6 +3269,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -3806,6 +3985,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -3890,6 +4123,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -4287,6 +4535,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -4409,6 +4682,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -5076,6 +5379,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -5160,6 +5517,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -5173,6 +5545,7 @@ "proposal_creation_policy": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposalCreationPolicy", + "description": "The policy configured in a proposal module that determines whether or not a pre-propose module is in use. If so, only the module can create new proposals. Otherwise, there is no restriction on proposal creation.", "oneOf": [ { "description": "Anyone may create a proposal, free of charge.", @@ -5533,6 +5906,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -5655,6 +6053,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -6341,6 +6769,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -6425,6 +6907,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, diff --git a/contracts/proposal/dao-proposal-multiple/src/contract.rs b/contracts/proposal/dao-proposal-multiple/src/contract.rs index abe9e73a4..6fc42f899 100644 --- a/contracts/proposal/dao-proposal-multiple/src/contract.rs +++ b/contracts/proposal/dao-proposal-multiple/src/contract.rs @@ -997,7 +997,10 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { let addr = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; diff --git a/contracts/proposal/dao-proposal-multiple/src/msg.rs b/contracts/proposal/dao-proposal-multiple/src/msg.rs index 1ed54db93..071e8d802 100644 --- a/contracts/proposal/dao-proposal-multiple/src/msg.rs +++ b/contracts/proposal/dao-proposal-multiple/src/msg.rs @@ -48,6 +48,7 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Creates a proposal in the governance module. Propose(MultipleChoiceProposeMsg), @@ -147,7 +148,7 @@ pub enum ExecuteMsg { #[proposal_module_query] #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { /// Gets the governance module's config. #[returns(crate::state::Config)] diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs index 8fd7e0c95..693edfbb0 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs @@ -12,8 +12,11 @@ use dao_testing::contracts::{ use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo, VotingModuleTokenType}, multiple_choice::VotingStrategy, - pre_propose::PreProposeInfo, - threshold::{ActiveThreshold, ActiveThreshold::AbsoluteCount, PercentageThreshold}, + pre_propose::{PreProposeInfo, PreProposeSubmissionPolicy}, + threshold::{ + ActiveThreshold::{self, AbsoluteCount}, + PercentageThreshold, + }, }; use dao_voting_cw4::msg::GroupContract; @@ -29,12 +32,23 @@ fn get_pre_propose_info( open_proposal_submission: bool, ) -> PreProposeInfo { let pre_propose_contract = app.store_code(pre_propose_multiple_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + } + }; + PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, msg: to_json_binary(&cppm::InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs index d7b51beaa..2b82b7ce7 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs @@ -9,6 +9,7 @@ use cw_utils::Duration; use dao_interface::state::ProposalModule; use dao_interface::state::{Admin, ModuleInstantiateInfo}; use dao_voting::multiple_choice::MultipleChoiceAutoVote; +use dao_voting::pre_propose::PreProposeSubmissionPolicy; use dao_voting::veto::{VetoConfig, VetoError}; use dao_voting::{ deposit::{ @@ -96,12 +97,23 @@ pub fn get_pre_propose_info( open_proposal_submission: bool, ) -> PreProposeInfo { let pre_propose_contract = app.store_code(pre_propose_multiple_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + } + }; + PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, msg: to_json_binary(&cppm::InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), diff --git a/contracts/proposal/dao-proposal-single/Cargo.toml b/contracts/proposal/dao-proposal-single/Cargo.toml index 06170dd00..b93c69a44 100644 --- a/contracts/proposal/dao-proposal-single/Cargo.toml +++ b/contracts/proposal/dao-proposal-single/Cargo.toml @@ -31,9 +31,10 @@ dao-pre-propose-base = { workspace = true } dao-voting = { workspace = true } thiserror = { workspace = true } -cw-utils-v1 = { workspace = true} +cw-utils-v1 = { workspace = true } voting-v1 = { workspace = true } cw-proposal-single-v1 = { workspace = true, features = ["library"] } +cw-orch = { workspace = true } [dev-dependencies] anyhow = { workspace = true } 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 2491d27aa..fdd578d12 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -1041,6 +1041,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -1116,6 +1141,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -1793,6 +1848,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -1877,6 +1986,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -3104,6 +3228,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -3226,6 +3375,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -3949,6 +4128,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -4033,6 +4266,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -4404,6 +4652,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -4526,6 +4799,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -5229,6 +5532,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -5313,6 +5670,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, @@ -5326,6 +5698,7 @@ "proposal_creation_policy": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposalCreationPolicy", + "description": "The policy configured in a proposal module that determines whether or not a pre-propose module is in use. If so, only the module can create new proposals. Otherwise, there is no restriction on proposal creation.", "oneOf": [ { "description": "Anyone may create a proposal, free of charge.", @@ -5649,6 +6022,31 @@ } }, "additionalProperties": false + }, + { + "description": "This is translated to a [[MsgFundCommunityPool](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#LL69C1-L76C2). `depositor` is automatically filled with the current contract's address.", + "type": "object", + "required": [ + "fund_community_pool" + ], + "properties": { + "fund_community_pool": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "description": "The amount to spend", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + } + }, + "additionalProperties": false } ] }, @@ -5771,6 +6169,36 @@ } }, "additionalProperties": false + }, + { + "description": "This maps directly to [MsgVoteWeighted](https://github.com/cosmos/cosmos-sdk/blob/v0.45.8/proto/cosmos/gov/v1beta1/tx.proto#L66-L78) in the Cosmos SDK with voter set to the contract address.", + "type": "object", + "required": [ + "vote_weighted" + ], + "properties": { + "vote_weighted": { + "type": "object", + "required": [ + "options", + "proposal_id" + ], + "properties": { + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/WeightedVoteOption" + } + }, + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + }, + "additionalProperties": false } ] }, @@ -6494,6 +6922,60 @@ }, "additionalProperties": false }, + { + "description": "Instantiates a new contracts from previously uploaded Wasm code using a predictable address derivation algorithm implemented in [`cosmwasm_std::instantiate2_address`].\n\nThis is translated to a [MsgInstantiateContract2](https://github.com/CosmWasm/wasmd/blob/v0.29.2/proto/cosmwasm/wasm/v1/tx.proto#L73-L96). `sender` is automatically filled with the current contract's address. `fix_msg` is automatically set to false.", + "type": "object", + "required": [ + "instantiate2" + ], + "properties": { + "instantiate2": { + "type": "object", + "required": [ + "code_id", + "funds", + "label", + "msg", + "salt" + ], + "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-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": { + "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)", + "allOf": [ + { + "$ref": "#/definitions/Binary" + } + ] + }, + "salt": { + "$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", @@ -6578,6 +7060,21 @@ "additionalProperties": false } ] + }, + "WeightedVoteOption": { + "type": "object", + "required": [ + "option", + "weight" + ], + "properties": { + "option": { + "$ref": "#/definitions/VoteOption" + }, + "weight": { + "$ref": "#/definitions/Decimal" + } + } } } }, diff --git a/contracts/proposal/dao-proposal-single/src/contract.rs b/contracts/proposal/dao-proposal-single/src/contract.rs index d7d9a2972..1943081d1 100644 --- a/contracts/proposal/dao-proposal-single/src/contract.rs +++ b/contracts/proposal/dao-proposal-single/src/contract.rs @@ -1081,7 +1081,10 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { let addr = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; diff --git a/contracts/proposal/dao-proposal-single/src/msg.rs b/contracts/proposal/dao-proposal-single/src/msg.rs index 302303b48..82e75f22f 100644 --- a/contracts/proposal/dao-proposal-single/src/msg.rs +++ b/contracts/proposal/dao-proposal-single/src/msg.rs @@ -47,6 +47,7 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Creates a proposal in the module. Propose(SingleChoiceProposeMsg), @@ -147,7 +148,7 @@ pub enum ExecuteMsg { #[proposal_module_query] #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { /// Gets the proposal module's config. #[returns(crate::state::Config)] diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index 020154700..217ad740f 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -8,7 +8,7 @@ use dao_pre_propose_single as cppbps; use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo, VotingModuleTokenType}, - pre_propose::PreProposeInfo, + pre_propose::{PreProposeInfo, PreProposeSubmissionPolicy}, threshold::{ActiveThreshold, PercentageThreshold, Threshold::ThresholdQuorum}, }; use dao_voting_cw4::msg::GroupContract; @@ -31,12 +31,23 @@ pub(crate) fn get_pre_propose_info( ) -> PreProposeInfo { let pre_propose_contract = app.store_code(crate::testing::contracts::pre_propose_single_contract()); + + let submission_policy = if open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + } + }; + PreProposeInfo::ModuleMayPropose { info: ModuleInstantiateInfo { code_id: pre_propose_contract, msg: to_json_binary(&cppbps::InstantiateMsg { deposit_info, - open_proposal_submission, + submission_policy, extension: Empty::default(), }) .unwrap(), diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index 4246e5a09..56bc68522 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -19,7 +19,7 @@ use dao_interface::{ use dao_testing::{ShouldExecute, TestSingleChoiceVote}; use dao_voting::{ deposit::{CheckedDepositInfo, UncheckedDepositInfo, VotingModuleTokenType}, - pre_propose::{PreProposeInfo, ProposalCreationPolicy}, + pre_propose::{PreProposeInfo, PreProposeSubmissionPolicy, ProposalCreationPolicy}, proposal::{SingleChoiceProposeMsg as ProposeMsg, MAX_PROPOSAL_SIZE}, reply::{ failed_pre_propose_module_hook_id, mask_proposal_execution_proposal_id, @@ -3955,7 +3955,11 @@ fn test_update_pre_propose_module() { amount: Uint128::new(1), refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + }, extension: Empty::default(), }) .unwrap(), @@ -4006,7 +4010,11 @@ fn test_update_pre_propose_module() { amount: Uint128::new(1), refund_policy: dao_voting::deposit::DepositRefundPolicy::OnlyPassed, }), - open_proposal_submission: false, + submission_policy: PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![] + }, } ); diff --git a/contracts/staking/cw20-stake-external-rewards/Cargo.toml b/contracts/staking/cw20-stake-external-rewards/Cargo.toml index 9f59c261e..f081eb63a 100644 --- a/contracts/staking/cw20-stake-external-rewards/Cargo.toml +++ b/contracts/staking/cw20-stake-external-rewards/Cargo.toml @@ -22,15 +22,16 @@ cw-storage-plus = { workspace = true } cw-controllers = { workspace = true } cw20 = { workspace = true } cw-utils = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } +cw20-base = { workspace = true, features = ["library"] } cw2 = { workspace = true } thiserror = { workspace = true } -cw20-stake = { workspace = true, features = ["library"]} +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" } +cw-orch = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } 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 cab9e7c5b..aeec250c4 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/staking/cw20-stake-external-rewards/src/msg.rs b/contracts/staking/cw20-stake-external-rewards/src/msg.rs index eb873c798..e4bc3be81 100644 --- a/contracts/staking/cw20-stake-external-rewards/src/msg.rs +++ b/contracts/staking/cw20-stake-external-rewards/src/msg.rs @@ -22,6 +22,7 @@ pub struct InstantiateMsg { #[cw_ownable_execute] #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { StakeChangeHook(StakeChangedHookMsg), Claim {}, @@ -45,7 +46,7 @@ pub enum ReceiveMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(InfoResponse)] Info {}, diff --git a/contracts/staking/cw20-stake-reward-distributor/Cargo.toml b/contracts/staking/cw20-stake-reward-distributor/Cargo.toml index 75972d965..5ecfcd971 100644 --- a/contracts/staking/cw20-stake-reward-distributor/Cargo.toml +++ b/contracts/staking/cw20-stake-reward-distributor/Cargo.toml @@ -1,7 +1,9 @@ [package] name = "cw20-stake-reward-distributor" edition = "2018" -authors = ["Vernon Johnson , ekez "] +authors = [ + "Vernon Johnson , ekez ", +] description = "Distributes cw20 staking rewards." license = { workspace = true } repository = { workspace = true } @@ -24,11 +26,12 @@ cw-storage-plus = { workspace = true } cw2 = { workspace = true } cw20 = { workspace = true } cw-utils = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } -cw20-stake = { workspace = true, features = ["library"]} +cw20-base = { workspace = true, features = ["library"] } +cw20-stake = { workspace = true, features = ["library"] } thiserror = { workspace = true } cw-ownable = { workspace = true } cw20-stake-reward-distributor-v1 = { workspace = true, features = ["library"] } +cw-orch = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } 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 bbbd6279b..3acae01e3 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.4.2", + "contract_version": "2.5.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/msg.rs b/contracts/staking/cw20-stake-reward-distributor/src/msg.rs index 47e347086..a54ca3c5f 100644 --- a/contracts/staking/cw20-stake-reward-distributor/src/msg.rs +++ b/contracts/staking/cw20-stake-reward-distributor/src/msg.rs @@ -18,6 +18,7 @@ pub struct InstantiateMsg { #[cw_ownable_execute] #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { UpdateConfig { staking_addr: String, @@ -29,7 +30,7 @@ pub enum ExecuteMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(InfoResponse)] Info {}, diff --git a/contracts/staking/cw20-stake/Cargo.toml b/contracts/staking/cw20-stake/Cargo.toml index 4417b372b..47cf154d0 100644 --- a/contracts/staking/cw20-stake/Cargo.toml +++ b/contracts/staking/cw20-stake/Cargo.toml @@ -23,7 +23,7 @@ cw-controllers = { workspace = true } cw-hooks = { workspace = true } cw20 = { workspace = true } cw-utils = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } +cw20-base = { workspace = true, features = ["library"] } cw2 = { workspace = true } thiserror = { workspace = true } cw-paginate-storage = { workspace = true } @@ -33,6 +33,7 @@ dao-voting = { workspace = true } cw20-stake-v1 = { workspace = true, features = ["library"] } cw-utils-v1 = { workspace = true } +cw-orch = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/staking/cw20-stake/schema/cw20-stake.json b/contracts/staking/cw20-stake/schema/cw20-stake.json index 5c04738ae..77c19cd17 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/staking/cw20-stake/src/msg.rs b/contracts/staking/cw20-stake/src/msg.rs index bfe65466b..19dd36f71 100644 --- a/contracts/staking/cw20-stake/src/msg.rs +++ b/contracts/staking/cw20-stake/src/msg.rs @@ -21,6 +21,7 @@ pub struct InstantiateMsg { #[cw_ownable_execute] #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { Receive(Cw20ReceiveMsg), Unstake { amount: Uint128 }, @@ -37,7 +38,7 @@ pub enum ReceiveMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(StakedBalanceAtHeightResponse)] StakedBalanceAtHeight { diff --git a/contracts/test/dao-gauge-adapter/Cargo.toml b/contracts/test/dao-gauge-adapter/Cargo.toml new file mode 100644 index 000000000..0f759c113 --- /dev/null +++ b/contracts/test/dao-gauge-adapter/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "dao-gauge-adapter" +authors = ["ekez "] +description = "A gauge adapter with the minimal required logic to work with gauge orchestrator." +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 = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +thiserror = { workspace = true } +dao-dao-macros = { workspace = true } +dao-interface = { workspace = true } +cw-orch = { workspace = true } +gauge-orchestrator = { path = "../../gauges/gauge" } + +[dev-dependencies] +cw-multi-test = { workspace = true } diff --git a/contracts/test/dao-gauge-adapter/src/contract.rs b/contracts/test/dao-gauge-adapter/src/contract.rs new file mode 100644 index 000000000..7b133781c --- /dev/null +++ b/contracts/test/dao-gauge-adapter/src/contract.rs @@ -0,0 +1,93 @@ + + +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{ + coin, to_json_binary, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, MessageInfo, + Order, Response, StdError, StdResult, +}; +use cw_storage_plus::{Item, Map}; + + +use gauge_orchestrator::msg::{ + AdapterQueryMsg, AllOptionsResponse, CheckOptionResponse, SampleGaugeMsgsResponse, +}; + +#[cw_serde] +pub struct InstantiateMsg { + pub options: Vec, + pub to_distribute: Coin, +} + +#[cw_serde] +#[derive(cw_orch::ExecuteFns)] +pub enum ExecuteMsg { + InvalidateOption { option: String }, + AddValidOption { option: String }, +} + +#[cw_serde] +struct EmptyMsg {} + +const OPTIONS: Map = Map::new("options"); +const TO_DISTRIBUTE: Item = Item::new("to_spend"); + +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + msg.options + .into_iter() + .try_for_each(|option| OPTIONS.save(deps.storage, option, &true))?; + TO_DISTRIBUTE.save(deps.storage, &msg.to_distribute)?; + Ok(Response::default()) +} + +pub fn execute( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::InvalidateOption { option } => { + OPTIONS.remove(deps.storage, option); + } + ExecuteMsg::AddValidOption { option } => { + OPTIONS.save(deps.storage, option, &true)?; + } + } + Ok(Response::new()) +} + +pub fn query(deps: Deps, _env: Env, msg: AdapterQueryMsg) -> Result { + match msg { + AdapterQueryMsg::AllOptions {} => to_json_binary(&AllOptionsResponse { + options: OPTIONS + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?, + }), + AdapterQueryMsg::CheckOption { option } => to_json_binary(&CheckOptionResponse { + valid: OPTIONS.has(deps.storage, option), + }), + AdapterQueryMsg::SampleGaugeMsgs { selected } => { + let to_distribute = TO_DISTRIBUTE.load(deps.storage)?; + let mut weights_sum = Decimal::zero(); + let execute = selected + .into_iter() + .map(|(option, weight)| { + weights_sum += weight; + CosmosMsg::Bank(cosmwasm_std::BankMsg::Send { + to_address: option, + amount: vec![coin( + (to_distribute.amount * weight).u128(), + to_distribute.denom.clone(), + )], + }) + }) + .collect::>(); + to_json_binary(&SampleGaugeMsgsResponse { execute }) + } + } +} \ No newline at end of file diff --git a/contracts/test/dao-gauge-adapter/src/lib.rs b/contracts/test/dao-gauge-adapter/src/lib.rs new file mode 100644 index 000000000..b2840a95a --- /dev/null +++ b/contracts/test/dao-gauge-adapter/src/lib.rs @@ -0,0 +1,7 @@ + +//! Gauge adapter contract to mock in tests. +//! I wrote it so that InstantiateMsg contains list of initially +//! available options. Query for CheckOption checks if option is already added, +//! otherwise returns true - option is valid. + +pub mod contract; \ No newline at end of file diff --git a/contracts/test/dao-proposal-hook-counter/Cargo.toml b/contracts/test/dao-proposal-hook-counter/Cargo.toml index 395fb338a..00606da08 100644 --- a/contracts/test/dao-proposal-hook-counter/Cargo.toml +++ b/contracts/test/dao-proposal-hook-counter/Cargo.toml @@ -23,7 +23,7 @@ cw-storage-plus = { workspace = true } cw2 = { workspace = true } thiserror = { workspace = true } dao-hooks = { workspace = true } - +cw-orch = { workspace = true } [dev-dependencies] cw-hooks = { workspace = true } cw20 = { workspace = true } diff --git a/contracts/test/dao-proposal-hook-counter/src/msg.rs b/contracts/test/dao-proposal-hook-counter/src/msg.rs index ad6048228..9a4a6f5e6 100644 --- a/contracts/test/dao-proposal-hook-counter/src/msg.rs +++ b/contracts/test/dao-proposal-hook-counter/src/msg.rs @@ -8,6 +8,7 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { ProposalHook(ProposalHookMsg), StakeChangeHook(StakeChangedHookMsg), @@ -15,7 +16,7 @@ pub enum ExecuteMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(Uint128)] StakeCounter {}, diff --git a/contracts/test/dao-proposal-sudo/Cargo.toml b/contracts/test/dao-proposal-sudo/Cargo.toml index f6e51f0de..cb4fa9f46 100644 --- a/contracts/test/dao-proposal-sudo/Cargo.toml +++ b/contracts/test/dao-proposal-sudo/Cargo.toml @@ -24,6 +24,7 @@ cw2 = { workspace = true } thiserror = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } +cw-orch = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/test/dao-proposal-sudo/src/msg.rs b/contracts/test/dao-proposal-sudo/src/msg.rs index 142148400..f6b6e32dc 100644 --- a/contracts/test/dao-proposal-sudo/src/msg.rs +++ b/contracts/test/dao-proposal-sudo/src/msg.rs @@ -7,12 +7,14 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { + #[cw_orch(fn_name("proposal_execute"))] Execute { msgs: Vec }, } #[cw_serde] -#[derive(QueryResponses)] +#[derive(cw_orch::QueryFns, QueryResponses)] pub enum QueryMsg { #[returns(cosmwasm_std::Addr)] Admin {}, diff --git a/contracts/test/dao-test-custom-factory/Cargo.toml b/contracts/test/dao-test-custom-factory/Cargo.toml index 1c7a8cb5c..e25fe5a54 100644 --- a/contracts/test/dao-test-custom-factory/Cargo.toml +++ b/contracts/test/dao-test-custom-factory/Cargo.toml @@ -33,6 +33,6 @@ cw-tokenfactory-issuer = { workspace = true, features = [ "library", "osmosis_tokenfactory", ] } - +cw-orch = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/test/dao-test-custom-factory/src/msg.rs b/contracts/test/dao-test-custom-factory/src/msg.rs index 42128cd2e..7e875d0d8 100644 --- a/contracts/test/dao-test-custom-factory/src/msg.rs +++ b/contracts/test/dao-test-custom-factory/src/msg.rs @@ -7,6 +7,7 @@ use dao_interface::token::NewTokenInfo; pub struct InstantiateMsg {} #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Example NFT factory implementation NftFactory { @@ -37,7 +38,7 @@ pub enum ExecuteMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(dao_interface::voting::InfoResponse)] Info {}, diff --git a/contracts/test/dao-voting-cw20-balance/Cargo.toml b/contracts/test/dao-voting-cw20-balance/Cargo.toml index f126b6840..f750ba315 100644 --- a/contracts/test/dao-voting-cw20-balance/Cargo.toml +++ b/contracts/test/dao-voting-cw20-balance/Cargo.toml @@ -26,7 +26,8 @@ cw-utils = { workspace = true } thiserror = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } +cw20-base = { workspace = true, features = ["library"] } +cw-orch = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/test/dao-voting-cw20-balance/src/msg.rs b/contracts/test/dao-voting-cw20-balance/src/msg.rs index c41590ee2..43002a7ba 100644 --- a/contracts/test/dao-voting-cw20-balance/src/msg.rs +++ b/contracts/test/dao-voting-cw20-balance/src/msg.rs @@ -33,5 +33,5 @@ pub enum ExecuteMsg {} #[cw20_token_query] #[voting_module_query] #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg {} diff --git a/contracts/voting/dao-voting-cw20-staked/Cargo.toml b/contracts/voting/dao-voting-cw20-staked/Cargo.toml index 4eae84a64..6a9863868 100644 --- a/contracts/voting/dao-voting-cw20-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw20-staked/Cargo.toml @@ -29,6 +29,7 @@ thiserror = { workspace = true } dao-dao-macros = { workspace = true } dao-interface = { workspace = true } dao-voting = { workspace = true } +cw-orch.workspace = true [dev-dependencies] cw-multi-test = { workspace = true } 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 a2e254e93..17afb6f5c 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.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw20-staked/src/msg.rs b/contracts/voting/dao-voting-cw20-staked/src/msg.rs index bdb5e9f2a..0031d0deb 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/msg.rs @@ -61,6 +61,7 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Sets the active threshold to a new value. Only the /// instantiator this contract (a DAO most likely) may call this @@ -74,7 +75,7 @@ pub enum ExecuteMsg { #[cw20_token_query] #[active_query] #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { /// Gets the address of the cw20-stake contract this voting module /// is wrapping. diff --git a/contracts/voting/dao-voting-cw4/Cargo.toml b/contracts/voting/dao-voting-cw4/Cargo.toml index 55c671560..6ad83eeb7 100644 --- a/contracts/voting/dao-voting-cw4/Cargo.toml +++ b/contracts/voting/dao-voting-cw4/Cargo.toml @@ -27,6 +27,7 @@ dao-dao-macros = { workspace = true } dao-interface = { workspace = true } cw4 = { workspace = true } cw4-group = { workspace = true } +cw-orch.workspace = true [dev-dependencies] cw-multi-test = { workspace = true } 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 f5c5a14ec..d8908601f 100644 --- a/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json +++ b/contracts/voting/dao-voting-cw4/schema/dao-voting-cw4.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-cw4", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw4/src/msg.rs b/contracts/voting/dao-voting-cw4/src/msg.rs index 24bd0eebc..8ae8cc6b9 100644 --- a/contracts/voting/dao-voting-cw4/src/msg.rs +++ b/contracts/voting/dao-voting-cw4/src/msg.rs @@ -18,11 +18,12 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg {} #[voting_module_query] #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(cosmwasm_std::Addr)] GroupContract {}, diff --git a/contracts/voting/dao-voting-cw721-roles/Cargo.toml b/contracts/voting/dao-voting-cw721-roles/Cargo.toml index e816d763c..3ad3128ea 100644 --- a/contracts/voting/dao-voting-cw721-roles/Cargo.toml +++ b/contracts/voting/dao-voting-cw721-roles/Cargo.toml @@ -29,6 +29,7 @@ cw-utils = { workspace = true } cw2 = { workspace = true } cw4 = { workspace = true } thiserror = { workspace = true } +cw-orch.workspace = true [dev-dependencies] cw-multi-test = { workspace = true } 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 index be8df0318..71f8d7c38 100644 --- 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 @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-cw721-roles", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw721-roles/src/error.rs b/contracts/voting/dao-voting-cw721-roles/src/error.rs index 2fa498222..62c33b0c6 100644 --- a/contracts/voting/dao-voting-cw721-roles/src/error.rs +++ b/contracts/voting/dao-voting-cw721-roles/src/error.rs @@ -15,7 +15,7 @@ pub enum ContractError { #[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")] + #[error("Only the owner of this contract may execute this message")] NotOwner {}, #[error("Got a submessage reply with unknown id: {id}")] diff --git a/contracts/voting/dao-voting-cw721-roles/src/msg.rs b/contracts/voting/dao-voting-cw721-roles/src/msg.rs index b15099529..3a2314adf 100644 --- a/contracts/voting/dao-voting-cw721-roles/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-roles/src/msg.rs @@ -44,11 +44,12 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg {} #[voting_module_query] #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(crate::state::Config)] Config {}, diff --git a/contracts/voting/dao-voting-cw721-staked/Cargo.toml b/contracts/voting/dao-voting-cw721-staked/Cargo.toml index af7d5532e..edb6f2a9f 100644 --- a/contracts/voting/dao-voting-cw721-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw721-staked/Cargo.toml @@ -38,6 +38,7 @@ dao-hooks = { workspace = true } dao-interface = { workspace = true } dao-voting = { workspace = true } thiserror = { workspace = true } +cw-orch.workspace = true [dev-dependencies] anyhow = { workspace = true } 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 d131f5109..aae82a3f8 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,6 +1,6 @@ { "contract_name": "dao-voting-cw721-staked", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-cw721-staked/src/error.rs b/contracts/voting/dao-voting-cw721-staked/src/error.rs index 287c7a509..df9526bd2 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/error.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/error.rs @@ -38,7 +38,7 @@ pub enum ContractError { #[error("Nothing to claim")] NothingToClaim {}, - #[error("Only the owner of this contract my execute this message")] + #[error("Only the owner of this contract may execute this message")] NotOwner {}, #[error("Can not unstake that which you have not staked (unstaking {token_id})")] diff --git a/contracts/voting/dao-voting-cw721-staked/src/lib.rs b/contracts/voting/dao-voting-cw721-staked/src/lib.rs index d4a73c5be..595daabe0 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/lib.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/lib.rs @@ -5,7 +5,4 @@ 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-staked/src/msg.rs b/contracts/voting/dao-voting-cw721-staked/src/msg.rs index 837851ed3..d318ddb48 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/msg.rs @@ -45,6 +45,7 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Used to stake NFTs. To stake a NFT send a cw721 send message /// to this contract with the NFT you would like to stake. The @@ -75,7 +76,7 @@ pub enum ExecuteMsg { #[active_query] #[voting_module_query] #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { #[returns(crate::state::Config)] Config {}, 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 de0824f52..c454212c4 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs @@ -18,7 +18,7 @@ mod test_tube_env; use cosmwasm_std::Addr; use cw_multi_test::{App, Executor}; use cw_utils::Duration; -use dao_testing::contracts::voting_cw721_staked_contract; +use dao_testing::contracts::cw721_staked_voting_contract; use crate::msg::{InstantiateMsg, NftContract}; @@ -35,7 +35,7 @@ pub(crate) struct 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()); + let module_id = app.store_code(cw721_staked_voting_contract()); let nft = instantiate_cw721_base(&mut app, CREATOR_ADDR, CREATOR_ADDR); let module = app 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 cdc057bb7..7126bd904 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs @@ -6,7 +6,7 @@ use cw_multi_test::{next_block, App, BankSudo, Executor, SudoMsg}; use cw_utils::Duration; use dao_interface::voting::IsActiveResponse; use dao_testing::contracts::{ - cw721_base_contract, dao_test_custom_factory, voting_cw721_staked_contract, + cw721_base_contract, cw721_staked_voting_contract, dao_test_custom_factory, }; use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; @@ -34,7 +34,7 @@ use super::{ #[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 module_id = app.store_code(cw721_staked_voting_contract()); let cw721_id = app.store_code(cw721_base_contract()); let module_addr = app @@ -421,7 +421,7 @@ fn test_add_remove_hooks() -> anyhow::Result<()> { #[test] fn test_instantiate_with_invalid_duration_fails() { let mut app = App::default(); - let module_id = app.store_code(voting_cw721_staked_contract()); + let module_id = app.store_code(cw721_staked_voting_contract()); let cw721_id = app.store_code(cw721_base_contract()); let err = app @@ -462,7 +462,7 @@ fn test_instantiate_with_invalid_duration_fails() { 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()); + let module_id = app.store_code(cw721_staked_voting_contract()); app.instantiate_contract( module_id, @@ -502,7 +502,7 @@ fn test_instantiate_zero_active_threshold_count() { 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()); + let module_id = app.store_code(cw721_staked_voting_contract()); app.instantiate_contract( module_id, @@ -541,7 +541,7 @@ fn test_instantiate_invalid_active_threshold_count_new_nft() { #[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 module_id = app.store_code(cw721_staked_voting_contract()); let cw721_addr = instantiate_cw721_base(&mut app, CREATOR_ADDR, CREATOR_ADDR); app.instantiate_contract( @@ -567,7 +567,7 @@ fn test_instantiate_invalid_active_threshold_count_existing_nft() { 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 module_id = app.store_code(cw721_staked_voting_contract()); let voting_addr = app .instantiate_contract( @@ -647,7 +647,7 @@ fn test_active_threshold_absolute_count() { 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 module_id = app.store_code(cw721_staked_voting_contract()); let voting_addr = app .instantiate_contract( @@ -708,7 +708,7 @@ fn test_active_threshold_percent() { 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 module_id = app.store_code(cw721_staked_voting_contract()); let voting_addr = app .instantiate_contract( @@ -810,7 +810,7 @@ fn test_active_threshold_percent_rounds_up() { 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 module_id = app.store_code(cw721_staked_voting_contract()); let voting_addr = app .instantiate_contract( @@ -887,7 +887,7 @@ fn test_update_active_threshold() { 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()); + let module_id = app.store_code(cw721_staked_voting_contract()); app.instantiate_contract( module_id, @@ -929,7 +929,7 @@ fn test_active_threshold_percentage_gt_100() { 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()); + let module_id = app.store_code(cw721_staked_voting_contract()); app.instantiate_contract( module_id, @@ -967,7 +967,7 @@ fn test_active_threshold_percentage_lte_0() { #[test] fn test_invalid_instantiate_msg() { let mut app = App::default(); - let module_id = app.store_code(voting_cw721_staked_contract()); + let module_id = app.store_code(cw721_staked_voting_contract()); let cw721_id = app.store_code(cw721_base_contract()); let err = app @@ -1006,7 +1006,7 @@ fn test_invalid_instantiate_msg() { #[test] fn test_invalid_initial_nft_msg() { let mut app = App::default(); - let module_id = app.store_code(voting_cw721_staked_contract()); + let module_id = app.store_code(cw721_staked_voting_contract()); let cw721_id = app.store_code(cw721_base_contract()); let err = app @@ -1045,7 +1045,7 @@ fn test_invalid_initial_nft_msg() { #[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 module_id = app.store_code(cw721_staked_voting_contract()); let cw721_id = app.store_code(cw721_base_contract()); let err = app @@ -1096,7 +1096,7 @@ fn test_invalid_initial_nft_msg_wrong_absolute_count() { 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 module_id = app.store_code(cw721_staked_voting_contract()); let err = app .instantiate_contract( @@ -1133,7 +1133,7 @@ fn test_no_initial_nfts_fails() { #[test] fn test_factory() { let mut app = App::default(); - let module_id = app.store_code(voting_cw721_staked_contract()); + let module_id = app.store_code(cw721_staked_voting_contract()); let cw721_id = app.store_code(cw721_base_contract()); let factory_id = app.store_code(dao_test_custom_factory()); @@ -1186,7 +1186,7 @@ fn test_factory() { #[test] fn test_factory_with_funds_pass_through() { let mut app = App::default(); - let module_id = app.store_code(voting_cw721_staked_contract()); + let module_id = app.store_code(cw721_staked_voting_contract()); let cw721_id = app.store_code(cw721_base_contract()); let factory_id = app.store_code(dao_test_custom_factory()); @@ -1307,7 +1307,7 @@ fn test_factory_with_funds_pass_through() { #[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 module_id = app.store_code(cw721_staked_voting_contract()); let cw721_id = app.store_code(cw721_base_contract()); // Instantiate using factory succeeds @@ -1352,7 +1352,7 @@ fn test_unsupported_factory_msg() { )] fn test_factory_wrong_callback() { let mut app = App::default(); - let module_id = app.store_code(voting_cw721_staked_contract()); + let module_id = app.store_code(cw721_staked_voting_contract()); let _cw721_id = app.store_code(cw721_base_contract()); let factory_id = app.store_code(dao_test_custom_factory()); @@ -1400,7 +1400,7 @@ fn test_factory_wrong_callback() { #[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 module_id = app.store_code(cw721_staked_voting_contract()); let _cw721_id = app.store_code(cw721_base_contract()); let factory_id = app.store_code(dao_test_custom_factory()); diff --git a/contracts/voting/dao-voting-onft-staked/Cargo.toml b/contracts/voting/dao-voting-onft-staked/Cargo.toml new file mode 100644 index 000000000..9c96ec2d7 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "dao-voting-onft-staked" +authors = [ + "CypherApe cypherape@protonmail.com", + "Jake Hartnell", + "ekez", + "noah ", +] +description = "A DAO DAO voting module based on staked x/onft tokens." +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 } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw-controllers = { workspace = true } +cw-hooks = { workspace = true } +cw721-controllers = { 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 } +omniflix-std = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +anyhow = { 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-test-tube = { workspace = true } +prost = { workspace = true } +prost-derive = { workspace = true } diff --git a/contracts/voting/dao-voting-onft-staked/README.md b/contracts/voting/dao-voting-onft-staked/README.md new file mode 100644 index 000000000..ac844ba94 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/README.md @@ -0,0 +1,45 @@ +# `dao-voting-onft-staked` + +This is a basic implementation of an NFT staking contract that supports +OmniFlix's NFT standard: +[x/onft](https://github.com/OmniFlix/omniflixhub/tree/main/x/onft). + +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). + +### Stake process + +Unlike the base cw721 smart contract, the x/onft SDK module doesn't support +executing a smart contract on NFT transfer, so the stake process is broken up +into three steps: + +1. The sender calls `PrepareStake` to inform this staking contract of the NFTs + that are about to be staked. This will succeed only if the sender currently + owns the NFT(s). +2. The sender then transfers the NFT(s) to the staking contract. +3. The sender calls `ConfirmStake` on this staking contract which confirms the + NFTs were transferred to it and registers the stake. + +In case this process is interrupted, or executed incorrectly (e.g. the sender +accidentally transfers an NFT to the staking contract without first preparing +it), there is also a `CancelStake` action to help recover NFTs. If called by: + +- the original stake preparer, the preparation will be canceled, and the NFT(s) + will be sent back if the staking contract owns them. +- the current NFT(s) owner, the preparation will be canceled, if any. +- the DAO, the preparation will be canceled (if any exists), and the NFT(s) will + be sent to the specified recipient (if the staking contract owns them). if no + recipient is specified but the NFT was prepared, it will be sent back to the + preparer. + +The recipient field only applies when the sender is the DAO. In the other cases, +the NFT(s) will always be sent back to the sender. Note: if the NFTs were sent +to the staking contract, but no stake was prepared, only the DAO will be able to +correct this and send them somewhere. + +The `PrepareStake` step overrides any previous `PrepareStake` calls as long as +the new sender owns the NFT(s) and the first stake was never confirmed (which +should be impossible if someone else now owns the NFT(s)). Thus there is no +combination of messages or steps where someone can stake nor prevent stake when +it would otherwise be valid. A stake is only ever confirmed if it was prepared +and transferred by the same address confirming, and the DAO can always recover +an NFT that accidentally skipped the preparation step. diff --git a/contracts/voting/dao-voting-onft-staked/examples/schema.rs b/contracts/voting/dao-voting-onft-staked/examples/schema.rs new file mode 100644 index 000000000..45e321363 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use dao_voting_onft_staked::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/voting/dao-voting-onft-staked/schema/dao-voting-onft-staked.json b/contracts/voting/dao-voting-onft-staked/schema/dao-voting-onft-staked.json new file mode 100644 index 000000000..07d68128f --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/schema/dao-voting-onft-staked.json @@ -0,0 +1,1034 @@ +{ + "contract_name": "dao-voting-onft-staked", + "contract_version": "2.5.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "onft_collection" + ], + "properties": { + "active_threshold": { + "description": "The number or percentage of tokens that must be staked for the DAO to be active", + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + }, + "onft_collection": { + "description": "ONFT collection that will be staked.", + "allOf": [ + { + "$ref": "#/definitions/OnftCollection" + } + ] + }, + "unstaking_duration": { + "description": "Amount of time between unstaking and tokens being available. To unstake with no delay, leave as `None`.", + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "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 + } + ] + }, + "OnftCollection": { + "oneOf": [ + { + "description": "Uses an existing x/onft denom/collection.", + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "description": "ID of an already created x/onft denom/collection.", + "type": "string" + } + }, + "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": "Step 1/3 of the NFT staking process. x/onft doesn't support executing a smart contract on NFT transfer like cw721s do, so the stake process is broken up: 1. The sender calls `PrepareStake` to inform this staking contract of the NFTs that are about to be staked. This will succeed only if the sender currently owns the NFT(s). 2. The sender then transfers the NFT(s) to the staking contract. 3. The sender calls `ConfirmStake` on this staking contract which confirms the NFTs were transferred to it and registers the stake.\n\nPrepareStake overrides any previous PrepareStake calls, as long as the sender owns the NFT(s).", + "type": "object", + "required": [ + "prepare_stake" + ], + "properties": { + "prepare_stake": { + "type": "object", + "required": [ + "token_ids" + ], + "properties": { + "token_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Step 3/3 of the NFT staking process. x/onft doesn't support executing a smart contract on NFT transfer like cw721s do, so the stake process is broken up: 1. The sender calls `PrepareStake` to inform this staking contract of the NFTs that are about to be staked. This will succeed only if the sender currently owns the NFT(s). 2. The sender then transfers the NFT(s) to the staking contract. 3. The sender calls `ConfirmStake` on this staking contract which confirms the NFTs were transferred to it and registers the stake.", + "type": "object", + "required": [ + "confirm_stake" + ], + "properties": { + "confirm_stake": { + "type": "object", + "required": [ + "token_ids" + ], + "properties": { + "token_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "CancelStake serves as an undo function in case an NFT or stake gets into a bad state, either because the stake process was never completed, or because someone sent an NFT to the staking contract without preparing the stake first.\n\nIf called by: - the original stake preparer, the preparation will be canceled, and the NFT(s) will be sent back if the staking contract owns them. - the current NFT(s) owner, the preparation will be canceled, if any. - the DAO, the preparation will be canceled (if any exists), and the NFT(s) will be sent to the specified recipient (if the staking contract owns them). if no recipient is specified but the NFT was prepared, it will be sent back to the preparer.\n\nThe recipient field only applies when the sender is the DAO. In the other cases, the NFT(s) will always be sent back to the sender. Note: if the NFTs were sent to the staking contract, but no stake was prepared, only the DAO will be able to correct this and send them somewhere.", + "type": "object", + "required": [ + "cancel_stake" + ], + "properties": { + "cancel_stake": { + "type": "object", + "required": [ + "token_ids" + ], + "properties": { + "recipient": { + "type": [ + "string", + "null" + ] + }, + "token_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Unstakes the specified token_ids on behalf of the sender. token_ids must have unique values and have non-zero length.", + "type": "object", + "required": [ + "unstake" + ], + "properties": { + "unstake": { + "type": "object", + "required": [ + "token_ids" + ], + "properties": { + "token_ids": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Claim NFTs that have been unstaked for the specified duration.", + "type": "object", + "required": [ + "claim_nfts" + ], + "properties": { + "claim_nfts": { + "type": "object", + "additionalProperties": false + } + }, + "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" + ], + "properties": { + "update_config": { + "type": "object", + "properties": { + "duration": { + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "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" + ], + "properties": { + "add_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "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" + ], + "properties": { + "remove_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "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 + } + ] + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "oneOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Time in seconds", + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "nft_claims" + ], + "properties": { + "nft_claims": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "staked_nfts" + ], + "properties": { + "staked_nfts": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "active_threshold" + ], + "properties": { + "active_threshold": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "is_active" + ], + "properties": { + "is_active": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the voting power for an address at a given height.", + "type": "object", + "required": [ + "voting_power_at_height" + ], + "properties": { + "voting_power_at_height": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total voting power at a given block heigh.", + "type": "object", + "required": [ + "total_power_at_height" + ], + "properties": { + "total_power_at_height": { + "type": "object", + "properties": { + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the address of the DAO this module belongs to.", + "type": "object", + "required": [ + "dao" + ], + "properties": { + "dao": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract version info.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "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" + } + } + }, + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "onft_collection_id" + ], + "properties": { + "onft_collection_id": { + "type": "string" + }, + "unstaking_duration": { + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "oneOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Time in seconds", + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + ] + } + } + }, + "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" + }, + "hooks": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HooksResponse", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InfoResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "$ref": "#/definitions/ContractVersion" + } + }, + "additionalProperties": false, + "definitions": { + "ContractVersion": { + "type": "object", + "required": [ + "contract", + "version" + ], + "properties": { + "contract": { + "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing", + "type": "string" + }, + "version": { + "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "is_active": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, + "nft_claims": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NftClaimsResponse", + "type": "object", + "required": [ + "nft_claims" + ], + "properties": { + "nft_claims": { + "type": "array", + "items": { + "$ref": "#/definitions/NftClaim" + } + } + }, + "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 + } + ] + }, + "NftClaim": { + "type": "object", + "required": [ + "release_at", + "token_id" + ], + "properties": { + "release_at": { + "$ref": "#/definitions/Expiration" + }, + "token_id": { + "type": "string" + } + }, + "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" + } + } + }, + "staked_nfts": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_String", + "type": "array", + "items": { + "type": "string" + } + }, + "total_power_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "voting_power_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VotingPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/contracts/voting/dao-voting-onft-staked/src/contract.rs b/contracts/voting/dao-voting-onft-staked/src/contract.rs new file mode 100644 index 000000000..2ab1ebd73 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/contract.rs @@ -0,0 +1,702 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, + StdResult, SubMsg, Uint128, Uint256, +}; +use cw2::{get_contract_version, set_contract_version, ContractVersion}; +use cw_storage_plus::Bound; +use cw_utils::Duration; +use dao_hooks::nft_stake::{stake_nft_hook_msgs, unstake_nft_hook_msgs}; +use dao_interface::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, OnftCollection, QueryMsg}; +use crate::omniflix::{get_onft_transfer_msg, query_onft_owner, query_onft_supply}; +use crate::state::{ + register_staked_nfts, register_unstaked_nfts, Config, ACTIVE_THRESHOLD, CONFIG, DAO, HOOKS, + MAX_CLAIMS, NFT_BALANCES, NFT_CLAIMS, PREPARED_ONFTS, STAKED_NFTS_PER_OWNER, TOTAL_STAKED_NFTS, +}; +use crate::ContractError; + +pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-onft-staked"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// We multiply by this when calculating needed power for being active +// when using active threshold with percent +const PRECISION_FACTOR: u128 = 10u128.pow(9); + +#[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)?; + + // Validate unstaking duration + validate_duration(msg.unstaking_duration)?; + + // 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 collection. + + let OnftCollection::Existing { ref id } = msg.onft_collection; + let nft_supply = query_onft_supply(deps.as_ref(), id)?; + + // 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.into()))?; + } + } + ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?; + } + + TOTAL_STAKED_NFTS.save(deps.storage, &Uint128::zero(), env.block.height)?; + + match msg.onft_collection { + OnftCollection::Existing { id } => { + let config = Config { + onft_collection_id: id.clone(), + unstaking_duration: msg.unstaking_duration, + }; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attribute("onft_collection_id", id)) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result, ContractError> { + match msg { + ExecuteMsg::PrepareStake { token_ids } => execute_prepare_stake(deps, info, token_ids), + ExecuteMsg::ConfirmStake { token_ids } => execute_confirm_stake(deps, env, info, token_ids), + ExecuteMsg::CancelStake { + token_ids, + recipient, + } => execute_cancel_stake(deps, env, info, token_ids, recipient), + ExecuteMsg::Unstake { token_ids } => execute_unstake(deps, env, info, token_ids), + ExecuteMsg::ClaimNfts {} => execute_claim_nfts(deps, env, info), + 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) + } + } +} + +pub fn execute_prepare_stake( + deps: DepsMut, + info: MessageInfo, + token_ids: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // verify sender owns all the tokens + let owns_all = token_ids + .iter() + .map(|token_id| -> StdResult { + let owner = query_onft_owner(deps.as_ref(), &config.onft_collection_id, token_id)?; + + Ok(owner == info.sender) + }) + .collect::>>()? + .into_iter() + .all(|b| b); + + if !owns_all { + return Err(ContractError::OnlyOwnerCanPrepareStake {}); + } + + // save and override prepared ONFTS, readying them to be transferred and + // staked + for token_id in &token_ids { + PREPARED_ONFTS.save(deps.storage, token_id.to_string(), &info.sender)?; + } + + Ok(Response::default() + .add_attribute("action", "prepare_stake") + .add_attribute("preparer", info.sender.to_string()) + .add_attribute("token_ids", token_ids.join(","))) +} + +pub fn execute_confirm_stake( + deps: DepsMut, + env: Env, + info: MessageInfo, + mut token_ids: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // de-duplicate token IDs to prevent double-counting exploit + token_ids.sort(); + token_ids.dedup(); + + // verify sender prepared and transferred all the tokens + let sender_prepared_all = token_ids + .iter() + .map(|token_id| -> StdResult { + // check if sender prepared + let prepared = PREPARED_ONFTS + .may_load(deps.storage, token_id.to_string())? + .map_or(false, |preparer| preparer == info.sender); + + // check that NFT was transferred to this contract + let owner = query_onft_owner(deps.as_ref(), &config.onft_collection_id, token_id)?; + + Ok(prepared && owner == env.contract.address) + }) + .collect::>>()? + .into_iter() + .all(|b| b); + + if !sender_prepared_all { + return Err(ContractError::StakeMustBePrepared {}); + } + + register_staked_nfts(deps.storage, env.block.height, &info.sender, &token_ids)?; + + let hook_msgs = token_ids + .iter() + .map(|token_id| { + stake_nft_hook_msgs(HOOKS, deps.storage, info.sender.clone(), token_id.clone()) + }) + .collect::>>>()? + .into_iter() + .flatten() + .collect::>(); + + Ok(Response::default() + .add_submessages(hook_msgs) + .add_attribute("action", "stake") + .add_attribute("from", info.sender) + .add_attribute("token_ids", token_ids.join(","))) +} + +/// CancelStake serves as an undo function in case an NFT or stake gets into a +/// bad state, either because the stake process was never completed, or because +/// someone sent an NFT to the staking contract without preparing the stake +/// first. +/// +/// If called by: +/// - the original stake preparer, the preparation will be canceled, and the +/// NFT(s) will be sent back if the staking contract owns them. +/// - the current NFT(s) owner, the preparation will be canceled, if any. +/// - the DAO, the preparation will be canceled (if any exists), and the NFT(s) +/// will be sent to the specified recipient (if the staking contract owns +/// them). if no recipient is specified but the NFT was prepared, it will be +/// sent back to the preparer. +/// +/// The recipient field only applies when the sender is the DAO. In the other +/// cases, the NFT(s) will always be sent back to the sender. Note: if the NFTs +/// were sent to the staking contract, but no stake was prepared, only the DAO +/// will be able to correct this and send them somewhere. +pub fn execute_cancel_stake( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_ids: Vec, + recipient: Option, +) -> Result { + let dao = DAO.load(deps.storage)?; + let config = CONFIG.load(deps.storage)?; + + // get preparers and owners of NFTs + let token_ids_with_owners_and_preparers = token_ids + .iter() + .map(|token_id| { + let preparer = PREPARED_ONFTS.may_load(deps.storage, token_id.clone())?; + + let owner = query_onft_owner(deps.as_ref(), &config.onft_collection_id, token_id)?; + + Ok((token_id, owner, preparer)) + }) + .collect::)>>>()?; + + let mut transfer_msgs: Vec = vec![]; + + // If DAO, cancel preparations (if any) and send NFTs to the specified + // recipient. + if info.sender == dao { + for (token_id, owner, preparer) in token_ids_with_owners_and_preparers { + // cancel preparation if it exists + if preparer.is_some() { + PREPARED_ONFTS.remove(deps.storage, token_id.to_string()); + } + + // if this contract owns the NFT, send it to the recipient (or + // preparer if one exists and no recipient was specified). + if owner == env.contract.address { + let recipient = recipient + .clone() + .or_else(|| preparer.map(|p| p.to_string())); + + if let Some(recipient) = recipient { + transfer_msgs.push(get_onft_transfer_msg( + &config.onft_collection_id, + token_id, + env.contract.address.as_str(), + &recipient, + )); + } else { + return Err(ContractError::NoRecipient {}); + } + } + } + } else { + for (token_id, owner, preparer) in token_ids_with_owners_and_preparers { + let is_preparer = preparer.as_ref().map_or(false, |p| *p == info.sender); + // only owner or preparer can cancel stake + if info.sender != owner && !is_preparer { + return Err(ContractError::NotPreparerNorOwner {}); + } + + // cancel preparation + PREPARED_ONFTS.remove(deps.storage, token_id.to_string()); + + // if owner is this staking contract, send it back to the preparer, + // who must also be the sender (but let's force unwrap the preparer + // just to make sure) + if owner == env.contract.address { + transfer_msgs.push(get_onft_transfer_msg( + &config.onft_collection_id, + token_id, + env.contract.address.as_str(), + preparer.unwrap().as_ref(), + )); + } + } + } + + Ok(Response::default() + .add_messages(transfer_msgs) + .add_attribute("action", "cancel_stake") + .add_attribute("sender", info.sender) + .add_attribute("token_ids", token_ids.join(",")) + .add_attribute( + "recipient", + recipient.unwrap_or_else(|| "_none".to_string()), + )) +} + +pub fn execute_unstake( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_ids: Vec, +) -> Result { + if token_ids.is_empty() { + return Err(ContractError::ZeroUnstake {}); + } + + register_unstaked_nfts(deps.storage, env.block.height, &info.sender, &token_ids)?; + + // Provided that the backing cw721 contract is non-malicious: + // + // 1. no token that has been staked may be staked again before + // first being unstaked. + // + // Provided that the other methods on this contract are functional: + // + // 2. there will never exist a pending claim for a token that is + // unstaked. + // 3. (6) => claims may only be created for tokens that are staked. + // 4. (1) && (2) && (3) => there will never be a staked NFT for + // which there is also a pending claim. + // + // (aside: the requirement on (1) for (4) may be confusing. it is + // needed because if a token could be staked more than once, a + // token could be staked, moved into the claims queue, and then + // staked again, in which case the token is both staked and has a + // pending claim.) + // + // If we reach this point in execution, `register_unstaked_nfts` + // has not errored and thus: + // + // 5. token_ids contains no duplicate values. + // 6. all NFTs in token_ids were staked by `info.sender` + // 7. (4) && (6) => none of the tokens in token_ids are in the + // claims queue for `info.sender` + // + // (5) && (7) are the invariants for calling `create_nft_claims` + // so if we reach this point in execution, we may safely create + // claims. + + 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 { + None => { + let return_messages = token_ids + .into_iter() + .map(|token_id| -> CosmosMsg { + get_onft_transfer_msg( + &config.onft_collection_id, + &token_id, + env.contract.address.as_str(), + info.sender.as_str(), + ) + }) + .collect::>(); + + Ok(Response::default() + .add_messages(return_messages) + .add_submessages(hook_msgs) + .add_attribute("action", "unstake") + .add_attribute("from", info.sender) + .add_attribute("claim_duration", "None")) + } + + Some(duration) => { + let outstanding_claims = NFT_CLAIMS + .query_claims(deps.as_ref(), &info.sender)? + .nft_claims; + if outstanding_claims.len() + token_ids.len() > MAX_CLAIMS as usize { + return Err(ContractError::TooManyClaims {}); + } + + // Out of gas here is fine - just try again with fewer + // tokens. + NFT_CLAIMS.create_nft_claims( + deps.storage, + &info.sender, + token_ids, + duration.after(&env.block), + )?; + + Ok(Response::default() + .add_attribute("action", "unstake") + .add_submessages(hook_msgs) + .add_attribute("from", info.sender) + .add_attribute("claim_duration", format!("{duration}"))) + } + } +} + +pub fn execute_claim_nfts( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let nfts = NFT_CLAIMS.claim_nfts(deps.storage, &info.sender, &env.block)?; + if nfts.is_empty() { + return Err(ContractError::NothingToClaim {}); + } + + let config = CONFIG.load(deps.storage)?; + + let msgs = nfts + .into_iter() + .map(|nft| -> CosmosMsg { + get_onft_transfer_msg( + &config.onft_collection_id, + &nft, + env.contract.address.as_str(), + info.sender.as_str(), + ) + }) + .collect::>(); + + Ok(Response::default() + .add_messages(msgs) + .add_attribute("action", "claim_nfts") + .add_attribute("from", info.sender)) +} + +pub fn execute_update_config( + info: MessageInfo, + deps: DepsMut, + duration: Option, +) -> Result { + let mut config: Config = CONFIG.load(deps.storage)?; + let dao = DAO.load(deps.storage)?; + + // Only the DAO can update the config. + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + // Validate unstaking duration + validate_duration(duration)?; + + config.unstaking_duration = duration; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("action", "update_config") + .add_attribute( + "unstaking_duration", + config + .unstaking_duration + .map(|d| d.to_string()) + .unwrap_or_else(|| "none".to_string()), + )) +} + +pub fn execute_add_hook( + deps: DepsMut, + info: MessageInfo, + addr: String, +) -> Result { + 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)?; + 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 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)?; + HOOKS.remove_hook(deps.storage, hook)?; + + Ok(Response::default() + .add_attribute("action", "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 = query_onft_supply(deps.as_ref(), &config.onft_collection_id)?; + assert_valid_absolute_count_threshold(count, Uint128::new(nft_supply.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::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 = query_onft_supply(deps, &config.onft_collection_id)?; + + 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).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 }) + } +} + +pub fn query_voting_power_at_height( + deps: Deps, + env: Env, + address: String, + height: Option, +) -> StdResult { + let address = deps.api.addr_validate(&address)?; + let height = height.unwrap_or(env.block.height); + let power = NFT_BALANCES + .may_load_at_height(deps.storage, &address, height)? + .unwrap_or_default(); + to_json_binary(&dao_interface::voting::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 = TOTAL_STAKED_NFTS + .may_load_at_height(deps.storage, height)? + .unwrap_or_default(); + to_json_binary(&dao_interface::voting::TotalPowerAtHeightResponse { power, 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_nft_claims(deps: Deps, address: String) -> StdResult { + to_json_binary(&NFT_CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?) +} + +pub fn query_hooks(deps: Deps) -> StdResult { + to_json_binary(&HOOKS.query_hooks(deps)?) +} + +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_staked_nfts( + deps: Deps, + address: String, + start_after: Option, + limit: Option, +) -> StdResult { + let prefix = deps.api.addr_validate(&address)?; + let prefix = STAKED_NFTS_PER_OWNER.prefix(&prefix); + + let start_after = start_after.as_deref().map(Bound::exclusive); + let range = prefix.keys( + deps.storage, + start_after, + None, + cosmwasm_std::Order::Ascending, + ); + let range: StdResult> = match limit { + Some(l) => range.take(l as usize).collect(), + None => range.collect(), + }; + 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")) +} diff --git a/contracts/voting/dao-voting-onft-staked/src/error.rs b/contracts/voting/dao-voting-onft-staked/src/error.rs new file mode 100644 index 000000000..252e8caf4 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/error.rs @@ -0,0 +1,48 @@ +use cosmwasm_std::StdError; +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)] + UnstakingDurationError(#[from] dao_voting::duration::UnstakingDurationError), + + #[error("Nothing to claim")] + NothingToClaim {}, + + #[error("Only an NFT's owner can prepare it to be staked")] + OnlyOwnerCanPrepareStake {}, + + #[error("NFTs must be prepared and transferred before they can be staked")] + StakeMustBePrepared {}, + + #[error("Recipient must be set when the DAO is cancelling a stake that was not prepared")] + NoRecipient {}, + + #[error("Only the owner or preparer can cancel a prepared stake")] + NotPreparerNorOwner {}, + + #[error("Can not unstake that which you have not staked (unstaking {token_id})")] + NotStaked { token_id: String }, + + #[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("Can't unstake zero NFTs.")] + ZeroUnstake {}, +} diff --git a/contracts/voting/dao-voting-onft-staked/src/lib.rs b/contracts/voting/dao-voting-onft-staked/src/lib.rs new file mode 100644 index 000000000..955654f05 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/lib.rs @@ -0,0 +1,12 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod msg; +mod omniflix; +pub mod state; + +#[cfg(test)] +mod testing; + +pub use crate::error::ContractError; diff --git a/contracts/voting/dao-voting-onft-staked/src/msg.rs b/contracts/voting/dao-voting-onft-staked/src/msg.rs new file mode 100644 index 000000000..08e589bc5 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/msg.rs @@ -0,0 +1,119 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cw_utils::Duration; +use dao_dao_macros::{active_query, voting_module_query}; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; + +#[cw_serde] +#[allow(clippy::large_enum_variant)] +pub enum OnftCollection { + /// Uses an existing x/onft denom/collection. + Existing { + /// ID of an already created x/onft denom/collection. + id: String, + }, +} + +#[cw_serde] +pub struct InstantiateMsg { + /// ONFT collection that will be staked. + pub onft_collection: OnftCollection, + /// Amount of time between unstaking and tokens being available. 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] +pub enum ExecuteMsg { + /// Step 1/3 of the NFT staking process. x/onft doesn't support executing a + /// smart contract on NFT transfer like cw721s do, so the stake process is + /// broken up: + /// 1. The sender calls `PrepareStake` to inform this staking contract of + /// the NFTs that are about to be staked. This will succeed only if the + /// sender currently owns the NFT(s). + /// 2. The sender then transfers the NFT(s) to the staking contract. + /// 3. The sender calls `ConfirmStake` on this staking contract which + /// confirms the NFTs were transferred to it and registers the stake. + /// + /// PrepareStake overrides any previous PrepareStake calls, as long as the + /// sender owns the NFT(s). + PrepareStake { token_ids: Vec }, + /// Step 3/3 of the NFT staking process. x/onft doesn't support executing a + /// smart contract on NFT transfer like cw721s do, so the stake process is + /// broken up: + /// 1. The sender calls `PrepareStake` to inform this staking contract of + /// the NFTs that are about to be staked. This will succeed only if the + /// sender currently owns the NFT(s). + /// 2. The sender then transfers the NFT(s) to the staking contract. + /// 3. The sender calls `ConfirmStake` on this staking contract which + /// confirms the NFTs were transferred to it and registers the stake. + ConfirmStake { token_ids: Vec }, + /// CancelStake serves as an undo function in case an NFT or stake gets into + /// a bad state, either because the stake process was never completed, or + /// because someone sent an NFT to the staking contract without preparing + /// the stake first. + /// + /// If called by: + /// - the original stake preparer, the preparation will be canceled, and the + /// NFT(s) will be sent back if the staking contract owns them. + /// - the current NFT(s) owner, the preparation will be canceled, if any. + /// - the DAO, the preparation will be canceled (if any exists), and the + /// NFT(s) will be sent to the specified recipient (if the staking + /// contract owns them). if no recipient is specified but the NFT was + /// prepared, it will be sent back to the preparer. + /// + /// The recipient field only applies when the sender is the DAO. In the + /// other cases, the NFT(s) will always be sent back to the sender. Note: if + /// the NFTs were sent to the staking contract, but no stake was prepared, + /// only the DAO will be able to correct this and send them somewhere. + CancelStake { + token_ids: Vec, + recipient: Option, + }, + /// 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 }, + /// Claim NFTs that have been unstaked for the specified duration. + ClaimNfts {}, + /// 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)] +pub enum QueryMsg { + #[returns(crate::state::Config)] + Config {}, + #[returns(::cw721_controllers::NftClaimsResponse)] + NftClaims { address: String }, + #[returns(::cw_controllers::HooksResponse)] + Hooks {}, + // List the staked NFTs for a given address. + #[returns(Vec)] + StakedNfts { + address: String, + start_after: Option, + limit: Option, + }, + #[returns(ActiveThresholdResponse)] + ActiveThreshold {}, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/contracts/voting/dao-voting-onft-staked/src/omniflix.rs b/contracts/voting/dao-voting-onft-staked/src/omniflix.rs new file mode 100644 index 000000000..fe018ef0e --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/omniflix.rs @@ -0,0 +1,32 @@ +use cosmwasm_std::{CosmosMsg, Deps, StdError, StdResult}; +use omniflix_std::types::omniflix::onft::v1beta1::{MsgTransferOnft, OnftQuerier}; + +pub fn query_onft_owner(deps: Deps, denom_id: &str, token_id: &str) -> StdResult { + let res = OnftQuerier::new(&deps.querier).onft(denom_id.to_string(), token_id.to_string())?; + let owner = res + .onft + .ok_or(StdError::generic_err("ONFT not found"))? + .owner; + + Ok(owner) +} + +pub fn query_onft_supply(deps: Deps, id: &str) -> StdResult { + let res = OnftQuerier::new(&deps.querier).supply(id.to_string(), "".to_string())?; + Ok(res.amount) +} + +pub fn get_onft_transfer_msg( + denom_id: &str, + token_id: &str, + sender: &str, + recipient: &str, +) -> CosmosMsg { + MsgTransferOnft { + denom_id: denom_id.to_string(), + id: token_id.to_string(), + sender: sender.to_string(), + recipient: recipient.to_string(), + } + .into() +} diff --git a/contracts/voting/dao-voting-onft-staked/src/state.rs b/contracts/voting/dao-voting-onft-staked/src/state.rs new file mode 100644 index 000000000..0e85822ab --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/state.rs @@ -0,0 +1,118 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Empty, StdError, StdResult, Storage, Uint128}; +use cw721_controllers::NftClaims; +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 onft_collection_id: String, + 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"); + +/// NFTs prepared to be staked. The owner must prepare the NFT before +/// transferring and staking so the contract can verify them as the rightful +/// owner before staking. Since ONFT transfer actions cannot include a message +/// to execute on transfer, we can't verify who sent an ONFT, so we have to +/// prepare it first. Once a stake is confirmed, the prepared stake is removed. +/// +/// Map token ID to validated preparer. +pub const PREPARED_ONFTS: Map = Map::new("po"); + +/// The set of NFTs currently staked by each address. The existence of +/// an `(address, token_id)` pair implies that `address` has staked +/// `token_id`. +pub const STAKED_NFTS_PER_OWNER: Map<(&Addr, &str), Empty> = Map::new("snpw"); +/// The number of NFTs staked by an address as a function of block +/// height. +pub const NFT_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new( + "nb", + "nb__checkpoints", + "nb__changelog", + Strategy::EveryBlock, +); +/// The number of NFTs staked with this contract as a function of +/// block height. +pub const TOTAL_STAKED_NFTS: SnapshotItem = SnapshotItem::new( + "tsn", + "tsn__checkpoints", + "tsn__changelog", + Strategy::EveryBlock, +); + +/// The maximum number of claims that may be outstanding. +pub const MAX_CLAIMS: u64 = 70; +pub const NFT_CLAIMS: NftClaims = NftClaims::new("nft_claims"); + +// Hooks to contracts that will receive staking and unstaking +// messages. +pub const HOOKS: Hooks = Hooks::new("hooks"); + +pub fn register_staked_nfts( + storage: &mut dyn Storage, + height: u64, + staker: &Addr, + token_ids: &Vec, +) -> StdResult<()> { + let count = token_ids.len() as u128; + let add_count = |prev: Option| -> StdResult { + prev.unwrap_or_default() + .checked_add(Uint128::new(count)) + .map_err(StdError::overflow) + }; + + for token_id in token_ids { + PREPARED_ONFTS.remove(storage, token_id.to_string()); + STAKED_NFTS_PER_OWNER.save(storage, (staker, token_id), &Empty::default())?; + } + + NFT_BALANCES.update(storage, staker, height, add_count)?; + TOTAL_STAKED_NFTS + .update(storage, height, add_count) + .map(|_| ()) +} + +/// Registers the unstaking of TOKEN_IDs in storage. Errors if: +/// +/// 1. `token_ids` is non-unique. +/// 2. a NFT being staked has not previously been staked. +pub fn register_unstaked_nfts( + storage: &mut dyn Storage, + height: u64, + staker: &Addr, + token_ids: &[String], +) -> Result<(), ContractError> { + let subtractor = |amount: u128| { + move |prev: Option| -> StdResult { + prev.expect("unstaking that which was not staked") + .checked_sub(Uint128::new(amount)) + .map_err(StdError::overflow) + } + }; + + for token in token_ids { + let key = (staker, token.as_str()); + if STAKED_NFTS_PER_OWNER.has(storage, key) { + STAKED_NFTS_PER_OWNER.remove(storage, key); + } else { + return Err(ContractError::NotStaked { + token_id: token.clone(), + }); + } + } + + // invariant: token_ids has unique values. for loop asserts this. + + let sub_n = subtractor(token_ids.len() as u128); + TOTAL_STAKED_NFTS.update(storage, height, sub_n)?; + NFT_BALANCES.update(storage, staker, height, sub_n)?; + Ok(()) +} diff --git a/contracts/voting/dao-voting-onft-staked/src/testing/app.rs b/contracts/voting/dao-voting-onft-staked/src/testing/app.rs new file mode 100644 index 000000000..951d157a9 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/testing/app.rs @@ -0,0 +1,61 @@ +use std::ops::{Deref, DerefMut}; + +use crate::testing::omniflix_stargate::StargateKeeper; +use cosmwasm_std::{testing::MockApi, Empty, GovMsg, IbcMsg, IbcQuery, MemoryStorage}; +use cw_multi_test::{ + no_init, App, AppBuilder, BankKeeper, DistributionKeeper, FailingModule, StakeKeeper, + WasmKeeper, +}; +#[allow(clippy::type_complexity)] +pub struct OmniflixApp( + App< + BankKeeper, + MockApi, + MemoryStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + FailingModule, + FailingModule, + StargateKeeper, + >, +); +impl Deref for OmniflixApp { + type Target = App< + BankKeeper, + MockApi, + MemoryStorage, + FailingModule, + WasmKeeper, + StakeKeeper, + DistributionKeeper, + FailingModule, + FailingModule, + StargateKeeper, + >; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for OmniflixApp { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} +impl Default for OmniflixApp { + fn default() -> Self { + Self::new() + } +} + +impl OmniflixApp { + pub fn new() -> Self { + let app_builder = AppBuilder::default(); + let stargate = StargateKeeper {}; + let app = app_builder.with_stargate(stargate).build(no_init); + OmniflixApp(app) + } +} diff --git a/contracts/voting/dao-voting-onft-staked/src/testing/execute.rs b/contracts/voting/dao-voting-onft-staked/src/testing/execute.rs new file mode 100644 index 000000000..fd79fd25e --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/testing/execute.rs @@ -0,0 +1,236 @@ +use crate::msg::ExecuteMsg; +use anyhow::Result as AnyResult; +use cosmwasm_std::Addr; +use cw_multi_test::AppResponse; +use cw_multi_test::Executor; +use cw_utils::Duration; +use omniflix_std::types::omniflix::onft::v1beta1::{MsgCreateDenom, MsgMintOnft, MsgTransferOnft}; + +use super::app::OmniflixApp; +use super::DAO; + +// Shorthand for an unchecked address. +macro_rules! addr { + ($x:expr ) => { + Addr::unchecked($x) + }; +} + +pub fn create_onft_collection( + app: &mut OmniflixApp, + id: &str, + sender: &str, + minter: &str, +) -> String { + app.execute( + addr!(sender), + MsgCreateDenom { + id: id.to_string(), + symbol: "BAD".to_string(), + name: "Bad Kids".to_string(), + description: "bad kids".to_string(), + preview_uri: "".to_string(), + schema: "".to_string(), + sender: minter.to_string(), + creation_fee: None, + uri: "".to_string(), + uri_hash: "".to_string(), + data: "".to_string(), + royalty_receivers: vec![], + } + .into(), + ) + .unwrap(); + + id.to_string() +} + +pub fn mint_nft( + app: &mut OmniflixApp, + collection_id: &str, + receiver: &str, + token_id: &str, +) -> AnyResult { + app.execute( + addr!(DAO), + MsgMintOnft { + id: token_id.to_string(), + denom_id: collection_id.to_string(), + metadata: None, + data: "".to_string(), + transferable: true, + extensible: false, + nsfw: false, + royalty_share: "".to_string(), + sender: DAO.to_string(), + recipient: receiver.to_string(), + } + .into(), + ) +} + +pub fn send_nft( + app: &mut OmniflixApp, + collection_id: &str, + token_id: &str, + sender: &str, + recipient: &str, +) -> AnyResult { + app.execute( + addr!(sender), + MsgTransferOnft { + denom_id: collection_id.to_string(), + id: token_id.to_string(), + sender: sender.to_string(), + recipient: recipient.to_string(), + } + .into(), + ) +} + +pub fn prepare_stake_nft( + app: &mut OmniflixApp, + module: &Addr, + sender: &str, + token_id: &str, +) -> AnyResult { + app.execute_contract( + addr!(sender), + module.clone(), + &ExecuteMsg::PrepareStake { + token_ids: vec![token_id.to_string()], + }, + &[], + ) +} + +pub fn confirm_stake_nft( + app: &mut OmniflixApp, + module: &Addr, + sender: &str, + token_id: &str, +) -> AnyResult { + app.execute_contract( + addr!(sender), + module.clone(), + &ExecuteMsg::ConfirmStake { + token_ids: vec![token_id.to_string()], + }, + &[], + ) +} + +pub fn stake_nft( + app: &mut OmniflixApp, + collection_id: &str, + module: &Addr, + sender: &str, + token_id: &str, +) -> AnyResult<()> { + prepare_stake_nft(app, module, sender, token_id)?; + send_nft(app, collection_id, token_id, sender, module.as_str())?; + confirm_stake_nft(app, module, sender, token_id)?; + Ok(()) +} + +pub fn cancel_stake( + app: &mut OmniflixApp, + module: &Addr, + sender: &str, + token_id: &str, + recipient: Option<&str>, +) -> AnyResult { + app.execute_contract( + addr!(sender), + module.clone(), + &ExecuteMsg::CancelStake { + token_ids: vec![token_id.to_string()], + recipient: recipient.map(|s| s.to_string()), + }, + &[], + ) +} + +pub fn mint_and_stake_nft( + app: &mut OmniflixApp, + collection_id: &str, + module: &Addr, + staker: &str, + token_id: &str, +) -> AnyResult<()> { + mint_nft(app, collection_id, staker, token_id)?; + stake_nft(app, collection_id, module, staker, token_id)?; + + Ok(()) +} + +pub fn unstake_nfts( + app: &mut OmniflixApp, + module: &Addr, + sender: &str, + token_ids: &[&str], +) -> AnyResult { + app.execute_contract( + addr!(sender), + module.clone(), + &ExecuteMsg::Unstake { + token_ids: token_ids.iter().map(|s| s.to_string()).collect(), + }, + &[], + ) +} + +pub fn update_config( + app: &mut OmniflixApp, + module: &Addr, + sender: &str, + duration: Option, +) -> AnyResult { + app.execute_contract( + addr!(sender), + module.clone(), + &ExecuteMsg::UpdateConfig { duration }, + &[], + ) +} + +pub fn claim_nfts(app: &mut OmniflixApp, module: &Addr, sender: &str) -> AnyResult { + app.execute_contract( + addr!(sender), + module.clone(), + &ExecuteMsg::ClaimNfts {}, + &[], + ) +} + +pub fn add_hook( + app: &mut OmniflixApp, + module: &Addr, + sender: &str, + hook: &str, +) -> AnyResult { + app.execute_contract( + addr!(sender), + module.clone(), + &ExecuteMsg::AddHook { + addr: hook.to_string(), + }, + &[], + ) +} + +pub fn remove_hook( + app: &mut OmniflixApp, + module: &Addr, + sender: &str, + hook: &str, +) -> AnyResult { + app.execute_contract( + addr!(sender), + module.clone(), + &ExecuteMsg::RemoveHook { + addr: hook.to_string(), + }, + &[], + ) +} diff --git a/contracts/voting/dao-voting-onft-staked/src/testing/hooks.rs b/contracts/voting/dao-voting-onft-staked/src/testing/hooks.rs new file mode 100644 index 000000000..b32d35398 --- /dev/null +++ b/contracts/voting/dao-voting-onft-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 { + onft_collection_id: "ekez-token".to_string(), + 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-onft-staked/src/testing/mod.rs b/contracts/voting/dao-voting-onft-staked/src/testing/mod.rs new file mode 100644 index 000000000..487a4dde0 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/testing/mod.rs @@ -0,0 +1,73 @@ +mod app; +mod execute; +mod hooks; +mod omniflix_stargate; +mod queries; +mod tests; + +use app::OmniflixApp; +use cosmwasm_std::Addr; +use cw_multi_test::Executor; +use cw_utils::Duration; +use dao_testing::contracts::onft_staked_voting_contract; +use dao_voting::threshold::ActiveThreshold; + +use crate::msg::{InstantiateMsg, OnftCollection}; + +use self::execute::create_onft_collection; + +/// Address used as the instantiator. +pub(crate) const DAO: &str = "dao"; +/// Address used to stake. +pub(crate) const STAKER: &str = "staker"; + +pub(crate) struct CommonTest { + app: OmniflixApp, + module_id: u64, + module: Addr, + nft: String, +} + +pub(crate) fn setup_test( + unstaking_duration: Option, + active_threshold: Option, +) -> CommonTest { + let mut app = OmniflixApp::new(); + let module_id = app.store_code(onft_staked_voting_contract()); + + let nft = create_onft_collection(&mut app, "nft", DAO, DAO); + let module = app + .instantiate_contract( + module_id, + Addr::unchecked(DAO), + &InstantiateMsg { + onft_collection: OnftCollection::Existing { + id: nft.to_string(), + }, + unstaking_duration, + active_threshold, + }, + &[], + "onft_voting", + None, + ) + .unwrap(); + + CommonTest { + app, + module_id, + module, + nft, + } +} + +// Advantage to using a macro for this is that the error trace links +// to the exact line that the error occured, instead of inside of a +// function where the assertion would otherwise happen. +macro_rules! is_error { + ($x:expr => $e:tt) => { + assert!(format!("{:#}", $x.unwrap_err()).contains($e)) + }; +} + +pub(crate) use is_error; diff --git a/contracts/voting/dao-voting-onft-staked/src/testing/omniflix_stargate.rs b/contracts/voting/dao-voting-onft-staked/src/testing/omniflix_stargate.rs new file mode 100644 index 000000000..baa249998 --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/testing/omniflix_stargate.rs @@ -0,0 +1,144 @@ +use anyhow::Result; +use cosmwasm_std::{from_json, to_json_binary, Addr, Api, Binary, BlockInfo, Querier, Storage}; +use cw_multi_test::{error::AnyResult, AppResponse, CosmosRouter, Stargate}; +use omniflix_std::types::omniflix::onft::v1beta1::{ + Collection, Denom, MsgCreateDenom, MsgCreateDenomResponse, MsgMintOnft, MsgMintOnftResponse, + MsgTransferOnft, MsgTransferOnftResponse, QuerySupplyRequest, QuerySupplyResponse, +}; +use omniflix_std::types::omniflix::onft::v1beta1::{Onft, QueryOnftRequest, QueryOnftResponse}; +use prost::{DecodeError, Message}; + +const COLLECTION_PREFIX: &str = "collection"; + +pub struct StargateKeeper {} + +impl StargateKeeper {} + +impl Stargate for StargateKeeper { + fn execute( + &self, + _api: &dyn Api, + storage: &mut dyn Storage, + _router: &dyn CosmosRouter, + _block: &BlockInfo, + sender: Addr, + type_url: String, + value: Binary, + ) -> AnyResult { + if type_url == *"/OmniFlix.onft.v1beta1.MsgCreateDenom" { + let msg: MsgCreateDenom = Message::decode(value.as_slice()).unwrap(); + let collection = Collection { + denom: Some(Denom { + creator: sender.to_string(), + data: msg.data, + name: msg.name, + id: msg.id.clone(), + preview_uri: msg.preview_uri, + description: msg.description, + schema: msg.schema, + symbol: msg.symbol, + uri: msg.uri, + uri_hash: msg.uri_hash, + royalty_receivers: msg.royalty_receivers, + }), + onfts: vec![], + }; + let key = format!("collections:{}:{}", COLLECTION_PREFIX, msg.id); + let serialized_collection = + to_json_binary(&collection).expect("Failed to serialize Collection"); + storage.set(key.as_bytes(), &serialized_collection); + + return Ok(AppResponse { + events: vec![], + data: Some(Binary::from(MsgCreateDenomResponse {})), + }); + } + if type_url == *"/OmniFlix.onft.v1beta1.MsgMintONFT" { + let msg: MsgMintOnft = Message::decode(value.as_slice()).unwrap(); + let key = format!("collections:{}:{}", COLLECTION_PREFIX, msg.denom_id.clone()); + let serialized_collection = storage.get(key.as_bytes()); + let mut collection: Collection = from_json(serialized_collection.unwrap()) + .expect("Failed to deserialize Collection"); + let onft = Onft { + id: msg.id, + created_at: None, + nsfw: msg.nsfw, + owner: msg.recipient, + data: msg.data, + transferable: msg.transferable, + extensible: msg.extensible, + metadata: msg.metadata, + royalty_share: msg.royalty_share, + }; + collection.onfts.push(onft); + let serialized_collection = + to_json_binary(&collection).expect("Failed to serialize Collection"); + storage.set(key.as_bytes(), &serialized_collection); + + return Ok(AppResponse { + events: vec![], + data: Some(Binary::from(MsgMintOnftResponse {})), + }); + } + if type_url == *"/OmniFlix.onft.v1beta1.MsgTransferONFT" { + let parsed_msg: Result = + Message::decode(value.as_slice()); + if let Ok(msg) = parsed_msg { + let key = format!("collections:{}:{}", COLLECTION_PREFIX, msg.denom_id.clone()); + let serialized_collection = storage.get(key.as_bytes()); + let mut collection: Collection = from_json(serialized_collection.unwrap()) + .expect("Failed to deserialize Collection"); + let onft = collection.onfts.iter_mut().find(|onft| onft.id == msg.id); + let onft = onft.unwrap(); + onft.owner = msg.recipient; + let serialized_collection = + to_json_binary(&collection).expect("Failed to serialize Collection"); + storage.set(key.as_bytes(), &serialized_collection); + + return Ok(AppResponse { + events: vec![], + data: Some(Binary::from(MsgTransferOnftResponse {})), + }); + }; + } + Ok(AppResponse::default()) + } + + fn query( + &self, + _api: &dyn Api, + storage: &dyn Storage, + _querier: &dyn Querier, + _block: &BlockInfo, + path: String, + data: Binary, + ) -> AnyResult { + if path == *"/OmniFlix.onft.v1beta1.Query/ONFT" { + let request: QueryOnftRequest = Message::decode(data.as_slice()).unwrap(); + + let key = format!("collections:{}:{}", COLLECTION_PREFIX, request.denom_id); + let serialized_collection = storage.get(key.as_bytes()); + let collection: Collection = from_json(serialized_collection.unwrap()) + .expect("Failed to deserialize Collection"); + let onft = collection + .onfts + .into_iter() + .find(|onft| onft.id == request.id); + + return Ok(to_json_binary(&QueryOnftResponse { onft })?); + } + if path == *"/OmniFlix.onft.v1beta1.Query/Supply" { + let request: QuerySupplyRequest = Message::decode(data.as_slice()).unwrap(); + + let key = format!("collections:{}:{}", COLLECTION_PREFIX, request.denom_id); + let serialized_collection = storage.get(key.as_bytes()); + let collection: Collection = from_json(serialized_collection.unwrap()) + .expect("Failed to deserialize Collection"); + + return Ok(to_json_binary(&QuerySupplyResponse { + amount: collection.onfts.len() as u64, + })?); + } + Ok(data) + } +} diff --git a/contracts/voting/dao-voting-onft-staked/src/testing/queries.rs b/contracts/voting/dao-voting-onft-staked/src/testing/queries.rs new file mode 100644 index 000000000..3ff1facde --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/testing/queries.rs @@ -0,0 +1,116 @@ +use cosmwasm_std::{Addr, StdResult, Uint128}; +use cw721_controllers::NftClaimsResponse; +use cw_controllers::HooksResponse; +use dao_interface::voting::{ + InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; +use omniflix_std::types::omniflix::onft::v1beta1::{QueryOnftRequest, QueryOnftResponse}; + +use crate::{msg::QueryMsg, state::Config}; + +use super::app::OmniflixApp; + +pub fn query_config(app: &OmniflixApp, module: &Addr) -> StdResult { + let config = app.wrap().query_wasm_smart(module, &QueryMsg::Config {})?; + Ok(config) +} + +pub fn query_claims(app: &OmniflixApp, module: &Addr, addr: &str) -> StdResult { + let claims = app.wrap().query_wasm_smart( + module, + &QueryMsg::NftClaims { + address: addr.to_string(), + }, + )?; + Ok(claims) +} + +pub fn query_hooks(app: &OmniflixApp, module: &Addr) -> StdResult { + let hooks = app.wrap().query_wasm_smart(module, &QueryMsg::Hooks {})?; + Ok(hooks) +} + +pub fn query_staked_nfts( + app: &OmniflixApp, + module: &Addr, + addr: &str, + start_after: Option, + limit: Option, +) -> StdResult> { + let nfts = app.wrap().query_wasm_smart( + module, + &QueryMsg::StakedNfts { + address: addr.to_string(), + start_after, + limit, + }, + )?; + Ok(nfts) +} + +pub fn query_voting_power( + app: &OmniflixApp, + 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: &OmniflixApp, + module: &Addr, + height: Option, +) -> StdResult { + let power = app + .wrap() + .query_wasm_smart(module, &QueryMsg::TotalPowerAtHeight { height })?; + Ok(power) +} + +pub fn query_dao(app: &OmniflixApp, module: &Addr) -> StdResult { + let dao = app.wrap().query_wasm_smart(module, &QueryMsg::Dao {})?; + Ok(dao) +} + +pub fn query_info(app: &OmniflixApp, module: &Addr) -> StdResult { + let info = app.wrap().query_wasm_smart(module, &QueryMsg::Info {})?; + Ok(info) +} + +pub fn query_total_and_voting_power( + app: &OmniflixApp, + module: &Addr, + addr: &str, + height: Option, +) -> StdResult<(Uint128, Uint128)> { + let total_power = query_total_power(app, module, height)?; + let voting_power = query_voting_power(app, module, addr, height)?; + + Ok((total_power.power, voting_power.power)) +} + +pub fn query_nft_owner( + app: &OmniflixApp, + collection_id: &str, + token_id: &str, +) -> StdResult { + let response: QueryOnftResponse = app + .wrap() + .query( + &QueryOnftRequest { + denom_id: collection_id.to_string(), + id: token_id.to_string(), + } + .into(), + ) + .unwrap(); + Ok(response.onft.unwrap().owner) +} diff --git a/contracts/voting/dao-voting-onft-staked/src/testing/tests.rs b/contracts/voting/dao-voting-onft-staked/src/testing/tests.rs new file mode 100644 index 000000000..4e5e5e0bb --- /dev/null +++ b/contracts/voting/dao-voting-onft-staked/src/testing/tests.rs @@ -0,0 +1,1207 @@ +use cosmwasm_std::testing::{mock_dependencies, mock_env}; +use cosmwasm_std::{Addr, Decimal, Uint128}; +use cw721_controllers::{NftClaim, NftClaimsResponse}; +use cw_multi_test::{next_block, Executor}; +use cw_utils::Duration; +use dao_interface::voting::IsActiveResponse; +use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; + +use crate::msg::OnftCollection; +use crate::testing::execute::{cancel_stake, confirm_stake_nft, prepare_stake_nft, send_nft}; +use crate::testing::queries::query_dao; +use crate::testing::DAO; +use crate::{ + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + state::MAX_CLAIMS, + testing::{ + execute::{ + claim_nfts, mint_and_stake_nft, mint_nft, stake_nft, unstake_nfts, update_config, + }, + queries::{query_config, query_hooks, query_nft_owner, query_total_and_voting_power}, + }, +}; + +use super::{ + execute::{add_hook, remove_hook}, + is_error, + queries::{query_claims, query_info, query_staked_nfts, query_total_power, query_voting_power}, + setup_test, CommonTest, STAKER, +}; + +// I can stake tokens, voting power and total power is updated one +// block later. +#[test] +fn test_stake_tokens() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + let total_power = query_total_power(&app, &module, None)?; + let voting_power = query_voting_power(&app, &module, STAKER, None)?; + + assert_eq!(total_power.power, Uint128::zero()); + assert_eq!(total_power.height, app.block_info().height); + + assert_eq!(voting_power.power, Uint128::zero()); + assert_eq!(voting_power.height, app.block_info().height); + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1")?; + + // Voting powers are not updated until a block has passed. + let (total, personal) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert!(total.is_zero()); + assert!(personal.is_zero()); + + app.update_block(next_block); + + let (total, personal) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(1)); + assert_eq!(personal, Uint128::new(1)); + + // Registering duplicate token IDs does not provide more voting power. + mint_nft(&mut app, &nft, STAKER, "2")?; + prepare_stake_nft(&mut app, &module, STAKER, "2")?; + send_nft(&mut app, &nft, "2", STAKER, module.as_str())?; + app.execute_contract( + Addr::unchecked(STAKER), + module.clone(), + &ExecuteMsg::ConfirmStake { + token_ids: vec!["2".to_string(), "2".to_string(), "2".to_string()], + }, + &[], + )?; + + app.update_block(next_block); + + let (total, personal) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(2)); + assert_eq!(personal, Uint128::new(2)); + + Ok(()) +} + +// I can unstake tokens. Unstaking more than one token at once +// works. I can not unstake a token more than once. I can not unstake +// another addresses' token. Voting power and total power is updated +// when I unstake. +#[test] +fn test_unstake_tokens_no_claims() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + let friend = "friend"; + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "2")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "3")?; + + mint_nft(&mut app, &nft, friend, "4")?; + mint_nft(&mut app, &nft, friend, "5")?; + stake_nft(&mut app, &nft, &module, friend, "4")?; + stake_nft(&mut app, &nft, &module, friend, "5")?; + + app.update_block(next_block); + + let (total, personal) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(5)); + assert_eq!(personal, Uint128::new(3)); + + unstake_nfts(&mut app, &module, STAKER, &["1", "2"])?; + + // Voting power is updated when I unstake. Waits a block as it's a + // snapshot map. + let (total, personal) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(5)); + assert_eq!(personal, Uint128::new(3)); + app.update_block(next_block); + let (total, personal) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(3)); + assert_eq!(personal, Uint128::new(1)); + + // I can not unstake tokens I do not own. Anyhow can't figure out + // how to downcast this error so we check for the expected string. + let res = unstake_nfts(&mut app, &module, STAKER, &["4"]); + is_error!(res => "Can not unstake that which you have not staked (unstaking 4)"); + + let res = unstake_nfts(&mut app, &module, STAKER, &["5", "4"]); + is_error!(res => "Can not unstake that which you have not staked (unstaking 5)"); + + let res = unstake_nfts(&mut app, &module, STAKER, &["☯️", "4"]); + is_error!(res => "Can not unstake that which you have not staked (unstaking ☯️)"); + + // I can not unstake tokens more than once. + let res = unstake_nfts(&mut app, &module, STAKER, &["1"]); + is_error!(res => "Can not unstake that which you have not staked (unstaking 1)"); + + Ok(()) +} + +// I cannot unstake zero tokens. +#[test] +fn test_unstake_zero_tokens() -> anyhow::Result<()> { + let CommonTest { + mut app, module, .. + } = setup_test(None, None); + + let res = unstake_nfts(&mut app, &module, STAKER, &[]); + is_error!(res => "Can't unstake zero NFTs."); + + Ok(()) +} + +// I can update the unstaking duration and the owner. Only the owner +// may do this. I can unset the owner. Updating the unstaking duration +// does not impact outstanding claims. +#[test] +fn test_update_config() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(Some(Duration::Height(3)), None); + + // non-DAO cannot update config + let res = update_config(&mut app, &module, STAKER, Some(Duration::Time(1))); + is_error!(res => "Unauthorized"); + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "2")?; + + unstake_nfts(&mut app, &module, STAKER, &["1"])?; + + let claims = query_claims(&app, &module, STAKER)?; + assert_eq!( + claims, + NftClaimsResponse { + nft_claims: vec![NftClaim { + token_id: "1".to_string(), + release_at: cw_utils::Expiration::AtHeight(app.block_info().height + 3) + }] + } + ); + + // Update config to invalid duration fails + let err = update_config(&mut app, &module, DAO, 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, DAO, Some(Duration::Time(1)))?; + + // Existing claims should remain unchanged. + let claims = query_claims(&app, &module, STAKER)?; + assert_eq!( + claims, + NftClaimsResponse { + nft_claims: vec![NftClaim { + token_id: "1".to_string(), + release_at: cw_utils::Expiration::AtHeight(app.block_info().height + 3) + }] + } + ); + + // New claims should reflect the new unstaking duration. Old ones + // should not. + unstake_nfts(&mut app, &module, STAKER, &["2"])?; + let claims = query_claims(&app, &module, STAKER)?; + assert_eq!( + claims, + NftClaimsResponse { + nft_claims: vec![ + NftClaim { + token_id: "1".to_string(), + release_at: cw_utils::Expiration::AtHeight(app.block_info().height + 3) + }, + NftClaim { + token_id: "2".to_string(), + release_at: Duration::Time(1).after(&app.block_info()) + } + ] + } + ); + + let info = app.block_info(); + app.update_block(|block| { + block.height += 3; + block.time = match Duration::Time(1).after(&info) { + cw_utils::Expiration::AtTime(timestamp) => timestamp, + _ => panic!("there should really be an easier way to do this"), + } + }); + + // Do a claim for good measure. + claim_nfts(&mut app, &module, STAKER)?; + let claims = query_claims(&app, &module, STAKER)?; + assert_eq!(claims, NftClaimsResponse { nft_claims: vec![] }); + + Ok(()) +} + +// I can query my pending claims. Attempting to claim with nothing to +// claim results in an error. Attempting to claim with tokens to claim +// results in me owning those tokens. +#[test] +fn test_claims() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(Some(Duration::Height(1)), None); + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "2")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "3")?; + + let claims = query_claims(&app, &module, STAKER)?; + assert_eq!(claims.nft_claims, vec![]); + + let res = claim_nfts(&mut app, &module, STAKER); + is_error!(res => "Nothing to claim"); + + unstake_nfts(&mut app, &module, STAKER, &["2"])?; + + let claims = query_claims(&app, &module, STAKER)?; + assert_eq!( + claims.nft_claims, + vec![NftClaim { + token_id: "2".to_string(), + release_at: cw_utils::Expiration::AtHeight(app.block_info().height + 1) + }] + ); + + // Claim now exists, but is not yet expired. Nothing to claim. + let res = claim_nfts(&mut app, &module, STAKER); + is_error!(res => "Nothing to claim"); + + app.update_block(next_block); + claim_nfts(&mut app, &module, STAKER)?; + + let owner = query_nft_owner(&app, &nft, "2")?; + assert_eq!(owner, STAKER.to_string()); + + Ok(()) +} + +// I can not have more than MAX_CLAIMS claims pending. +#[test] +fn test_max_claims() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(Some(Duration::Height(1)), None); + + for i in 0..MAX_CLAIMS { + let i_str = &i.to_string(); + mint_and_stake_nft(&mut app, &nft, &module, STAKER, i_str)?; + unstake_nfts(&mut app, &module, STAKER, &[i_str])?; + } + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "a")?; + let res = unstake_nfts(&mut app, &module, STAKER, &["a"]); + is_error!(res => "Too many outstanding claims. Claim some tokens before unstaking more."); + + Ok(()) +} + +// I can list all of the currently staked NFTs for an address. +#[test] +fn test_list_staked_nfts() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(Some(Duration::Height(1)), None); + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "2")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "3")?; + + let deardrie = "deardrie"; + mint_nft(&mut app, &nft, deardrie, "4")?; + mint_nft(&mut app, &nft, deardrie, "5")?; + + let nfts = query_staked_nfts(&app, &module, deardrie, None, None)?; + assert!(nfts.is_empty()); + + stake_nft(&mut app, &nft, &module, deardrie, "4")?; + stake_nft(&mut app, &nft, &module, deardrie, "5")?; + + let nfts = query_staked_nfts(&app, &module, deardrie, None, None)?; + assert_eq!(nfts, vec!["4".to_string(), "5".to_string()]); + + let nfts = query_staked_nfts(&app, &module, STAKER, Some("1".to_string()), Some(0))?; + assert!(nfts.is_empty()); + + let nfts = query_staked_nfts(&app, &module, STAKER, Some("3".to_string()), None)?; + assert!(nfts.is_empty()); + let nfts = query_staked_nfts(&app, &module, STAKER, Some("3".to_string()), Some(500))?; + assert!(nfts.is_empty()); + + let nfts = query_staked_nfts(&app, &module, STAKER, Some("1".to_string()), Some(2))?; + assert_eq!(nfts, vec!["2".to_string(), "3".to_string()]); + + unstake_nfts(&mut app, &module, STAKER, &["2"])?; + let nfts = query_staked_nfts(&app, &module, STAKER, Some("1".to_string()), Some(2))?; + assert_eq!(nfts, vec!["3".to_string()]); + + Ok(()) +} + +#[test] +fn test_info_query_works() -> anyhow::Result<()> { + let CommonTest { app, module, .. } = setup_test(None, None); + let info = query_info(&app, &module)?; + assert_eq!(info.info.version, env!("CARGO_PKG_VERSION").to_string()); + Ok(()) +} + +#[test] +fn test_dao_query_works() -> anyhow::Result<()> { + let CommonTest { app, module, .. } = setup_test(None, None); + let dao = query_dao(&app, &module)?; + assert_eq!(dao, DAO.to_string()); + Ok(()) +} + +// The owner may add and remove hooks. +#[test] +fn test_add_remove_hooks() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + add_hook(&mut app, &module, DAO, "meow")?; + remove_hook(&mut app, &module, DAO, "meow")?; + + // Minting NFT works if no hooks + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1").unwrap(); + + // Add a hook to a fake contract called "meow" + add_hook(&mut app, &module, DAO, "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, STAKER, "1").unwrap_err(); + + let res = add_hook(&mut app, &module, DAO, "meow"); + is_error!(res => "Given address already registered as a hook"); + + let res = remove_hook(&mut app, &module, DAO, "blue"); + is_error!(res => "Given address not registered as a hook"); + + let res = add_hook(&mut app, &module, "ekez", "evil"); + is_error!(res => "Unauthorized"); + let res = remove_hook(&mut app, &module, "ekez", "evil"); + is_error!(res => "Unauthorized"); + + Ok(()) +} + +#[test] +#[should_panic(expected = "Active threshold count must be greater than zero")] +fn test_instantiate_zero_active_threshold_count() { + setup_test( + None, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::zero(), + }), + ); +} + +#[test] +#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] +fn test_instantiate_invalid_active_threshold_count() { + setup_test( + None, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + ); +} + +#[test] +fn test_active_threshold_absolute_count() { + let CommonTest { + mut app, + module_id, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1").unwrap(); + mint_nft(&mut app, &nft, STAKER, "2").unwrap(); + mint_nft(&mut app, &nft, STAKER, "3").unwrap(); + + let module = app + .instantiate_contract( + module_id, + Addr::unchecked(DAO), + &InstantiateMsg { + onft_collection: OnftCollection::Existing { + id: nft.to_string(), + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(3), + }), + }, + &[], + "onft_voting", + None, + ) + .unwrap(); + + // Get collection ID + let onft_collection_id = query_config(&app, &module).unwrap().onft_collection_id; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(module.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake NFTs + stake_nft(&mut app, &onft_collection_id, &module, STAKER, "1").unwrap(); + stake_nft(&mut app, &onft_collection_id, &module, STAKER, "2").unwrap(); + stake_nft(&mut app, &onft_collection_id, &module, STAKER, "3").unwrap(); + + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(module, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent() { + let CommonTest { + mut app, + module_id, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1").unwrap(); + + let module = app + .instantiate_contract( + module_id, + Addr::unchecked(DAO), + &InstantiateMsg { + onft_collection: OnftCollection::Existing { + id: nft.to_string(), + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(20), + }), + }, + &[], + "onft_voting", + None, + ) + .unwrap(); + + // Get collection ID + let onft_collection_id = query_config(&app, &module).unwrap().onft_collection_id; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(module.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake NFTs + stake_nft(&mut app, &onft_collection_id, &module, STAKER, "1").unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(module, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent_rounds_up() { + let CommonTest { + mut app, + module_id, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1").unwrap(); + mint_nft(&mut app, &nft, STAKER, "2").unwrap(); + mint_nft(&mut app, &nft, STAKER, "3").unwrap(); + mint_nft(&mut app, &nft, STAKER, "4").unwrap(); + mint_nft(&mut app, &nft, STAKER, "5").unwrap(); + + let module = app + .instantiate_contract( + module_id, + Addr::unchecked(DAO), + &InstantiateMsg { + onft_collection: OnftCollection::Existing { + id: nft.to_string(), + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50), + }), + }, + &[], + "onft_voting", + None, + ) + .unwrap(); + + // Get collection ID + let onft_collection_id = query_config(&app, &module).unwrap().onft_collection_id; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(module.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 2 token as creator, should not be active. + stake_nft(&mut app, &onft_collection_id, &module, STAKER, "1").unwrap(); + stake_nft(&mut app, &onft_collection_id, &module, STAKER, "2").unwrap(); + + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(module.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 1 more token as creator, should now be active. + stake_nft(&mut app, &onft_collection_id, &module, STAKER, "3").unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(module, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_update_active_threshold() { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1").unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(module.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"), module.clone(), &msg, &[]) + .unwrap_err(); + + // Expect success as sender is the DAO + app.execute_contract(Addr::unchecked(DAO), module.clone(), &msg, &[]) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(module.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!( + resp.active_threshold, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(1) + }) + ); + + app.execute_contract( + Addr::unchecked(DAO), + module.clone(), + &ExecuteMsg::UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50), + }), + }, + &[], + ) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(module.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!( + resp.active_threshold, + Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50) + }) + ); + + // remove + app.execute_contract( + Addr::unchecked(DAO), + module.clone(), + &ExecuteMsg::UpdateActiveThreshold { + new_threshold: None, + }, + &[], + ) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(module.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!(resp.active_threshold, None); + + // verify is active + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(module, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +#[should_panic( + expected = "Active threshold percentage must be greater than 0 and not greater than 1" +)] +fn test_active_threshold_percentage_gt_100() { + setup_test( + None, + 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() { + setup_test( + None, + Some(ActiveThreshold::Percentage { + percent: Decimal::percent(0), + }), + ); +} + +#[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); + + // migrate again, should do nothing + 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); +} + +/// Staking tokens has a one block delay before staked tokens are +/// reflected in voting power. Unstaking tokens has a one block delay +/// before the unstaking is reflected in voting power, yet you have +/// access to the NFT. If I immediately stake an unstaked NFT, my +/// voting power should not change. +#[test] +fn test_circular_stake() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "2")?; + + app.update_block(next_block); + + let (total, voting) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(2)); + assert_eq!(voting, Uint128::new(2)); + + unstake_nfts(&mut app, &module, STAKER, &["1", "2"])?; + + // Unchanged, one block delay. + let (total, voting) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(2)); + assert_eq!(voting, Uint128::new(2)); + + stake_nft(&mut app, &nft, &module, STAKER, "1")?; + stake_nft(&mut app, &nft, &module, STAKER, "2")?; + + // Unchanged. + let (total, voting) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(2)); + assert_eq!(voting, Uint128::new(2)); + + app.update_block(next_block); + + // Still unchanged. + let (total, voting) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::new(2)); + assert_eq!(voting, Uint128::new(2)); + + Ok(()) +} + +/// I can immediately unstake after staking even though voting powers +/// aren't updated until one block later. Voting power does not change +/// if I do this. +#[test] +fn test_immediate_unstake() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1")?; + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "2")?; + + unstake_nfts(&mut app, &module, STAKER, &["1", "2"])?; + + app.update_block(next_block); + + let (total, voting) = query_total_and_voting_power(&app, &module, STAKER, None)?; + assert_eq!(total, Uint128::zero()); + assert_eq!(voting, Uint128::zero()); + + Ok(()) +} + +/// I cannot prepare/stake an NFT I do not own. +#[test] +fn test_stake_unowned() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + let res = stake_nft(&mut app, &nft, &module, "other", "1"); + is_error!(res => "Only an NFT's owner can prepare it to be staked"); + + app.update_block(next_block); + let voting = query_voting_power(&app, &module, STAKER, None)?; + assert_eq!(voting.power, Uint128::new(0)); + + Ok(()) +} + +/// I cannot confirm a stake before preparing it. +#[test] +fn test_stake_unprepared() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // attempt confirm without preparing + let res = confirm_stake_nft(&mut app, &module, STAKER, "1"); + is_error!(res => "NFTs must be prepared and transferred before they can be staked"); + + app.update_block(next_block); + let voting = query_voting_power(&app, &module, STAKER, None)?; + assert_eq!(voting.power, Uint128::new(0)); + + Ok(()) +} + +/// I cannot confirm a stake before preparing it and transferring NFT. +#[test] +fn test_stake_prepared_untransferred() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // prepare but don't transfer + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + + // attempt confirm + let res = confirm_stake_nft(&mut app, &module, STAKER, "1"); + is_error!(res => "NFTs must be prepared and transferred before they can be staked"); + + app.update_block(next_block); + let voting = query_voting_power(&app, &module, STAKER, None)?; + assert_eq!(voting.power, Uint128::new(0)); + + Ok(()) +} + +/// I cannot confirm a stake that someone else prepared. +#[test] +fn test_stake_prepared_confirm_other_owner() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // prepare + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + + // transfer to voting contract + send_nft(&mut app, &nft, "1", STAKER, module.as_str())?; + + // attempt confirm + let res = confirm_stake_nft(&mut app, &module, "other", "1"); + is_error!(res => "NFTs must be prepared and transferred before they can be staked"); + + app.update_block(next_block); + let voting = query_voting_power(&app, &module, "other", None)?; + assert_eq!(voting.power, Uint128::new(0)); + + Ok(()) +} + +/// I can override a prepared stake. +#[test] +fn test_override_prepared() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // prepare + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + + // transfer to someone else + send_nft(&mut app, &nft, "1", STAKER, "other")?; + + // override previous owner's prepare + prepare_stake_nft(&mut app, &module, "other", "1")?; + + // transfer to voting contract + send_nft(&mut app, &nft, "1", "other", module.as_str())?; + + // confirm + confirm_stake_nft(&mut app, &module, "other", "1")?; + + app.update_block(next_block); + let voting = query_voting_power(&app, &module, "other", None)?; + assert_eq!(voting.power, Uint128::new(1)); + + Ok(()) +} + +/// I can determine what my voting power _will_ be after staking by +/// asking for my voting power one block in the future. +#[test] +fn test_query_the_future() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_and_stake_nft(&mut app, &nft, &module, STAKER, "1")?; + + // Future voting power will be one under current conditions. + let voting = query_voting_power(&app, &module, STAKER, Some(app.block_info().height + 100))?; + assert_eq!(voting.power, Uint128::new(1)); + + // Current voting power is zero. + let voting = query_voting_power(&app, &module, STAKER, None)?; + assert_eq!(voting.power, Uint128::new(0)); + + unstake_nfts(&mut app, &module, STAKER, &["1"])?; + + // Future voting power is now zero. + let voting = query_voting_power(&app, &module, STAKER, Some(app.block_info().height + 100))?; + assert_eq!(voting.power, Uint128::zero()); + + Ok(()) +} + +/// I can not unstake more than one NFT in a TX in order to bypass the +/// MAX_CLAIMS limit. +#[test] +fn test_bypass_max_claims() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(Some(Duration::Height(1)), None); + let mut to_stake = vec![]; + for i in 1..(MAX_CLAIMS + 10) { + let i_str = &i.to_string(); + mint_and_stake_nft(&mut app, &nft, &module, STAKER, i_str)?; + if i < MAX_CLAIMS { + // unstake MAX_CLAMS - 1 NFTs + unstake_nfts(&mut app, &module, STAKER, &[i_str])?; + } else { + // push rest of NFT ids to vec + to_stake.push(i_str.clone()); + } + } + let binding = to_stake.iter().map(|s| s.as_str()).collect::>(); + let to_stake_slice: &[&str] = binding.as_slice(); + let res = unstake_nfts(&mut app, &module, STAKER, to_stake_slice); + is_error!(res => "Too many outstanding claims. Claim some tokens before unstaking more."); + Ok(()) +} + +/// I can cancel my own prepared stake. +#[test] +fn test_preparer_cancel_prepared_stake() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // prepare but don't transfer + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + + // cancel + cancel_stake(&mut app, &module, STAKER, "1", None)?; + + // prepare and transfer + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + send_nft(&mut app, &nft, "1", STAKER, module.as_str())?; + + // voting contract has the NFT + let owner = query_nft_owner(&app, &nft, "1")?; + assert_eq!(owner, module.to_string()); + + // cancel + cancel_stake(&mut app, &module, STAKER, "1", None)?; + + // original preparer has the NFT + let owner = query_nft_owner(&app, &nft, "1")?; + assert_eq!(owner, STAKER.to_string()); + + // no voting power + app.update_block(next_block); + let voting = query_voting_power(&app, &module, STAKER, None)?; + assert_eq!(voting.power, Uint128::new(0)); + + Ok(()) +} + +/// I cannot cancel someone else's prepared stake, unless I own it. +#[test] +fn test_no_cancel_other_prepared_stake() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // prepare + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + + // cancel from other + let res = cancel_stake(&mut app, &module, "other", "1", None); + is_error!(res => "Only the owner or preparer can cancel a prepared stake"); + + // transfer to other + send_nft(&mut app, &nft, "1", STAKER, "other")?; + // cancel from other + cancel_stake(&mut app, &module, "other", "1", None)?; + + Ok(()) +} + +/// The DAO can cancel a prepared stake. +#[test] +fn test_dao_cancel_stake() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // prepare + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + + // cancel from DAO + cancel_stake(&mut app, &module, DAO, "1", None)?; + + Ok(()) +} + +/// The DAO can cancel a prepared stake and send back to the preparer. +#[test] +fn test_dao_cancel_stake_and_return_to_preparer() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // prepare and transfer + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + // transfer + send_nft(&mut app, &nft, "1", STAKER, module.as_str())?; + + // voting contract has the NFT + let owner = query_nft_owner(&app, &nft, "1")?; + assert_eq!(owner, module.to_string()); + + // cancel from DAO + cancel_stake(&mut app, &module, DAO, "1", None)?; + + // preparer has the NFT + let owner = query_nft_owner(&app, &nft, "1")?; + assert_eq!(owner, STAKER); + + Ok(()) +} + +/// The DAO can cancel a prepared stake and send to anyone. +#[test] +fn test_dao_cancel_stake_and_send_to_anyone() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // prepare and transfer + prepare_stake_nft(&mut app, &module, STAKER, "1")?; + // transfer + send_nft(&mut app, &nft, "1", STAKER, module.as_str())?; + + // voting contract has the NFT + let owner = query_nft_owner(&app, &nft, "1")?; + assert_eq!(owner, module.to_string()); + + // cancel from DAO and send to other + cancel_stake(&mut app, &module, DAO, "1", Some("other"))?; + + // other has the NFT + let owner = query_nft_owner(&app, &nft, "1")?; + assert_eq!(owner, "other"); + + // other can stake + stake_nft(&mut app, &nft, &module, "other", "1")?; + + Ok(()) +} + +/// The DAO must specify a recipient if no one prepared the NFT. +#[test] +fn test_dao_cancel_stake_must_have_recipient() -> anyhow::Result<()> { + let CommonTest { + mut app, + module, + nft, + .. + } = setup_test(None, None); + + mint_nft(&mut app, &nft, STAKER, "1")?; + + // transfer without preparing + send_nft(&mut app, &nft, "1", STAKER, module.as_str())?; + + // voting contract has the NFT + let owner = query_nft_owner(&app, &nft, "1")?; + assert_eq!(owner, module.to_string()); + + // cancel from DAO without prepared stake requires recipient + let res = cancel_stake(&mut app, &module, DAO, "1", None); + is_error!(res => "Recipient must be set when the DAO is cancelling a stake that was not prepared"); + + // cancel from DAO and send back to staker + cancel_stake(&mut app, &module, DAO, "1", Some(STAKER))?; + + // staker has the NFT + let owner = query_nft_owner(&app, &nft, "1")?; + assert_eq!(owner, STAKER); + + // staker can stake + stake_nft(&mut app, &nft, &module, STAKER, "1")?; + + Ok(()) +} diff --git a/contracts/voting/dao-voting-token-staked/Cargo.toml b/contracts/voting/dao-voting-token-staked/Cargo.toml index 8be6ca1f6..7f3c9d57f 100644 --- a/contracts/voting/dao-voting-token-staked/Cargo.toml +++ b/contracts/voting/dao-voting-token-staked/Cargo.toml @@ -32,7 +32,7 @@ cosmwasm_tokenfactory = ["cw-tokenfactory-issuer/cosmwasm_tokenfactory"] kujira_tokenfactory = ["cw-tokenfactory-issuer/kujira_tokenfactory"] [dependencies] -cosmwasm-std = { workspace = true, features = ["cosmwasm_1_1"] } +cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cw-ownable = { workspace = true } cw-storage-plus = { workspace = true } @@ -48,6 +48,7 @@ dao-voting = { workspace = true } cw-tokenfactory-issuer = { workspace = true, default-features = false, features = [ "library", ] } +cw-orch.workspace = true [dev-dependencies] anyhow = { workspace = true } diff --git a/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json b/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json index 43c934c01..8f6c9aaba 100644 --- a/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json +++ b/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json @@ -1,6 +1,6 @@ { "contract_name": "dao-voting-token-staked", - "contract_version": "2.4.2", + "contract_version": "2.5.0", "idl_version": "1.0.0", "instantiate": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-token-staked/src/contract.rs b/contracts/voting/dao-voting-token-staked/src/contract.rs index 058210f44..33b85db11 100644 --- a/contracts/voting/dao-voting-token-staked/src/contract.rs +++ b/contracts/voting/dao-voting-token-staked/src/contract.rs @@ -746,6 +746,32 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result Uploadable for DaoDaoCore { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_dao_core") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_migrate(migrate) + .with_reply(reply), + ) + } +} diff --git a/packages/cw-orch/src/distribution/cw_fund_distributor.rs b/packages/cw-orch/src/distribution/cw_fund_distributor.rs new file mode 100644 index 000000000..4ff63de15 --- /dev/null +++ b/packages/cw-orch/src/distribution/cw_fund_distributor.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw_fund_distributor::contract::{execute, instantiate, query}; +use cw_fund_distributor::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoFundsDistributor; + +impl Uploadable for DaoFundsDistributor { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw_fund_distributor") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/distribution/dao_rewards_distributor.rs b/packages/cw-orch/src/distribution/dao_rewards_distributor.rs new file mode 100644 index 000000000..1ccbb3bb8 --- /dev/null +++ b/packages/cw-orch/src/distribution/dao_rewards_distributor.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_rewards_distributor::contract::{execute, instantiate, query}; +use dao_rewards_distributor::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoRewardsDistributor; + +impl Uploadable for DaoRewardsDistributor { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_rewards_distributor") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/distribution/mod.rs b/packages/cw-orch/src/distribution/mod.rs new file mode 100644 index 000000000..fb3e0fb50 --- /dev/null +++ b/packages/cw-orch/src/distribution/mod.rs @@ -0,0 +1,5 @@ +mod cw_fund_distributor; +mod dao_rewards_distributor; + +pub use cw_fund_distributor::DaoFundsDistributor; +pub use dao_rewards_distributor::DaoRewardsDistributor; diff --git a/packages/cw-orch/src/external/admin_factory.rs b/packages/cw-orch/src/external/admin_factory.rs new file mode 100644 index 000000000..2fa5f726b --- /dev/null +++ b/packages/cw-orch/src/external/admin_factory.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw_admin_factory::contract::{execute, instantiate, query, reply}; +use cw_admin_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoExternalAdminFactory; + +impl Uploadable for DaoExternalAdminFactory { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw_admin_factory") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply)) + } +} diff --git a/packages/cw-orch/src/external/btsg_ft_factory.rs b/packages/cw-orch/src/external/btsg_ft_factory.rs new file mode 100644 index 000000000..d3c29c6b9 --- /dev/null +++ b/packages/cw-orch/src/external/btsg_ft_factory.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use btsg_ft_factory::contract::{execute, instantiate, query, reply}; +use btsg_ft_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoExternalFantokenFactory; + +impl Uploadable for DaoExternalFantokenFactory { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("btsg_ft_factory") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply)) + } +} diff --git a/packages/cw-orch/src/external/cw721_roles.rs b/packages/cw-orch/src/external/cw721_roles.rs new file mode 100644 index 000000000..b2b40ab18 --- /dev/null +++ b/packages/cw-orch/src/external/cw721_roles.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw721_roles::contract::{execute, instantiate, query}; +use cw721_roles::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoExternalCw721Roles; + +impl Uploadable for DaoExternalCw721Roles { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw721_roles") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/external/cw_vesting.rs b/packages/cw-orch/src/external/cw_vesting.rs new file mode 100644 index 000000000..ecf9e30cb --- /dev/null +++ b/packages/cw-orch/src/external/cw_vesting.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw_vesting::contract::{execute, instantiate, query}; +use cw_vesting::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoExternalCwVesting; + +impl Uploadable for DaoExternalCwVesting { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw_vesting") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/external/migrator.rs b/packages/cw-orch/src/external/migrator.rs new file mode 100644 index 000000000..6f3ae1dad --- /dev/null +++ b/packages/cw-orch/src/external/migrator.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_migrator::contract::{execute, instantiate, query, reply}; +use dao_migrator::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoExternalMigrator; + +impl Uploadable for DaoExternalMigrator { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_migrator") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply)) + } +} diff --git a/packages/cw-orch/src/external/mod.rs b/packages/cw-orch/src/external/mod.rs new file mode 100644 index 000000000..ee24e4bfd --- /dev/null +++ b/packages/cw-orch/src/external/mod.rs @@ -0,0 +1,17 @@ +mod admin_factory; +mod btsg_ft_factory; +mod cw721_roles; +mod cw_vesting; +mod migrator; +mod payroll_factory; +mod token_swap; +mod tokenfactory_issuer; + +pub use admin_factory::DaoExternalAdminFactory; +pub use btsg_ft_factory::DaoExternalFantokenFactory; +pub use cw721_roles::DaoExternalCw721Roles; +pub use cw_vesting::DaoExternalCwVesting; +pub use migrator::DaoExternalMigrator; +pub use payroll_factory::DaoExternalPayrollFactory; +pub use token_swap::DaoExternalTokenSwap; +pub use tokenfactory_issuer::DaoExternalTokenfactoryIssuer; diff --git a/packages/cw-orch/src/external/payroll_factory.rs b/packages/cw-orch/src/external/payroll_factory.rs new file mode 100644 index 000000000..64ce0b0db --- /dev/null +++ b/packages/cw-orch/src/external/payroll_factory.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw_payroll_factory::contract::{execute, instantiate, query, reply}; +use cw_payroll_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoExternalPayrollFactory; + +impl Uploadable for DaoExternalPayrollFactory { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw_payroll_factory") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply)) + } +} diff --git a/packages/cw-orch/src/external/token_swap.rs b/packages/cw-orch/src/external/token_swap.rs new file mode 100644 index 000000000..fb6a19855 --- /dev/null +++ b/packages/cw-orch/src/external/token_swap.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw_token_swap::contract::{execute, instantiate, migrate, query}; +use cw_token_swap::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoExternalTokenSwap; + +impl Uploadable for DaoExternalTokenSwap { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw_token_swap") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_migrate(migrate)) + } +} diff --git a/packages/cw-orch/src/external/tokenfactory_issuer.rs b/packages/cw-orch/src/external/tokenfactory_issuer.rs new file mode 100644 index 000000000..4c9b0d8e8 --- /dev/null +++ b/packages/cw-orch/src/external/tokenfactory_issuer.rs @@ -0,0 +1,24 @@ +use cw_orch::{interface, prelude::*}; + +use cw_tokenfactory_issuer::contract::{execute, instantiate, migrate, query, reply}; +use cw_tokenfactory_issuer::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoExternalTokenfactoryIssuer; + +impl Uploadable for DaoExternalTokenfactoryIssuer { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw_tokenfactory_issuer") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_reply(reply) + .with_migrate(migrate), + ) + } +} diff --git a/packages/cw-orch/src/gauges/adapter.rs b/packages/cw-orch/src/gauges/adapter.rs new file mode 100644 index 000000000..156c68480 --- /dev/null +++ b/packages/cw-orch/src/gauges/adapter.rs @@ -0,0 +1,19 @@ +use cw_orch::{interface, prelude::*}; +use gauge_adapter::contract::{execute, instantiate, query}; +use gauge_adapter::msg::{AdapterQueryMsg, ExecuteMsg, InstantiateMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, AdapterQueryMsg, Empty)] +pub struct DaoGaugeAdapter; + +impl Uploadable for DaoGaugeAdapter { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("gauge_adapter") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/gauges/mod.rs b/packages/cw-orch/src/gauges/mod.rs new file mode 100644 index 000000000..9cc524d8e --- /dev/null +++ b/packages/cw-orch/src/gauges/mod.rs @@ -0,0 +1,5 @@ +mod adapter; +mod orchestrator; + +pub use adapter::DaoGaugeAdapter; +pub use orchestrator::DaoGaugeOrchestrator; \ No newline at end of file diff --git a/packages/cw-orch/src/gauges/orchestrator.rs b/packages/cw-orch/src/gauges/orchestrator.rs new file mode 100644 index 000000000..473eb7101 --- /dev/null +++ b/packages/cw-orch/src/gauges/orchestrator.rs @@ -0,0 +1,22 @@ +use cw_orch::{interface, prelude::*}; +use gauge_orchestrator::contract::{execute, instantiate, query}; +use gauge_orchestrator::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoGaugeOrchestrator; + +impl Uploadable for DaoGaugeOrchestrator { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("gauge_orchestrator") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_migrate(gauge_orchestrator::contract::migrate), + ) + } +} diff --git a/packages/cw-orch/src/lib.rs b/packages/cw-orch/src/lib.rs new file mode 100644 index 000000000..ea837ba08 --- /dev/null +++ b/packages/cw-orch/src/lib.rs @@ -0,0 +1,41 @@ +#[cfg(not(target_arch = "wasm32"))] +mod core; +#[cfg(not(target_arch = "wasm32"))] +mod distribution; +#[cfg(not(target_arch = "wasm32"))] +mod external; +#[cfg(not(target_arch = "wasm32"))] +mod gauges; +#[cfg(not(target_arch = "wasm32"))] +mod pre_propose; +#[cfg(not(target_arch = "wasm32"))] +mod proposal; +#[cfg(not(target_arch = "wasm32"))] +mod staking; +#[cfg(not(target_arch = "wasm32"))] +mod test_contracts; +#[cfg(not(target_arch = "wasm32"))] +mod voting; + +#[cfg(not(target_arch = "wasm32"))] +pub use core::*; +#[cfg(not(target_arch = "wasm32"))] +pub use distribution::*; +#[cfg(not(target_arch = "wasm32"))] +pub use external::*; +#[cfg(not(target_arch = "wasm32"))] +pub use gauges::*; +#[cfg(not(target_arch = "wasm32"))] +pub use pre_propose::*; +#[cfg(not(target_arch = "wasm32"))] +pub use proposal::*; +#[cfg(not(target_arch = "wasm32"))] +pub use staking::*; +#[cfg(not(target_arch = "wasm32"))] +pub use test_contracts::*; +#[cfg(not(target_arch = "wasm32"))] +pub use voting::*; + +#[cfg(feature = "wasm_test")] +#[cfg(test)] +pub mod tests; diff --git a/packages/cw-orch/src/pre_propose/approval_single.rs b/packages/cw-orch/src/pre_propose/approval_single.rs new file mode 100644 index 000000000..98b9927f6 --- /dev/null +++ b/packages/cw-orch/src/pre_propose/approval_single.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_pre_propose_approval_single::contract::{execute, instantiate, query}; +use dao_pre_propose_approval_single::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoPreProposeApprovalSingle; + +impl Uploadable for DaoPreProposeApprovalSingle { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_pre_propose_approval_single") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/pre_propose/approver.rs b/packages/cw-orch/src/pre_propose/approver.rs new file mode 100644 index 000000000..cebd7c13a --- /dev/null +++ b/packages/cw-orch/src/pre_propose/approver.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_pre_propose_approver::contract::{execute, instantiate, query}; +use dao_pre_propose_approver::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoPreProposeApprover; + +impl Uploadable for DaoPreProposeApprover { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_pre_propose_approver") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/pre_propose/mod.rs b/packages/cw-orch/src/pre_propose/mod.rs new file mode 100644 index 000000000..64ccef2f2 --- /dev/null +++ b/packages/cw-orch/src/pre_propose/mod.rs @@ -0,0 +1,9 @@ +mod approval_single; +mod approver; +mod multiple; +mod single; + +pub use approval_single::DaoPreProposeApprovalSingle; +pub use approver::DaoPreProposeApprover; +pub use multiple::DaoPreProposeMultiple; +pub use single::DaoPreProposeSingle; diff --git a/packages/cw-orch/src/pre_propose/multiple.rs b/packages/cw-orch/src/pre_propose/multiple.rs new file mode 100644 index 000000000..7517df320 --- /dev/null +++ b/packages/cw-orch/src/pre_propose/multiple.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_pre_propose_multiple::contract::{execute, instantiate, query}; +use dao_pre_propose_multiple::contract::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoPreProposeMultiple; + +impl Uploadable for DaoPreProposeMultiple { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_pre_propose_multiple") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/pre_propose/single.rs b/packages/cw-orch/src/pre_propose/single.rs new file mode 100644 index 000000000..891654455 --- /dev/null +++ b/packages/cw-orch/src/pre_propose/single.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_pre_propose_single::contract::{execute, instantiate, query}; +use dao_pre_propose_single::contract::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoPreProposeSingle; + +impl Uploadable for DaoPreProposeSingle { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_pre_propose_single") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/proposal/condorcet.rs b/packages/cw-orch/src/proposal/condorcet.rs new file mode 100644 index 000000000..fdfb8fb50 --- /dev/null +++ b/packages/cw-orch/src/proposal/condorcet.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_proposal_condorcet::contract::{execute, instantiate, query, reply}; +use dao_proposal_condorcet::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoProposalCondorcet; + +impl Uploadable for DaoProposalCondorcet { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_proposal_condorcet") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply)) + } +} diff --git a/packages/cw-orch/src/proposal/mod.rs b/packages/cw-orch/src/proposal/mod.rs new file mode 100644 index 000000000..faf0b7796 --- /dev/null +++ b/packages/cw-orch/src/proposal/mod.rs @@ -0,0 +1,7 @@ +mod condorcet; +mod multiple; +mod single; + +pub use condorcet::DaoProposalCondorcet; +pub use multiple::DaoProposalMultiple; +pub use single::DaoProposalSingle; diff --git a/packages/cw-orch/src/proposal/multiple.rs b/packages/cw-orch/src/proposal/multiple.rs new file mode 100644 index 000000000..dc28e8ef0 --- /dev/null +++ b/packages/cw-orch/src/proposal/multiple.rs @@ -0,0 +1,24 @@ +use cw_orch::{interface, prelude::*}; + +use dao_proposal_multiple::contract::{execute, instantiate, migrate, query, reply}; +use dao_proposal_multiple::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoProposalMultiple; + +impl Uploadable for DaoProposalMultiple { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_proposal_multiple") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_reply(reply) + .with_migrate(migrate), + ) + } +} diff --git a/packages/cw-orch/src/proposal/single.rs b/packages/cw-orch/src/proposal/single.rs new file mode 100644 index 000000000..7db09ee3b --- /dev/null +++ b/packages/cw-orch/src/proposal/single.rs @@ -0,0 +1,24 @@ +use cw_orch::{interface, prelude::*}; + +use dao_proposal_single::contract::{execute, instantiate, migrate, query, reply}; +use dao_proposal_single::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoProposalSingle; + +impl Uploadable for DaoProposalSingle { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_proposal_single") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_reply(reply) + .with_migrate(migrate), + ) + } +} diff --git a/packages/cw-orch/src/staking/cw20_stake.rs b/packages/cw-orch/src/staking/cw20_stake.rs new file mode 100644 index 000000000..03951004d --- /dev/null +++ b/packages/cw-orch/src/staking/cw20_stake.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw20_stake::contract::{execute, instantiate, migrate, query}; +use cw20_stake::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoStakingCw20; + +impl Uploadable for DaoStakingCw20 { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw20_stake") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_migrate(migrate)) + } +} diff --git a/packages/cw-orch/src/staking/external_rewards.rs b/packages/cw-orch/src/staking/external_rewards.rs new file mode 100644 index 000000000..ec1f584b0 --- /dev/null +++ b/packages/cw-orch/src/staking/external_rewards.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw20_stake_external_rewards::contract::{execute, instantiate, migrate, query}; +use cw20_stake_external_rewards::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoStakingCw20ExternalRewards; + +impl Uploadable for DaoStakingCw20ExternalRewards { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw20_stake_external_rewards") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_migrate(migrate)) + } +} diff --git a/packages/cw-orch/src/staking/mod.rs b/packages/cw-orch/src/staking/mod.rs new file mode 100644 index 000000000..1823256f6 --- /dev/null +++ b/packages/cw-orch/src/staking/mod.rs @@ -0,0 +1,7 @@ +mod cw20_stake; +mod external_rewards; +mod reward_distributor; + +pub use cw20_stake::DaoStakingCw20; +pub use external_rewards::DaoStakingCw20ExternalRewards; +pub use reward_distributor::DaoStakingCw20RewardDistributor; diff --git a/packages/cw-orch/src/staking/reward_distributor.rs b/packages/cw-orch/src/staking/reward_distributor.rs new file mode 100644 index 000000000..1bb80bf82 --- /dev/null +++ b/packages/cw-orch/src/staking/reward_distributor.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use cw20_stake_reward_distributor::contract::{execute, instantiate, migrate, query}; +use cw20_stake_reward_distributor::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoStakingCw20RewardDistributor; + +impl Uploadable for DaoStakingCw20RewardDistributor { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("cw20_stake_reward_distributor") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_migrate(migrate)) + } +} diff --git a/packages/cw-orch/src/test_contracts/cw721.rs b/packages/cw-orch/src/test_contracts/cw721.rs new file mode 100644 index 000000000..8f1097f08 --- /dev/null +++ b/packages/cw-orch/src/test_contracts/cw721.rs @@ -0,0 +1,19 @@ +use cosmwasm_std::Empty; +use cw721_base::{ + entry::{execute, instantiate, query}, + ExecuteMsg, InstantiateMsg, QueryMsg, +}; +use cw_orch::{interface, prelude::*}; + +pub type Cw721BaseQueryMsg = QueryMsg; +#[interface(InstantiateMsg, ExecuteMsg, Cw721BaseQueryMsg, Empty)] +pub struct Cw721BaseGeneric; + +impl Uploadable for Cw721BaseGeneric { + // Return a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} + +pub type Cw721Base = Cw721BaseGeneric, Empty>; diff --git a/packages/cw-orch/src/test_contracts/gauge_adapter.rs b/packages/cw-orch/src/test_contracts/gauge_adapter.rs new file mode 100644 index 000000000..f11020e52 --- /dev/null +++ b/packages/cw-orch/src/test_contracts/gauge_adapter.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_gauge_adapter::contract::{execute, instantiate, query, ExecuteMsg, InstantiateMsg}; +use gauge_adapter::msg::AdapterQueryMsg; + +#[interface(InstantiateMsg, ExecuteMsg, AdapterQueryMsg, Empty)] +pub struct DaoGaugeAdapterGeneric; + +impl Uploadable for DaoGaugeAdapterGeneric { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_proposal_hook_counter") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/test_contracts/mod.rs b/packages/cw-orch/src/test_contracts/mod.rs new file mode 100644 index 000000000..e6eb17504 --- /dev/null +++ b/packages/cw-orch/src/test_contracts/mod.rs @@ -0,0 +1,14 @@ +mod cw721; +mod gauge_adapter; +mod proposal_hook_counter; +mod proposal_sudo; +mod test_custom_factory; +mod voting_cw20_balance; + +#[cfg(not(target_arch = "wasm32"))] +pub use gauge_adapter::DaoGaugeAdapterGeneric; +pub use cw721::Cw721Base; +pub use proposal_hook_counter::DaoProposalHookCounter; +pub use proposal_sudo::DaoProposalSudo; +pub use test_custom_factory::DaoTestCustomFactory; +pub use voting_cw20_balance::DaoVotingCw20Balance; diff --git a/packages/cw-orch/src/test_contracts/proposal_hook_counter.rs b/packages/cw-orch/src/test_contracts/proposal_hook_counter.rs new file mode 100644 index 000000000..7dc26654e --- /dev/null +++ b/packages/cw-orch/src/test_contracts/proposal_hook_counter.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_proposal_hook_counter::contract::{execute, instantiate, query}; +use dao_proposal_hook_counter::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoProposalHookCounter; + +impl Uploadable for DaoProposalHookCounter { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_proposal_hook_counter") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/test_contracts/proposal_sudo.rs b/packages/cw-orch/src/test_contracts/proposal_sudo.rs new file mode 100644 index 000000000..bd623624a --- /dev/null +++ b/packages/cw-orch/src/test_contracts/proposal_sudo.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_proposal_sudo::contract::{execute, instantiate, query}; +use dao_proposal_sudo::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoProposalSudo; + +impl Uploadable for DaoProposalSudo { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_proposal_sudo") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query)) + } +} diff --git a/packages/cw-orch/src/test_contracts/test_custom_factory.rs b/packages/cw-orch/src/test_contracts/test_custom_factory.rs new file mode 100644 index 000000000..3424df887 --- /dev/null +++ b/packages/cw-orch/src/test_contracts/test_custom_factory.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_test_custom_factory::contract::{execute, instantiate, query, reply}; +use dao_test_custom_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoTestCustomFactory; + +impl Uploadable for DaoTestCustomFactory { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_test_custom_factory") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply)) + } +} diff --git a/packages/cw-orch/src/test_contracts/voting_cw20_balance.rs b/packages/cw-orch/src/test_contracts/voting_cw20_balance.rs new file mode 100644 index 000000000..fdf7b84c0 --- /dev/null +++ b/packages/cw-orch/src/test_contracts/voting_cw20_balance.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_voting_cw20_balance::contract::{execute, instantiate, query, reply}; +use dao_voting_cw20_balance::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoVotingCw20Balance; + +impl Uploadable for DaoVotingCw20Balance { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_voting_cw20_balance") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply)) + } +} diff --git a/packages/cw-orch/src/tests.rs b/packages/cw-orch/src/tests.rs new file mode 100644 index 000000000..c3636e190 --- /dev/null +++ b/packages/cw-orch/src/tests.rs @@ -0,0 +1,69 @@ +use std::collections::HashSet; + +use cw_orch::{ + contract::interface_traits::Uploadable, + environment::{ChainInfo, ChainKind, NetworkInfo}, + mock::Mock, +}; + +use crate::{ + Cw20Stake, Cw20StakeExternalRewards, Cw20StakeRewardDistributor, DaoDaoCore, + DaoPreProposeApprovalSingle, DaoPreProposeApprover, DaoPreProposeMultiple, DaoPreProposeSingle, + DaoProposalCondorcet, DaoProposalHookCounter, DaoProposalMultiple, DaoProposalSingle, + DaoProposalSudo, DaoTestCustomFactory, DaoVotingCw20Balance, DaoVotingCw20Staked, DaoVotingCw4, + DaoVotingCw721Roles, DaoVotingCw721Staked, DaoVotingTokenStaked, +}; + +pub const DUMMY_CHAIN_INFO: ChainInfo = ChainInfo { + chain_id: "mock-1", + gas_denom: "none", + gas_price: 0.0, + grpc_urls: &[], + lcd_url: None, + fcd_url: None, + network_info: NetworkInfo { + chain_name: "mock", + pub_address_prefix: "mock", + coin_type: 118, + }, + kind: ChainKind::Local, +}; + +#[test] +fn test_all_wasms_different() { + let all_paths = vec![ + // CORE + DaoDaoCore::::wasm(&DUMMY_CHAIN_INFO.into()), + // PRE-PROPOSE + DaoPreProposeApprovalSingle::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoPreProposeApprover::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoPreProposeMultiple::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoPreProposeSingle::::wasm(&DUMMY_CHAIN_INFO.into()), + // PROPOSAL + DaoProposalCondorcet::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoProposalMultiple::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoProposalSingle::::wasm(&DUMMY_CHAIN_INFO.into()), + // Stake + Cw20Stake::::wasm(&DUMMY_CHAIN_INFO.into()), + Cw20StakeExternalRewards::::wasm(&DUMMY_CHAIN_INFO.into()), + Cw20StakeRewardDistributor::::wasm(&DUMMY_CHAIN_INFO.into()), + // Voting + DaoVotingCw4::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoVotingCw20Staked::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoVotingCw721Staked::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoVotingCw721Roles::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoVotingTokenStaked::::wasm(&DUMMY_CHAIN_INFO.into()), + // Test + DaoProposalHookCounter::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoProposalSudo::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoTestCustomFactory::::wasm(&DUMMY_CHAIN_INFO.into()), + DaoVotingCw20Balance::::wasm(&DUMMY_CHAIN_INFO.into()), + ]; + let all_paths: Vec<_> = all_paths + .into_iter() + .map(|path| path.path().as_os_str().to_string_lossy().to_string()) + .collect(); + + let mut uniq = HashSet::new(); + assert!(all_paths.into_iter().all(move |x| uniq.insert(x))); +} diff --git a/packages/cw-orch/src/voting/cw20_staked.rs b/packages/cw-orch/src/voting/cw20_staked.rs new file mode 100644 index 000000000..1f044a913 --- /dev/null +++ b/packages/cw-orch/src/voting/cw20_staked.rs @@ -0,0 +1,24 @@ +use cw_orch::{interface, prelude::*}; + +use dao_voting_cw20_staked::contract::{execute, instantiate, migrate, query, reply}; +use dao_voting_cw20_staked::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoVotingCw20Staked; + +impl Uploadable for DaoVotingCw20Staked { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_voting_cw20_staked") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_reply(reply) + .with_migrate(migrate), + ) + } +} diff --git a/packages/cw-orch/src/voting/cw4.rs b/packages/cw-orch/src/voting/cw4.rs new file mode 100644 index 000000000..11dfac5ea --- /dev/null +++ b/packages/cw-orch/src/voting/cw4.rs @@ -0,0 +1,24 @@ +use cw_orch::{interface, prelude::*}; + +use dao_voting_cw4::contract::{execute, instantiate, migrate, query, reply}; +use dao_voting_cw4::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoVotingCw4; + +impl Uploadable for DaoVotingCw4 { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_voting_cw4") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_reply(reply) + .with_migrate(migrate), + ) + } +} diff --git a/packages/cw-orch/src/voting/cw721_roles.rs b/packages/cw-orch/src/voting/cw721_roles.rs new file mode 100644 index 000000000..6469a0887 --- /dev/null +++ b/packages/cw-orch/src/voting/cw721_roles.rs @@ -0,0 +1,20 @@ +use cw_orch::{interface, prelude::*}; + +use dao_voting_cw721_roles::contract::{execute, instantiate, query, reply}; +use dao_voting_cw721_roles::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, Empty)] +pub struct DaoVotingCw721Roles; + +impl Uploadable for DaoVotingCw721Roles { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_voting_cw721_roles") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new(ContractWrapper::new_with_empty(execute, instantiate, query).with_reply(reply)) + } +} diff --git a/packages/cw-orch/src/voting/cw721_staked.rs b/packages/cw-orch/src/voting/cw721_staked.rs new file mode 100644 index 000000000..5ed2f1ce1 --- /dev/null +++ b/packages/cw-orch/src/voting/cw721_staked.rs @@ -0,0 +1,24 @@ +use cw_orch::{interface, prelude::*}; + +use dao_voting_cw721_staked::contract::{execute, instantiate, migrate, query, reply}; +use dao_voting_cw721_staked::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoVotingCw721Staked; + +impl Uploadable for DaoVotingCw721Staked { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_voting_cw721_staked") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_reply(reply) + .with_migrate(migrate), + ) + } +} diff --git a/packages/cw-orch/src/voting/mod.rs b/packages/cw-orch/src/voting/mod.rs new file mode 100644 index 000000000..41e697927 --- /dev/null +++ b/packages/cw-orch/src/voting/mod.rs @@ -0,0 +1,11 @@ +mod cw20_staked; +mod cw4; +mod cw721_roles; +mod cw721_staked; +mod token_staked; + +pub use cw20_staked::DaoVotingCw20Staked; +pub use cw4::DaoVotingCw4; +pub use cw721_roles::DaoVotingCw721Roles; +pub use cw721_staked::DaoVotingCw721Staked; +pub use token_staked::DaoVotingTokenStaked; diff --git a/packages/cw-orch/src/voting/token_staked.rs b/packages/cw-orch/src/voting/token_staked.rs new file mode 100644 index 000000000..a8f41ca94 --- /dev/null +++ b/packages/cw-orch/src/voting/token_staked.rs @@ -0,0 +1,24 @@ +use cw_orch::{interface, prelude::*}; + +use dao_voting_token_staked::contract::{execute, instantiate, migrate, query, reply}; +use dao_voting_token_staked::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[interface(InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg)] +pub struct DaoVotingTokenStaked; + +impl Uploadable for DaoVotingTokenStaked { + /// Return the path to the wasm file corresponding to the contract + fn wasm(_chain: &ChainInfoOwned) -> WasmPath { + artifacts_dir_from_workspace!() + .find_wasm_path("dao_voting_token_staked") + .unwrap() + } + /// Returns a CosmWasm contract wrapper + fn wrapper() -> Box> { + Box::new( + ContractWrapper::new_with_empty(execute, instantiate, query) + .with_reply(reply) + .with_migrate(migrate), + ) + } +} diff --git a/packages/cw-tokenfactory-types/Cargo.toml b/packages/cw-tokenfactory-types/Cargo.toml index 4a546a136..3c571b153 100644 --- a/packages/cw-tokenfactory-types/Cargo.toml +++ b/packages/cw-tokenfactory-types/Cargo.toml @@ -23,6 +23,7 @@ dao-interface = { workspace = true } osmosis-std = { workspace = true } osmosis-std-derive = { workspace = true } prost = { workspace = true } +prost-derive = { workspace = true } prost-types = { workspace = true } schemars = { workspace = true } serde = { workspace = true } diff --git a/packages/dao-cw721-extensions/Cargo.toml b/packages/dao-cw721-extensions/Cargo.toml index 609e89d0d..f3d2ca49e 100644 --- a/packages/dao-cw721-extensions/Cargo.toml +++ b/packages/dao-cw721-extensions/Cargo.toml @@ -12,3 +12,4 @@ cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cw-controllers = { workspace = true } cw4 = { workspace = true } +cw-orch = { workspace = true } diff --git a/packages/dao-cw721-extensions/src/roles.rs b/packages/dao-cw721-extensions/src/roles.rs index 0f2c9166a..a303e508e 100644 --- a/packages/dao-cw721-extensions/src/roles.rs +++ b/packages/dao-cw721-extensions/src/roles.rs @@ -10,6 +10,7 @@ pub struct MetadataExt { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteExt { /// Add a new hook to be informed of all membership changes. /// Must be called by Admin @@ -32,7 +33,7 @@ pub enum ExecuteExt { impl CustomMsg for ExecuteExt {} #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryExt { /// Total weight at a given height #[returns(cw4::TotalWeightResponse)] diff --git a/packages/dao-interface/Cargo.toml b/packages/dao-interface/Cargo.toml index 643c84df3..6cf745ab2 100644 --- a/packages/dao-interface/Cargo.toml +++ b/packages/dao-interface/Cargo.toml @@ -15,6 +15,7 @@ cw20 = { workspace = true } cw721 = { workspace = true } cw-utils = { workspace = true } osmosis-std = { workspace = true } +cw-orch = {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 aae0678fd..d410b5b68 100644 --- a/packages/dao-interface/src/lib.rs +++ b/packages/dao-interface/src/lib.rs @@ -8,3 +8,6 @@ pub mod query; pub mod state; pub mod token; pub mod voting; + +pub use msg::ExecuteMsgFns as CoreExecuteMsgFns; +pub use msg::QueryMsgFns as CoreQueryMsgFns; diff --git a/packages/dao-interface/src/msg.rs b/packages/dao-interface/src/msg.rs index 969288433..e8027f7c5 100644 --- a/packages/dao-interface/src/msg.rs +++ b/packages/dao-interface/src/msg.rs @@ -53,6 +53,7 @@ pub struct InstantiateMsg { } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Callable by the Admin, if one is configured. /// Executes messages in order. @@ -134,7 +135,7 @@ pub enum ExecuteMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg { /// Get's the DAO's admin. Returns `Addr`. #[returns(cosmwasm_std::Addr)] @@ -147,7 +148,7 @@ pub enum QueryMsg { Config {}, /// Gets the token balance for each cw20 registered with the /// contract. - #[returns(crate::query::Cw20BalanceResponse)] + #[returns(Vec)] Cw20Balances { start_after: Option, limit: Option, @@ -178,7 +179,7 @@ pub enum QueryMsg { /// example, given the items `{ "group": "foo", "subdao": "bar"}` /// this query would return `[("group", "foo"), ("subdao", /// "bar")]`. - #[returns(Vec)] + #[returns(Vec<(String, String)>)] ListItems { start_after: Option, limit: Option, diff --git a/packages/dao-pre-propose-base/Cargo.toml b/packages/dao-pre-propose-base/Cargo.toml index c063d34f9..e94f297fd 100644 --- a/packages/dao-pre-propose-base/Cargo.toml +++ b/packages/dao-pre-propose-base/Cargo.toml @@ -28,6 +28,13 @@ dao-interface = { workspace = true } dao-voting = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } +cw-orch = {workspace = true} +semver = { workspace = true } + +# v2.4.1 packages for state migration +cw-denom-v241 = { workspace = true } +dao-pre-propose-base-v241 = { workspace = true } +dao-voting-v241 = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/packages/dao-pre-propose-base/src/error.rs b/packages/dao-pre-propose-base/src/error.rs index 127996166..fc3bbaefb 100644 --- a/packages/dao-pre-propose-base/src/error.rs +++ b/packages/dao-pre-propose-base/src/error.rs @@ -4,7 +4,9 @@ use cw_utils::ParseReplyError; use thiserror::Error; use cw_hooks::HookError; -use dao_voting::{deposit::DepositError, status::Status}; +use dao_voting::{ + deposit::DepositError, pre_propose::PreProposeSubmissionPolicyError, status::Status, +}; #[derive(Error, Debug, PartialEq)] pub enum PreProposeError { @@ -23,15 +25,15 @@ pub enum PreProposeError { #[error(transparent)] ParseReplyError(#[from] ParseReplyError), + #[error(transparent)] + SubmissionPolicy(#[from] PreProposeSubmissionPolicyError), + #[error("Message sender is not proposal module")] NotModule {}, #[error("Message sender is not dao")] NotDao {}, - #[error("You must be a member of this DAO (have voting power) to create a proposal")] - NotMember {}, - #[error("No denomination for withdrawal. specify a denomination to withdraw")] NoWithdrawalDenom {}, @@ -49,4 +51,10 @@ pub enum PreProposeError { #[error("An unknown reply ID was received.")] UnknownReplyID {}, + + #[error("Unsupported")] + Unsupported {}, + + #[error("Cannot migrate contract version {actual}. Requires: {required}")] + CannotMigrateVersion { required: String, actual: String }, } diff --git a/packages/dao-pre-propose-base/src/execute.rs b/packages/dao-pre-propose-base/src/execute.rs index 64e0ed3d6..e90ac127c 100644 --- a/packages/dao-pre-propose-base/src/execute.rs +++ b/packages/dao-pre-propose-base/src/execute.rs @@ -1,33 +1,42 @@ use cosmwasm_schema::schemars::JsonSchema; use cosmwasm_std::{ - to_json_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, - WasmMsg, + to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError, + StdResult, SubMsg, WasmMsg, }; -use cw2::set_contract_version; +use semver::{Version, VersionReq}; -use cw_denom::UncheckedDenom; +use cw2::{get_contract_version, set_contract_version, ContractVersion}; + +use cw_denom::{CheckedDenom, UncheckedDenom}; use dao_interface::voting::{Query as CwCoreQuery, VotingPowerAtHeightResponse}; use dao_voting::{ - deposit::{DepositRefundPolicy, UncheckedDepositInfo}, + deposit::{CheckedDepositInfo, DepositRefundPolicy, UncheckedDepositInfo}, + pre_propose::{PreProposeSubmissionPolicy, PreProposeSubmissionPolicyError}, status::Status, }; use serde::Serialize; use crate::{ error::PreProposeError, - msg::{DepositInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}, + helpers::add_and_remove_addresses, + msg::{DepositInfoResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, state::{Config, PreProposeContract}, }; +use cw_denom_v241::CheckedDenom as CheckedDenomV241; +use dao_pre_propose_base_v241::state::PreProposeContract as PreProposeContractV241; +use dao_voting_v241::deposit::DepositRefundPolicy as DepositRefundPolicyV241; + const CONTRACT_NAME: &str = "crates.io::dao-pre-propose-base"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -impl - PreProposeContract +impl + PreProposeContract where ProposalMessage: Serialize, QueryExt: JsonSchema, + MigrateExt: JsonSchema, { pub fn instantiate( &self, @@ -56,9 +65,11 @@ where .map(|info| info.into_checked(deps.as_ref(), dao.clone())) .transpose()?; + msg.submission_policy.validate()?; + let config = Config { deposit_info, - open_proposal_submission: msg.open_proposal_submission, + submission_policy: msg.submission_policy, }; self.config.save(deps.storage, &config)?; @@ -68,8 +79,8 @@ where .add_attribute("proposal_module", info.sender.into_string()) .add_attribute("deposit_info", format!("{:?}", config.deposit_info)) .add_attribute( - "open_proposal_submission", - config.open_proposal_submission.to_string(), + "submission_policy", + config.submission_policy.human_readable(), ) .add_attribute("dao", dao)) } @@ -85,8 +96,23 @@ where ExecuteMsg::Propose { msg } => self.execute_propose(deps, env, info, msg), ExecuteMsg::UpdateConfig { deposit_info, - open_proposal_submission, - } => self.execute_update_config(deps, info, deposit_info, open_proposal_submission), + submission_policy, + } => self.execute_update_config(deps, info, deposit_info, submission_policy), + ExecuteMsg::UpdateSubmissionPolicy { + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, + } => self.execute_update_submission_policy( + deps, + info, + denylist_add, + denylist_remove, + set_dao_members, + allowlist_add, + allowlist_remove, + ), ExecuteMsg::Withdraw { denom } => { self.execute_withdraw(deps.as_ref(), env, info, denom) } @@ -171,27 +197,117 @@ where deps: DepsMut, info: MessageInfo, deposit_info: Option, - open_proposal_submission: bool, + submission_policy: Option, ) -> Result { let dao = self.dao.load(deps.storage)?; if info.sender != dao { - Err(PreProposeError::NotDao {}) - } else { - let deposit_info = deposit_info - .map(|d| d.into_checked(deps.as_ref(), dao)) - .transpose()?; - self.config.save( - deps.storage, - &Config { + return Err(PreProposeError::NotDao {}); + } + + let deposit_info = deposit_info + .map(|d| d.into_checked(deps.as_ref(), dao)) + .transpose()?; + + if let Some(submision_policy) = &submission_policy { + submision_policy.validate()? + } + + self.config + .update(deps.storage, |prev| -> Result { + let new_submission_policy = if let Some(submission_policy) = submission_policy { + submission_policy + } else { + prev.submission_policy + }; + + Ok(Config { deposit_info, - open_proposal_submission, - }, - )?; + submission_policy: new_submission_policy, + }) + })?; + + Ok(Response::default() + .add_attribute("method", "update_config") + .add_attribute("sender", info.sender)) + } + + #[allow(clippy::too_many_arguments)] + pub fn execute_update_submission_policy( + &self, + deps: DepsMut, + info: MessageInfo, + denylist_add: Option>, + denylist_remove: Option>, + set_dao_members: Option, + allowlist_add: Option>, + allowlist_remove: Option>, + ) -> Result { + let dao = self.dao.load(deps.storage)?; + if info.sender != dao { + return Err(PreProposeError::NotDao {}); + } + + let mut config = self.config.load(deps.storage)?; + + match config.submission_policy { + PreProposeSubmissionPolicy::Anyone { mut denylist } => { + // Error if other values that apply to Specific were set. + if set_dao_members.is_some() + || allowlist_add.is_some() + || allowlist_remove.is_some() + { + return Err(PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::AnyoneInvalidUpdateFields {}, + )); + } + + add_and_remove_addresses( + deps.as_ref(), + &mut denylist, + denylist_add, + denylist_remove, + )?; + + config.submission_policy = PreProposeSubmissionPolicy::Anyone { denylist }; + } + PreProposeSubmissionPolicy::Specific { + dao_members, + mut allowlist, + mut denylist, + } => { + let dao_members = if let Some(new_dao_members) = set_dao_members { + new_dao_members + } else { + dao_members + }; - Ok(Response::default() - .add_attribute("method", "update_config") - .add_attribute("sender", info.sender)) + add_and_remove_addresses( + deps.as_ref(), + &mut allowlist, + allowlist_add, + allowlist_remove, + )?; + add_and_remove_addresses( + deps.as_ref(), + &mut denylist, + denylist_add, + denylist_remove, + )?; + + config.submission_policy = PreProposeSubmissionPolicy::Specific { + dao_members, + allowlist, + denylist, + }; + } } + + config.submission_policy.validate()?; + self.config.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("method", "update_submission_policy") + .add_attribute("sender", info.sender)) } pub fn execute_withdraw( @@ -341,20 +457,47 @@ where pub fn check_can_submit(&self, deps: Deps, who: Addr) -> Result<(), PreProposeError> { let config = self.config.load(deps.storage)?; - if !config.open_proposal_submission { - let dao = self.dao.load(deps.storage)?; - let voting_power: VotingPowerAtHeightResponse = deps.querier.query_wasm_smart( - dao.into_string(), - &CwCoreQuery::VotingPowerAtHeight { - address: who.into_string(), - height: None, - }, - )?; - if voting_power.power.is_zero() { - return Err(PreProposeError::NotMember {}); + match config.submission_policy { + PreProposeSubmissionPolicy::Anyone { denylist } => { + if !denylist.contains(&who) { + return Ok(()); + } + } + PreProposeSubmissionPolicy::Specific { + dao_members, + allowlist, + denylist, + } => { + // denylist overrides all other settings + if !denylist.contains(&who) { + // if on the allowlist, return early + if allowlist.contains(&who) { + return Ok(()); + } + + // check DAO membership only if not on the allowlist + if dao_members { + let dao = self.dao.load(deps.storage)?; + let voting_power: VotingPowerAtHeightResponse = + deps.querier.query_wasm_smart( + dao.into_string(), + &CwCoreQuery::VotingPowerAtHeight { + address: who.into_string(), + height: None, + }, + )?; + if !voting_power.power.is_zero() { + return Ok(()); + } + } + } } } - Ok(()) + + // all other cases are not allowed + Err(PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::Unauthorized {}, + )) } pub fn query(&self, deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { @@ -363,6 +506,9 @@ where to_json_binary(&self.proposal_module.load(deps.storage)?) } QueryMsg::Dao {} => to_json_binary(&self.dao.load(deps.storage)?), + QueryMsg::Info {} => to_json_binary(&dao_interface::proposal::InfoResponse { + info: cw2::get_contract_version(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)?; @@ -371,10 +517,104 @@ where proposer, }) } + QueryMsg::CanPropose { address } => { + let addr = deps.api.addr_validate(&address)?; + match self.check_can_submit(deps, addr) { + Ok(_) => to_json_binary(&true), + Err(err) => match err { + PreProposeError::SubmissionPolicy( + PreProposeSubmissionPolicyError::Unauthorized {}, + ) => to_json_binary(&false), + PreProposeError::Std(err) => Err(err), + _ => Err(StdError::generic_err(format!( + "unexpected error: {:?}", + err + ))), + }, + } + } QueryMsg::ProposalSubmittedHooks {} => { to_json_binary(&self.proposal_submitted_hooks.query_hooks(deps)?) } QueryMsg::QueryExtension { .. } => Ok(Binary::default()), } } + + pub fn migrate( + &self, + deps: DepsMut, + msg: MigrateMsg, + ) -> Result { + match msg { + MigrateMsg::FromUnderV250 { policy } => { + // all contracts >= v2.4.1 and < v2.5.0 have the same config + let required_str = ">=2.4.1, <2.5.0"; + + // ensure acceptable version + let requirement = VersionReq::parse(required_str).unwrap(); + let ContractVersion { version, .. } = get_contract_version(deps.storage)?; + let sem_version = Version::parse(&version).unwrap(); + + if !requirement.matches(&sem_version) { + return Err(PreProposeError::CannotMigrateVersion { + required: required_str.to_string(), + actual: version.clone(), + }); + } + + let old_contract = PreProposeContractV241::::default(); + let old_config = old_contract.config.load(deps.storage)?; + + // if provided a policy to update with, use it + let submission_policy = if let Some(submission_policy) = policy { + submission_policy + + // otherwise convert old `open_proposal_submission` flag + // into new policy enum + } else if old_config.open_proposal_submission { + PreProposeSubmissionPolicy::Anyone { denylist: vec![] } + } else { + PreProposeSubmissionPolicy::Specific { + dao_members: true, + allowlist: vec![], + denylist: vec![], + } + }; + + submission_policy.validate()?; + + let deposit_info: Option = + old_config.deposit_info.map(|old| CheckedDepositInfo { + denom: match old.denom { + CheckedDenomV241::Cw20(address) => CheckedDenom::Cw20(address), + CheckedDenomV241::Native(denom) => CheckedDenom::Native(denom), + }, + amount: old.amount, + refund_policy: match old.refund_policy { + DepositRefundPolicyV241::Always => DepositRefundPolicy::Always, + DepositRefundPolicyV241::Never => DepositRefundPolicy::Never, + DepositRefundPolicyV241::OnlyPassed => DepositRefundPolicy::OnlyPassed, + }, + }); + + self.config.save( + deps.storage, + &Config { + deposit_info, + submission_policy, + }, + )?; + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default() + .add_attribute("action", "migrate") + .add_attribute("from", version) + .add_attribute("to", CONTRACT_VERSION)) + } + MigrateMsg::Extension { .. } => Err(PreProposeError::Std(StdError::generic_err( + "not implemented", + ))), + } + } } diff --git a/packages/dao-pre-propose-base/src/helpers.rs b/packages/dao-pre-propose-base/src/helpers.rs new file mode 100644 index 000000000..cbc265da6 --- /dev/null +++ b/packages/dao-pre-propose-base/src/helpers.rs @@ -0,0 +1,32 @@ +use cosmwasm_std::{Addr, Deps, StdResult}; + +/// validate addresses and add to and/or remove from an existing list of +/// addresses, removing any duplicates. mutates the original list. +pub fn add_and_remove_addresses( + deps: Deps, + list: &mut Vec, + add: Option>, + remove: Option>, +) -> StdResult<()> { + if let Some(add) = add { + let mut addrs = add + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + list.append(&mut addrs); + list.sort(); + list.dedup(); + } + + if let Some(remove) = remove { + let addrs = remove + .iter() + .map(|addr| deps.api.addr_validate(addr)) + .collect::>>()?; + + list.retain(|a| !addrs.contains(a)); + } + + Ok(()) +} diff --git a/packages/dao-pre-propose-base/src/lib.rs b/packages/dao-pre-propose-base/src/lib.rs index be0040039..707bf4681 100644 --- a/packages/dao-pre-propose-base/src/lib.rs +++ b/packages/dao-pre-propose-base/src/lib.rs @@ -2,6 +2,7 @@ pub mod error; pub mod execute; +pub mod helpers; pub mod msg; pub mod state; diff --git a/packages/dao-pre-propose-base/src/msg.rs b/packages/dao-pre-propose-base/src/msg.rs index b2f2cc410..7a15f3c28 100644 --- a/packages/dao-pre-propose-base/src/msg.rs +++ b/packages/dao-pre-propose-base/src/msg.rs @@ -1,7 +1,9 @@ use cosmwasm_schema::{cw_serde, schemars::JsonSchema, QueryResponses}; use cw_denom::UncheckedDenom; +use dao_interface::proposal::InfoResponse; use dao_voting::{ deposit::{CheckedDepositInfo, UncheckedDepositInfo}, + pre_propose::PreProposeSubmissionPolicy, status::Status, }; @@ -10,19 +12,19 @@ pub struct InstantiateMsg { /// Information about the deposit requirements for this /// module. None if no deposit. pub deposit_info: Option, - /// If false, only members (addresses with voting power) may create - /// proposals in the DAO. Otherwise, any address may create a - /// proposal so long as they pay the deposit. - pub open_proposal_submission: bool, + /// The policy dictating who is allowed to submit proposals. + pub submission_policy: PreProposeSubmissionPolicy, /// Extension for instantiation. The default implementation will /// do nothing with this data. pub extension: InstantiateExt, } #[cw_serde] +#[derive(cw_orch::ExecuteFns)] pub enum ExecuteMsg { /// Creates a new proposal in the pre-propose module. MSG will be /// serialized and used as the proposal creation message. + #[cw_orch(payable)] Propose { msg: ProposalMessage }, /// Updates the configuration of this module. This will completely @@ -30,8 +32,25 @@ pub enum ExecuteMsg { /// will only apply to proposals created after the config is /// updated. Only the DAO may execute this message. UpdateConfig { + /// If None, will remove the deposit. Backwards compatible. deposit_info: Option, - open_proposal_submission: bool, + /// If None, will leave the submission policy in the config as-is. + submission_policy: Option, + }, + + /// Perform more granular submission policy updates to allow for atomic + /// operations that don't override others. + UpdateSubmissionPolicy { + /// Optionally add to the denylist. Works for any submission policy. + denylist_add: Option>, + /// Optionally remove from denylist. Works for any submission policy. + denylist_remove: Option>, + /// If using specific policy, optionally update the `dao_members` flag. + set_dao_members: Option, + /// If using specific policy, optionally add to the allowlist. + allowlist_add: Option>, + /// If using specific policy, optionally remove from the allowlist. + allowlist_remove: Option>, }, /// Withdraws funds inside of this contract to the message @@ -89,7 +108,7 @@ pub enum ExecuteMsg { } #[cw_serde] -#[derive(QueryResponses)] +#[derive(QueryResponses, cw_orch::QueryFns)] pub enum QueryMsg where QueryExt: JsonSchema, @@ -102,6 +121,9 @@ where /// with. Returns `Addr`. #[returns(cosmwasm_std::Addr)] Dao {}, + /// Returns contract version info. + #[returns(InfoResponse)] + Info {}, /// Gets the module's configuration. #[returns(crate::state::Config)] Config {}, @@ -109,6 +131,9 @@ where /// PROPOSAL_ID. #[returns(DepositInfoResponse)] DepositInfo { proposal_id: u64 }, + /// Returns whether or not the address can submit proposals. + #[returns(bool)] + CanPropose { address: String }, /// Returns list of proposal submitted hooks. #[returns(cw_hooks::HooksResponse)] ProposalSubmittedHooks {}, @@ -125,3 +150,18 @@ pub struct DepositInfoResponse { /// The address that created the proposal. pub proposer: cosmwasm_std::Addr, } + +#[cw_serde] +pub enum MigrateMsg +where + MigrateExt: JsonSchema, +{ + FromUnderV250 { + /// Optionally set a new submission policy with more granular controls. + /// If not set, the current policy will remain. + policy: Option, + }, + Extension { + msg: MigrateExt, + }, +} diff --git a/packages/dao-pre-propose-base/src/state.rs b/packages/dao-pre-propose-base/src/state.rs index 26310a5cb..5004cfab5 100644 --- a/packages/dao-pre-propose-base/src/state.rs +++ b/packages/dao-pre-propose-base/src/state.rs @@ -5,20 +5,18 @@ use cosmwasm_std::Addr; use cw_hooks::Hooks; use cw_storage_plus::{Item, Map}; -use dao_voting::deposit::CheckedDepositInfo; +use dao_voting::{deposit::CheckedDepositInfo, pre_propose::PreProposeSubmissionPolicy}; #[cw_serde] pub struct Config { /// Information about the deposit required to create a /// proposal. If `None`, no deposit is required. pub deposit_info: Option, - /// If false, only members (addresses with voting power) may create - /// proposals in the DAO. Otherwise, any address may create a - /// proposal so long as they pay the deposit. - pub open_proposal_submission: bool, + /// The policy dictating who is allowed to submit proposals. + pub submission_policy: PreProposeSubmissionPolicy, } -pub struct PreProposeContract { +pub struct PreProposeContract { /// The proposal module that this module is associated with. pub proposal_module: Item<'static, Addr>, /// The DAO (dao-dao-core module) that this module is associated @@ -37,11 +35,12 @@ pub struct PreProposeContract, execute_type: PhantomData, query_type: PhantomData, + migrate_type: PhantomData, proposal_type: PhantomData, } -impl - PreProposeContract +impl + PreProposeContract { const fn new( proposal_key: &'static str, @@ -59,13 +58,14 @@ impl execute_type: PhantomData, instantiate_type: PhantomData, query_type: PhantomData, + migrate_type: PhantomData, proposal_type: PhantomData, } } } -impl Default - for PreProposeContract +impl Default + for PreProposeContract { fn default() -> Self { // Call into constant function here. Presumably, the compiler diff --git a/packages/dao-pre-propose-base/src/tests.rs b/packages/dao-pre-propose-base/src/tests.rs index a6b12a74d..c575ea677 100644 --- a/packages/dao-pre-propose-base/src/tests.rs +++ b/packages/dao-pre-propose-base/src/tests.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{ to_json_binary, Addr, Binary, ContractResult, Empty, Response, SubMsg, WasmMsg, }; use cw_hooks::HooksResponse; -use dao_voting::status::Status; +use dao_voting::{pre_propose::PreProposeSubmissionPolicy, status::Status}; use crate::{ error::PreProposeError, @@ -12,7 +12,7 @@ use crate::{ state::{Config, PreProposeContract}, }; -type Contract = PreProposeContract; +type Contract = PreProposeContract; #[test] fn test_completed_hook_status_invariant() { @@ -87,7 +87,7 @@ fn test_proposal_submitted_hooks() { &mut deps.storage, &Config { deposit_info: None, - open_proposal_submission: true, + submission_policy: PreProposeSubmissionPolicy::Anyone { denylist: vec![] }, }, ) .unwrap(); diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index 51d0685b4..9b191be86 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -11,6 +11,8 @@ version = { workspace = true } # 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"] # 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 @@ -34,6 +36,7 @@ rand = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +cw-admin-factory = { workspace = true } cw-core-v1 = { workspace = true, features = ["library"] } cw-hooks = { workspace = true } cw-proposal-single-v1 = { workspace = true } @@ -55,6 +58,7 @@ dao-voting-cw20-staked = { workspace = true } dao-voting-cw4 = { workspace = true } dao-voting-cw721-staked = { workspace = true } dao-voting-cw721-roles = { workspace = true } +dao-voting-onft-staked = { 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 a0418a48f..1739ed124 100644 --- a/packages/dao-testing/src/contracts.rs +++ b/packages/dao-testing/src/contracts.rs @@ -113,11 +113,12 @@ pub fn native_staked_balances_voting_contract() -> Box> { dao_voting_token_staked::contract::execute, dao_voting_token_staked::contract::instantiate, dao_voting_token_staked::contract::query, - ); + ) + .with_reply(dao_voting_token_staked::contract::reply); Box::new(contract) } -pub fn voting_cw721_staked_contract() -> Box> { +pub fn cw721_staked_voting_contract() -> Box> { let contract = ContractWrapper::new( dao_voting_cw721_staked::contract::execute, dao_voting_cw721_staked::contract::instantiate, @@ -127,6 +128,15 @@ pub fn voting_cw721_staked_contract() -> Box> { Box::new(contract) } +pub fn onft_staked_voting_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_onft_staked::contract::execute, + dao_voting_onft_staked::contract::instantiate, + dao_voting_onft_staked::contract::query, + ); + Box::new(contract) +} + pub fn dao_dao_contract() -> Box> { let contract = ContractWrapper::new( dao_dao_core::contract::execute, diff --git a/packages/dao-testing/src/test_tube/cw4_group.rs b/packages/dao-testing/src/test_tube/cw4_group.rs new file mode 100644 index 000000000..a40d407c7 --- /dev/null +++ b/packages/dao-testing/src/test_tube/cw4_group.rs @@ -0,0 +1,128 @@ +use cosmwasm_std::Coin; +use cw4_group::{ + 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 Cw4Group<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> Cw4Group<'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("cw4_group.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("cw4_group-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_admin_factory.rs b/packages/dao-testing/src/test_tube/cw_admin_factory.rs new file mode 100644 index 000000000..17675f5b2 --- /dev/null +++ b/packages/dao-testing/src/test_tube/cw_admin_factory.rs @@ -0,0 +1,128 @@ +use cosmwasm_std::Coin; +use cw_admin_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 CwAdminFactory<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> CwAdminFactory<'a> { + pub fn new( + app: &'a OsmosisTestApp, + admin: Option, + 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, + &InstantiateMsg { admin }, + 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("cw_admin_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("cw_admin_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/dao_voting_cw4.rs b/packages/dao-testing/src/test_tube/dao_voting_cw4.rs new file mode 100644 index 000000000..ce0fdac33 --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_voting_cw4.rs @@ -0,0 +1,128 @@ +use cosmwasm_std::Coin; +use cw_admin_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 DaoVotingCw4<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> DaoVotingCw4<'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_voting_cw4.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_voting_cw4-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 index 0af2999a4..107ea403d 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -1,13 +1,16 @@ -// 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_admin_factory; + #[cfg(feature = "test-tube")] pub mod cw_tokenfactory_issuer; +#[cfg(feature = "test-tube")] +pub mod cw4_group; + #[cfg(feature = "test-tube")] pub mod cw721_base; @@ -19,3 +22,6 @@ pub mod dao_proposal_single; #[cfg(feature = "test-tube")] pub mod dao_test_custom_factory; + +#[cfg(feature = "test-tube")] +pub mod dao_voting_cw4; diff --git a/packages/dao-voting/src/pre_propose.rs b/packages/dao-voting/src/pre_propose.rs index 482bf9260..9384c971c 100644 --- a/packages/dao-voting/src/pre_propose.rs +++ b/packages/dao-voting/src/pre_propose.rs @@ -4,6 +4,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Empty, StdResult, SubMsg}; use dao_interface::state::ModuleInstantiateInfo; +use thiserror::Error; use crate::reply::pre_propose_module_instantiation_id; @@ -16,6 +17,9 @@ pub enum PreProposeInfo { ModuleMayPropose { info: ModuleInstantiateInfo }, } +/// The policy configured in a proposal module that determines whether or not a +/// pre-propose module is in use. If so, only the module can create new +/// proposals. Otherwise, there is no restriction on proposal creation. #[cw_serde] pub enum ProposalCreationPolicy { /// Anyone may create a proposal, free of charge. @@ -58,6 +62,78 @@ impl PreProposeInfo { } } +/// The policy configured in a pre-propose module that determines who can submit +/// proposals. This is the preferred way to restrict proposal creation (as +/// opposed to the ProposalCreationPolicy above) since pre-propose modules +/// support other features, such as proposal deposits. +#[cw_serde] +pub enum PreProposeSubmissionPolicy { + /// Anyone may create proposals, except for those in the denylist. + Anyone { + /// Addresses that may not create proposals. + denylist: Vec, + }, + /// Specific people may create proposals. + Specific { + /// Whether or not DAO members may create proposals. + dao_members: bool, + /// Addresses that may create proposals. + allowlist: Vec, + /// Addresses that may not create proposals, overriding other settings. + denylist: Vec, + }, +} + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum PreProposeSubmissionPolicyError { + #[error("The proposal submission policy doesn't allow anyone to submit proposals")] + NoOneAllowed {}, + + #[error("Denylist cannot contain addresses in the allowlist")] + DenylistAllowlistOverlap {}, + + #[error("You are not allowed to submit proposals")] + Unauthorized {}, + + #[error("The current proposal submission policy (Anyone) only supports a denylist. Change the policy to Specific in order to configure more granular permissions.")] + AnyoneInvalidUpdateFields {}, +} + +impl PreProposeSubmissionPolicy { + /// Validate the policy configuration. + pub fn validate(&self) -> Result<(), PreProposeSubmissionPolicyError> { + if let PreProposeSubmissionPolicy::Specific { + dao_members, + allowlist, + denylist, + } = self + { + // prevent allowlist and denylist from overlapping + if denylist.iter().any(|a| allowlist.iter().any(|b| a == b)) { + return Err(PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {}); + } + + // ensure someone is allowed to submit proposals, be it DAO members + // or someone on the allowlist. we can't verify that the denylist + // doesn't contain all DAO members, so this is the best we can do to + // ensure that someone is allowed to submit. + if !dao_members && allowlist.is_empty() { + return Err(PreProposeSubmissionPolicyError::NoOneAllowed {}); + } + } + + Ok(()) + } + + /// Human readable string for use in events. + pub fn human_readable(&self) -> String { + match self { + Self::Anyone { .. } => "anyone".to_string(), + Self::Specific { .. } => "specific".to_string(), + } + } +} + #[cfg(test)] mod tests { use cosmwasm_std::{to_json_binary, WasmMsg}; diff --git a/scripts/Cargo.lock b/scripts/Cargo.lock new file mode 100644 index 000000000..46ef4da9b --- /dev/null +++ b/scripts/Cargo.lock @@ -0,0 +1,4876 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "abstract-cw-multi-test" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c77f8d4bac08f74fbc4fce8943cb2d35e742682b6cae8cb65555d6cd3830feb" +dependencies = [ + "anyhow", + "bech32 0.11.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20-ics20", + "derivative", + "hex", + "itertools 0.12.1", + "log", + "prost 0.12.3", + "schemars", + "serde", + "serde_json", + "sha2 0.10.8", + "thiserror", +] + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base16" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27c3610c36aee21ce8ac510e6224498de4228ad772a171ed65643a24693a5a8" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[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 = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + +[[package]] +name = "bip32" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa13fae8b6255872fd86f7faf4b41168661d7d78609f7bfe6771b85c6739a15b" +dependencies = [ + "bs58", + "hmac", + "k256", + "rand_core 0.6.4", + "ripemd", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "bitcoin" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" +dependencies = [ + "bech32 0.9.1", + "bitcoin-private", + "bitcoin_hashes", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin_hashes" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +dependencies = [ + "bitcoin-private", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bnum" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56953345e39537a3e18bdaeba4cb0c58a78c1f61f361dc0fa7c5c7340ae87c5f" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.8", +] + +[[package]] +name = "btsg-ft-factory" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "dao-interface 2.5.0", + "osmosis-std-derive", + "prost 0.12.3", + "prost-derive 0.12.3", + "prost-types 0.12.3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cosmos-sdk-proto" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32560304ab4c365791fd307282f76637213d8083c1a98490c35159cd67852237" +dependencies = [ + "prost 0.12.3", + "prost-types 0.12.3", + "tendermint-proto 0.34.1", + "tonic", +] + +[[package]] +name = "cosmos-sdk-proto" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e23f6ab56d5f031cde05b8b82a5fefd3a1a223595c79e32317a97189e612bc" +dependencies = [ + "prost 0.12.3", + "prost-types 0.12.3", + "tendermint-proto 0.35.0", +] + +[[package]] +name = "cosmrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47126f5364df9387b9d8559dcef62e99010e1d4098f39eb3f7ee4b5c254e40ea" +dependencies = [ + "bip32", + "cosmos-sdk-proto 0.20.0", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature", + "subtle-encoding", + "tendermint", + "tendermint-rpc", + "thiserror", + "tokio", +] + +[[package]] +name = "cosmwasm-crypto" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd50718a2b6830ce9eb5d465de5a018a12e71729d66b70807ce97e6dd14f931d" +dependencies = [ + "digest 0.10.7", + "ecdsa", + "ed25519-zebra", + "k256", + "rand_core 0.6.4", + "thiserror", +] + +[[package]] +name = "cosmwasm-derive" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "242e98e7a231c122e08f300d9db3262d1007b51758a8732cd6210b3e9faa4f3a" +dependencies = [ + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-schema" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7879036156092ad1c22fe0d7316efc5a5eceec2bc3906462a2560215f2a2f929" +dependencies = [ + "cosmwasm-schema-derive", + "schemars", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cosmwasm-schema-derive" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb57855fbfc83327f8445ae0d413b1a05ac0d68c396ab4d122b2abd7bb82cb6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-std" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c1556156fdf892a55cced6115968b961eaaadd6f724a2c2cb7d1e168e32dd3" +dependencies = [ + "base64 0.21.7", + "bech32 0.9.1", + "bnum", + "cosmwasm-crypto", + "cosmwasm-derive", + "derivative", + "forward_ref", + "hex", + "schemars", + "serde", + "serde-json-wasm", + "sha2 0.10.8", + "static_assertions", + "thiserror", +] + +[[package]] +name = "cosmwasm-storage" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66de2ab9db04757bcedef2b5984fbe536903ada4a8a9766717a4a71197ef34f6" +dependencies = [ + "cosmwasm-std", + "serde", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + +[[package]] +name = "cw-address-like" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451a4691083a88a3c0630a8a88799e9d4cd6679b7ce8ff22b8da2873ff31d380" +dependencies = [ + "cosmwasm-std", +] + +[[package]] +name = "cw-admin-factory" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "thiserror", +] + +[[package]] +name = "cw-controllers" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f0bc6019b4d3d81e11f5c384bcce7173e2210bd654d75c6c9668e12cca05dfa" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-controllers" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57de8d3761e46be863e3ac1eba8c8a976362a48c6abf240df1e26c3e421ee9e8" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "468b8f2696f625c8e15b5468f9420c8eabfaf23cb4fd7e6c660fc7e0cc8d77b8" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-core-interface 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-core-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-paginate-storage 0.1.0", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw20 0.13.4", + "cw721 0.13.4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-core-interface" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c93e684945473777ebed2bcaf9f0af2291653f79d5c81774c6826350ba6d88de" +dependencies = [ + "cosmwasm-std", + "cw-core-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw2 0.13.4", + "schemars", + "serde", +] + +[[package]] +name = "cw-core-interface" +version = "0.1.0" +source = "git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0#e531c760a5d057329afd98d62567aaa4dca2c96f" +dependencies = [ + "cosmwasm-std", + "cw-core-macros 0.1.0 (git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0)", + "cw2 0.13.4", + "schemars", + "serde", +] + +[[package]] +name = "cw-core-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f20a77489d2dc8a1c12cb0b9671b6cbdca88f12fe65e1a4ee9899490f7669dcc" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cw-core-macros" +version = "0.1.0" +source = "git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0#e531c760a5d057329afd98d62567aaa4dca2c96f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cw-denom" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decaa8be7ffa8090dc62d8bb8ee97cd3f41f815a41ba08de1d40cacef6c3cb4b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw20 1.1.2", + "thiserror", +] + +[[package]] +name = "cw-denom" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw20 1.1.2", + "thiserror", +] + +[[package]] +name = "cw-fund-distributor" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-paginate-storage 2.5.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-stake 2.5.0", + "dao-interface 2.5.0", + "dao-voting-cw20-staked", + "thiserror", +] + +[[package]] +name = "cw-hooks" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "097ee97b99ecc90372eac3bcaf698d940a15f806f2ba1e1e901c729f6523e16e" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "thiserror", +] + +[[package]] +name = "cw-hooks" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "thiserror", +] + +[[package]] +name = "cw-orch" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c81cb500eb2f9be31a0f90c7ce66572ee4a790ffbae1c6b42ff2e3f9faf3479" +dependencies = [ + "anyhow", + "cosmrs", + "cosmwasm-std", + "cw-orch-contract-derive", + "cw-orch-core", + "cw-orch-daemon", + "cw-orch-fns-derive", + "cw-orch-mock", + "cw-orch-networks", + "cw-orch-traits", + "cw-utils 1.0.3", + "hex", + "log", + "schemars", + "serde", + "thiserror", + "tokio", +] + +[[package]] +name = "cw-orch-contract-derive" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc8ba75692fc7bd30e91c78fad2dc208a738e4e6ea26b232f9352c320e35543" +dependencies = [ + "convert_case", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "cw-orch-core" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5abd640f67f655411542a3c148769270c7a9e63d4097c2dc4a1f6edf23b7a9b4" +dependencies = [ + "abstract-cw-multi-test", + "anyhow", + "cosmos-sdk-proto 0.21.1", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "dirs", + "log", + "serde", + "serde_json", + "sha2 0.10.8", + "thiserror", +] + +[[package]] +name = "cw-orch-daemon" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e6d5033f1bf9d32a6d1b11dd8e9b9ad5cb673c3621def8fe63fa1310eda520" +dependencies = [ + "anyhow", + "async-recursion", + "base16", + "base64 0.22.1", + "bitcoin", + "chrono", + "cosmrs", + "cosmwasm-std", + "cw-orch-core", + "cw-orch-networks", + "cw-orch-traits", + "dirs", + "ed25519-dalek", + "eyre", + "file-lock", + "flate2", + "hex", + "hkd32", + "lazy_static", + "log", + "once_cell", + "prost 0.12.3", + "prost-types 0.12.3", + "rand_core 0.6.4", + "reqwest", + "ring", + "ripemd", + "schemars", + "serde", + "serde_json", + "sha2 0.10.8", + "thiserror", + "tokio", + "tonic", + "uid", +] + +[[package]] +name = "cw-orch-fns-derive" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e21b23116a0702f540d7fa3f16e8276682d860b589fed56259220ad59d768e" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cw-orch-mock" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57beb30d841bde79df51c9402741ef926ca8ef7ecd3570aa180074f767ac04d3" +dependencies = [ + "abstract-cw-multi-test", + "cosmwasm-std", + "cw-orch-core", + "cw-utils 1.0.3", + "log", + "serde", + "sha2 0.10.8", +] + +[[package]] +name = "cw-orch-networks" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b0e1af56869ced61c25f509a0a40cddd028779d838441b70f4578a8d7c0253" +dependencies = [ + "cw-orch-core", + "serde", +] + +[[package]] +name = "cw-orch-traits" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e6b81dc282724c9c6334a499f4867e575458e69fe5b99034d4f962860f3357" +dependencies = [ + "cw-orch-core", + "prost 0.12.3", + "prost-types 0.12.3", +] + +[[package]] +name = "cw-ownable" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093dfb4520c48b5848274dd88ea99e280a04bc08729603341c7fb0d758c74321" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-address-like", + "cw-ownable-derive", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "thiserror", +] + +[[package]] +name = "cw-ownable-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d3bf2e0f341bb6cc100d7d441d31cf713fbd3ce0c511f91e79f14b40a889af" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cw-paginate-storage" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b854833e07c557dee02d1b61a21bb0731743bb2e3bbdc3e446a0d8a38af40ec4" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus 0.13.4", + "serde", +] + +[[package]] +name = "cw-paginate-storage" +version = "2.5.0" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "serde", +] + +[[package]] +name = "cw-payroll-factory" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.5.0", + "cw-orch", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw-vesting", + "cw2 1.1.2", + "cw20 1.1.2", + "thiserror", +] + +[[package]] +name = "cw-proposal-single" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6408483e1ac17a7e2b98ef6fa1379776964353bcbf501942d22ee1c1323117" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-core", + "cw-core-interface 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-core-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw20 0.13.4", + "cw3", + "dao-voting 0.1.0", + "indexable-hooks", + "proposal-hooks", + "schemars", + "serde", + "thiserror", + "vote-hooks", +] + +[[package]] +name = "cw-stake-tracker" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-wormhole", +] + +[[package]] +name = "cw-storage-plus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648b1507290bbc03a8d88463d7cd9b04b1fa0155e5eef366c4fa052b9caaac7a" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-storage-plus" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b6f91c0b94481a3e9ef1ceb183c37d00764f8751e39b45fc09f4d9b970d469" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-storage-plus" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5ff29294ee99373e2cd5fd21786a3c0ced99a52fec2ca347d565489c61b723c" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-token-swap" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "thiserror", +] + +[[package]] +name = "cw-tokenfactory-issuer" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-tokenfactory-types", + "cw2 1.1.2", + "dao-interface 2.5.0", + "osmosis-std", + "prost 0.12.3", + "prost-derive 0.12.3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-tokenfactory-types" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "dao-interface 2.5.0", + "osmosis-std", + "osmosis-std-derive", + "prost 0.12.3", + "prost-derive 0.12.3", + "prost-types 0.12.3", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "cw-utils" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-utils" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a84c6c1c0acc3616398eba50783934bd6c964bad6974241eaee3460c8f5b26" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 0.16.0", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw-utils" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4a657e5caacc3a0d00ee96ca8618745d050b8f757c709babafb81208d4239c" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 1.1.2", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw-vesting" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.5.0", + "cw-orch", + "cw-ownable", + "cw-stake-tracker", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw-wormhole", + "cw2 1.1.2", + "cw20 1.1.2", + "serde", + "thiserror", + "wynd-utils", +] + +[[package]] +name = "cw-wormhole" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "serde", +] + +[[package]] +name = "cw2" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 0.13.4", + "schemars", + "serde", +] + +[[package]] +name = "cw2" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91398113b806f4d2a8d5f8d05684704a20ffd5968bf87e3473e1973710b884ad" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.16.0", + "schemars", + "serde", +] + +[[package]] +name = "cw2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c120b24fbbf5c3bedebb97f2cc85fbfa1c3287e09223428e7e597b5293c1fa" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw20" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cb782b8f110819a4eb5dbbcfed25ffba49ec16bbe32b4ad8da50a5ce68fec05" +dependencies = [ + "cosmwasm-std", + "cw-utils 0.13.4", + "schemars", + "serde", +] + +[[package]] +name = "cw20" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "schemars", + "serde", +] + +[[package]] +name = "cw20-base" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0306e606581f4fb45e82bcbb7f0333179ed53dd949c6523f01a99b4bfc1475a0" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw20 0.13.4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw20-base" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ad79e86ea3707229bf78df94e08732e8f713207b4a77b2699755596725e7d9" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "cw20 1.1.2", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw20-ics20" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76221201da08fed611c857ea3aa21c031a4a7dc771a8b1750559ca987335dc02" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw20-stake" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f0d51ce27a97b51f66d737183845bc6d82f46f4b246dc959d1265d86906ccc" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers 0.13.4", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw20 0.13.4", + "cw20-base 0.13.4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw20-stake" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-hooks 2.5.0", + "cw-orch", + "cw-ownable", + "cw-paginate-storage 2.5.0", + "cw-storage-plus 1.2.0", + "cw-utils 0.13.4", + "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.5.0", + "thiserror", +] + +[[package]] +name = "cw20-stake-external-rewards" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-orch", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 0.13.4", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 2.5.0", + "dao-hooks", + "stake-cw20-external-rewards", + "thiserror", +] + +[[package]] +name = "cw20-stake-reward-distributor" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-ownable", + "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.5.0", + "stake-cw20-reward-distributor", + "thiserror", +] + +[[package]] +name = "cw20-staked-balance-voting" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf8c2ee92372d35c3a48fd6ddd490a1a4426902748017dd0b7f551d06484e28" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-core-interface 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-core-macros 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw20 0.13.4", + "cw20-base 0.13.4", + "cw20-stake 0.2.6", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw3" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" +dependencies = [ + "cosmwasm-std", + "cw-utils 0.13.4", + "schemars", + "serde", +] + +[[package]] +name = "cw4" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 0.13.4", + "schemars", + "serde", +] + +[[package]] +name = "cw4" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24754ff6e45f2a1c60adc409d9b2eb87666012c44021329141ffaab3388fccd2" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "schemars", + "serde", +] + +[[package]] +name = "cw4-group" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c95c89153e7831c8306c8eba40a3daa76f9c7b8f5179dd0b8628aca168ec7a" +dependencies = [ + "cosmwasm-std", + "cw-controllers 0.13.4", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw4 0.13.4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4-group" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e24a22c3af54c52edf528673b420a67a1648be2c159b8ec778d2fbf543df24b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "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", +] + +[[package]] +name = "cw4-voting" +version = "0.1.0" +source = "git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0#e531c760a5d057329afd98d62567aaa4dca2c96f" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-core-interface 0.1.0 (git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0)", + "cw-core-macros 0.1.0 (git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0)", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw4 0.13.4", + "cw4-group 0.13.4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw721" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "035818368a74c07dd9ed5c5a93340199ba251530162010b9f34c3809e3b97df1" +dependencies = [ + "cosmwasm-std", + "cw-utils 0.13.4", + "schemars", + "serde", +] + +[[package]] +name = "cw721" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a1ea6e6277bdd6dfc043a9b1380697fe29d6e24b072597439523658d21d791" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 0.16.0", + "schemars", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77518e27431d43214cff4cdfbd788a7508f68d9b1f32389e6fce513e7eaccbef" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", + "cw2 0.16.0", + "cw721 0.16.0", + "schemars", + "serde", + "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.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "thiserror", +] + +[[package]] +name = "cw721-roles" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-orch", + "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", + "serde", + "thiserror", +] + +[[package]] +name = "dao-cw-orch" +version = "2.5.0" +dependencies = [ + "btsg-ft-factory", + "cosmwasm-std", + "cw-admin-factory", + "cw-fund-distributor", + "cw-orch", + "cw-payroll-factory", + "cw-token-swap", + "cw-tokenfactory-issuer", + "cw-vesting", + "cw20-stake 2.5.0", + "cw20-stake-external-rewards", + "cw20-stake-reward-distributor", + "cw721-base 0.18.0", + "cw721-roles", + "dao-dao-core", + "dao-interface 2.5.0", + "dao-migrator", + "dao-pre-propose-approval-single", + "dao-pre-propose-approver", + "dao-pre-propose-multiple", + "dao-pre-propose-single", + "dao-proposal-condorcet", + "dao-proposal-hook-counter", + "dao-proposal-multiple", + "dao-proposal-single", + "dao-proposal-sudo", + "dao-rewards-distributor", + "dao-test-custom-factory", + "dao-voting-cw20-balance", + "dao-voting-cw20-staked", + "dao-voting-cw4", + "dao-voting-cw721-roles", + "dao-voting-cw721-staked", + "dao-voting-token-staked", + "gauge-adapter", + "gauge-orchestrator", + "serde", +] + +[[package]] +name = "dao-cw721-extensions" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-orch", + "cw4 1.1.2", +] + +[[package]] +name = "dao-dao-core" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-core", + "cw-paginate-storage 2.5.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw721 0.18.0", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-dao-macros" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c3f39b7aaf9d913d0de8c8742c151011da00662acdbe95c33a5f7bad1b835a" +dependencies = [ + "cosmwasm-schema", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dao-dao-macros" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dao-hooks" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-hooks 2.5.0", + "cw4 1.1.2", + "dao-pre-propose-base 2.5.0", + "dao-voting 2.5.0", +] + +[[package]] +name = "dao-interface" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e546da3e3d9c3b9f607c40256561f0424715d0aef840df9d37669b55d8590e1a" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw721 0.18.0", + "osmosis-std", +] + +[[package]] +name = "dao-interface" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "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.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-core", + "cw-core-interface 0.1.0 (git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0)", + "cw-orch", + "cw-proposal-single", + "cw-storage-plus 1.2.0", + "cw-utils 0.13.4", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 0.13.4", + "cw20 1.1.2", + "cw20-base 1.1.2", + "cw20-stake 0.2.6", + "cw20-stake 2.5.0", + "cw20-staked-balance-voting", + "cw4 0.13.4", + "cw4-voting", + "dao-dao-core", + "dao-interface 2.5.0", + "dao-proposal-single", + "dao-voting 0.1.0", + "dao-voting 2.5.0", + "dao-voting-cw20-staked", + "dao-voting-cw4", + "thiserror", +] + +[[package]] +name = "dao-pre-propose-approval-single" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-paginate-storage 2.5.0", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.5.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-pre-propose-approver" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-interface 2.5.0", + "dao-pre-propose-approval-single", + "dao-pre-propose-base 2.5.0", + "dao-voting 2.5.0", +] + +[[package]] +name = "dao-pre-propose-base" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd285523d7dea35a0dd76f0a5f20c190935922a7b58fe0ec753eb407e68d718b" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.4.1", + "cw-hooks 2.4.2", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-interface 2.4.2", + "dao-voting 2.4.1", + "serde", + "thiserror", +] + +[[package]] +name = "dao-pre-propose-base" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.4.1", + "cw-denom 2.5.0", + "cw-hooks 2.5.0", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.4.1", + "dao-voting 2.4.1", + "dao-voting 2.5.0", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "dao-pre-propose-multiple" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 1.1.2", + "dao-pre-propose-base 2.5.0", + "dao-voting 2.5.0", +] + +[[package]] +name = "dao-pre-propose-single" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2 1.1.2", + "dao-pre-propose-base 2.5.0", + "dao-voting 2.5.0", +] + +[[package]] +name = "dao-proposal-condorcet" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-proposal-hook-counter" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "dao-hooks", + "thiserror", +] + +[[package]] +name = "dao-proposal-multiple" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-hooks 2.5.0", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "dao-dao-macros 2.5.0", + "dao-hooks", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.5.0", + "dao-pre-propose-multiple", + "dao-voting 0.1.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-proposal-single" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-hooks 2.5.0", + "cw-orch", + "cw-proposal-single", + "cw-storage-plus 1.2.0", + "cw-utils 0.13.4", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "dao-dao-macros 2.5.0", + "dao-hooks", + "dao-interface 2.5.0", + "dao-pre-propose-base 2.5.0", + "dao-voting 0.1.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-proposal-sudo" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw2 1.1.2", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-rewards-distributor" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-orch", + "cw-ownable", + "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 1.1.2", + "dao-hooks", + "dao-interface 2.5.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-test-custom-factory" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "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 2.5.0", + "dao-interface 2.5.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-voting" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442d770933e3b3ecab4cfb4d6e9d054082b007d35fda3cf0c3d3ddd1cfa91782" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "dao-voting" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945898e8e168eada7ed06fa713d679e541673ee0dd8c70aee8d1f224ccd031a0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.4.1", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "dao-dao-macros 2.4.2", + "dao-interface 2.4.2", + "thiserror", +] + +[[package]] +name = "dao-voting" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.5.0", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw20 1.1.2", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-voting-cw20-balance" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "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 2.5.0", + "dao-interface 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-voting-cw20-staked" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "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.5.0", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-voting-cw4" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "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 2.5.0", + "dao-interface 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-voting-cw721-roles" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-orch", + "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", + "cw721-controllers", + "dao-cw721-extensions", + "dao-dao-macros 2.5.0", + "dao-interface 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-voting-cw721-staked" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-hooks 2.5.0", + "cw-orch", + "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 2.5.0", + "dao-hooks", + "dao-interface 2.5.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "dao-voting-token-staked" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 1.1.2", + "cw-hooks 2.5.0", + "cw-orch", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-tokenfactory-issuer", + "cw-utils 1.0.3", + "cw2 1.1.2", + "dao-dao-macros 2.5.0", + "dao-hooks", + "dao-interface 2.5.0", + "dao-voting 2.5.0", + "thiserror", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519", + "serde", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek 3.2.0", + "hashbrown 0.12.3", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[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 = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "file-lock" +version = "2.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "040b48f80a749da50292d0f47a1e2d5bf1d772f52836c07f64bfccc62ba6e664" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "flate2" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "eyre", + "paste", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "forward_ref" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "gauge-adapter" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.5.0", + "cw-orch", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "semver", + "thiserror", +] + +[[package]] +name = "gauge-orchestrator" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw4 1.1.2", + "dao-hooks", + "dao-interface 2.5.0", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hkd32" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e013a4f0b8772418eee1fc462e74017aba13c364a7b61bd3df1ddcbfe47b065" +dependencies = [ + "hmac", + "once_cell", + "pbkdf2", + "rand_core 0.6.4", + "sha2 0.10.8", + "subtle-encoding", + "zeroize", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexable-hooks" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d70922e1e0e68d99ec1a24446c70756cc3e56deaddb505b1f4b43914522d809" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus 0.13.4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.8", + "signature", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[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 0.11.9", + "schemars", + "serde", + "serde-cw-value", +] + +[[package]] +name = "osmosis-std-derive" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ebdfd1bc8ed04db596e110c6baa9b174b04f6ed1ec22c666ddc5cb3fa91bd7" +dependencies = [ + "itertools 0.10.5", + "proc-macro2", + "prost-types 0.11.9", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "peg" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty_env_logger" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865724d4dbe39d9f3dd3b52b88d859d66bcb2d6a0acfd5ea68a65fb66d4bdc1c" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proposal-hooks" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9a2f15b848398bad689771b35313c7e7095e772d444e299dbdb54b906691f8a" +dependencies = [ + "cosmwasm-std", + "indexable-hooks", + "schemars", + "serde", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive 0.11.9", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive 0.12.3", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "prost-types" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" +dependencies = [ + "prost 0.11.9", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost 0.12.3", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[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.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.72", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scripts" +version = "2.5.0" +dependencies = [ + "btsg-ft-factory", + "cw-admin-factory", + "cw-orch", + "cw-payroll-factory", + "cw-token-swap", + "cw-tokenfactory-issuer", + "cw-vesting", + "cw721-roles", + "dao-cw-orch", + "dao-interface 2.5.0", + "dao-migrator", + "dao-proposal-single", + "dao-proposal-sudo", + "dao-voting 2.5.0", + "dotenv", + "pretty_env_logger", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a129b9e9efbfb223753b9163c4ab3b13cff7fd9c7f010fbac25ab4099fa07e" +dependencies = [ + "cc", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-cw-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d32da6b8ed758b7d850b6c3c08f1d7df51a4df3cb201296e63e34a78e99d4" +dependencies = [ + "serde", +] + +[[package]] +name = "serde-json-wasm" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9213a07d53faa0b8dd81e767a54a8188a242fdb9be99ab75ec576a774bfdd7" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stake-cw20-external-rewards" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9bbc1e4b7a932957a05a76921015a849b234c3f25e59fe1fd0d2eab71654bc" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers 0.13.4", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw20 0.13.4", + "cw20-base 0.13.4", + "cw20-stake 0.2.6", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "stake-cw20-reward-distributor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4260ff7aec6dddb43cb5f1104ef5cebe2787853bc83af9172ce5b828b577c4c5" +dependencies = [ + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus 0.13.4", + "cw-utils 0.13.4", + "cw2 0.13.4", + "cw20 0.13.4", + "cw20-base 0.13.4", + "cw20-stake 0.2.6", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendermint" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15ab8f0a25d0d2ad49ac615da054d6a76aa6603ff95f7d18bafdd34450a1a04b" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost 0.12.3", + "prost-types 0.12.3", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", + "subtle", + "subtle-encoding", + "tendermint-proto 0.34.1", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-config" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1a02da769166e2052cd537b1a97c78017632c2d9e19266367b27e73910434fc" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint", + "toml", + "url", +] + +[[package]] +name = "tendermint-proto" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b797dd3d2beaaee91d2f065e7bdf239dc8d80bba4a183a288bc1279dd5a69a1e" +dependencies = [ + "bytes", + "flex-error", + "num-derive 0.3.3", + "num-traits", + "prost 0.12.3", + "prost-types 0.12.3", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-proto" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff525d5540a9fc535c38dc0d92a98da3ee36fcdfbda99cecb9f3cce5cd4d41d7" +dependencies = [ + "bytes", + "flex-error", + "num-derive 0.4.2", + "num-traits", + "prost 0.12.3", + "prost-types 0.12.3", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-rpc" +version = "0.34.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71afae8bb5f6b14ed48d4e1316a643b6c2c3cbad114f510be77b4ed20b7b3e42" +dependencies = [ + "async-trait", + "bytes", + "flex-error", + "futures", + "getrandom", + "peg", + "pin-project", + "rand", + "reqwest", + "semver", + "serde", + "serde_bytes", + "serde_json", + "subtle", + "subtle-encoding", + "tendermint", + "tendermint-config", + "tendermint-proto 0.34.1", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d040ac2b29ab03b09d4129c2f5bbd012a3ac2f79d38ff506a4bf8dd34b0eac8a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tonic" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.21.7", + "bytes", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost 0.12.3", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "tokio", + "tokio-rustls", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uid" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "281a11e4180ee68f8c06a7bf82d1be3c496e0debcadcc4576cc0b56132201edf" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vote-hooks" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef617ad17edd195f8a3bce72498bfcc406a27cecfc23828f562fa91a3e2fb141" +dependencies = [ + "cosmwasm-std", + "indexable-hooks", + "schemars", + "serde", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.72", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.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.6", +] + +[[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.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wynd-utils" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daa37b3fba808df599acc6f0d7523b465baf47a0b0361867c4f1635eb53f72aa" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] diff --git a/scripts/Cargo.toml b/scripts/Cargo.toml new file mode 100644 index 000000000..876753f5f --- /dev/null +++ b/scripts/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "scripts" +edition = "2021" +license = "BSD-3-Clause" +repository = "https://github.com/DA0-DA0/dao-contracts" +version = "2.5.0" + +[dependencies] +cw-orch = { version = "0.24.1", features = ["daemon"] } +cosmwasm-std = { version = "1.5.4", features = ["ibc3", "cosmwasm_1_1"] } +cw-utils = "1.0" +cw20 = "1.1" +cw4-group = "1.1" +cw4 = "1.1" +dao-cw-orch = { path = "../packages/cw-orch", version = "2.5.0" } +dao-hooks = { path = "../packages/dao-hooks", version = "2.5.0" } +# scripts specific +dotenv = { version = "0.15.0" } +pretty_env_logger = { version = "0.5.0" } + +# cw-orch enabled DAO DAO deps +[dev-dependencies] +abstract-cw-plus-interface = "2.0.1" +abstract-cw20 = "2.0.0" +abstract-cw20-base = "2.0.0" +cw-denom = { path = "../packages/cw-denom" } +cw-orch-core = "1.2.2" + +dao-proposal-sudo = { path = "../contracts/test/dao-proposal-sudo", features = [ + "library", +] } +dao-proposal-single = { path = "../contracts/proposal/dao-proposal-single", features = [ + "library", +] } +dao-interface = { path = "../packages/dao-interface" } +dao-voting = { path = "../packages/dao-voting" } + +# external contracts +btsg-ft-factory = { path = "../contracts/external/btsg-ft-factory" } +cw-payroll-factory = { path = "../contracts/external/cw-payroll-factory" } +cw-token-swap = { path = "../contracts/external/cw-token-swap" } +cw-admin-factory = { path = "../contracts/external/cw-admin-factory" } +cw-tokenfactory-issuer = { path = "../contracts/external/cw-tokenfactory-issuer" } +cw-vesting = { path = "../contracts/external/cw-vesting" } +cw721-roles = { path = "../contracts/external/cw721-roles" } + +dao-migrator = { path = "../contracts/external/dao-migrator" } +dao-voting-cw4 = { path = "../contracts/voting/dao-voting-cw4" } + +# gauge contracts +gauge-orchestrator = { path = "../contracts/gauges/gauge" } +gauge-adapter = { path = "../contracts/gauges/gauge-adapter" } +dao-gauge-adapter = { path = "../contracts/test/dao-gauge-adapter" } \ No newline at end of file diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..0aac055bb --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,9 @@ +# DAO DAO Scripts +This package contains the integration test suites and scripts to deploy and run actions, via Abstract's [Cw-Orchestrator](https://github.com/AbstractSDK/cw-orchestrator). + + +| Folder | Description | +|----------|--------------| +| `bin` | Rust scripts for production environment deployment and actions | +| `deploy` | Deploy trait implementations | +| `test` | All Integration tests | \ No newline at end of file diff --git a/scripts/publish.sh b/scripts/publish.sh old mode 100644 new mode 100755 index 9b42ab909..4eb17e1e6 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -29,6 +29,7 @@ START_DIR=$(pwd) # We start with publish packages, aside from dao-testing which must be published last. # Packages + cd packages/cw-denom cargo publish cd "$START_DIR" @@ -49,8 +50,6 @@ cd packages/cw-paginate-storage cargo publish cd "$START_DIR" -sleep 120 - cd packages/cw721-controllers cargo publish cd "$START_DIR" @@ -59,6 +58,8 @@ cd packages/dao-cw721-extensions cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" +sleep 120 + cd packages/dao-interface cargo publish cd "$START_DIR" @@ -67,6 +68,10 @@ cd packages/dao-dao-macros cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" +cd packages/dao-pre-propose-base +cargo publish +cd "$START_DIR" + cd packages/dao-voting cargo publish cd "$START_DIR" @@ -75,17 +80,18 @@ cd packages/dao-hooks cargo publish cd "$START_DIR" -sleep 120 - -cd packages/dao-pre-propose-base +cd packages/cw-tokenfactory-types cargo publish cd "$START_DIR" -Test contracts +# Test contracts + cd contracts/test/dao-proposal-sudo cargo publish cd "$START_DIR" +sleep 120 + cd contracts/test/dao-voting-cw20-balance cargo publish cd "$START_DIR" @@ -94,9 +100,8 @@ 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" @@ -117,6 +122,8 @@ cd contracts/external/cw-payroll-factory cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" +sleep 120 + cd contracts/external/cw721-roles cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" @@ -125,8 +132,6 @@ cd contracts/pre-propose/dao-pre-propose-single cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" -sleep 120 - cd contracts/pre-propose/dao-pre-propose-multiple cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" @@ -169,8 +174,6 @@ cd contracts/voting/dao-voting-cw4 cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" -sleep 120 - cd contracts/voting/dao-voting-cw20-staked cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" @@ -179,6 +182,8 @@ cd contracts/voting/dao-voting-cw721-roles cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" +sleep 120 + cd contracts/voting/dao-voting-cw721-staked cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" @@ -187,6 +192,10 @@ cd contracts/voting/dao-voting-token-staked cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" +cd contracts/voting/dao-voting-onft-staked +cargo hack publish --no-dev-deps --allow-dirty +cd "$START_DIR" + cd contracts/dao-dao-core cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" @@ -195,17 +204,24 @@ cd contracts/external/cw-admin-factory cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" -sleep 120 - -# TODO re-enable when ready -# cd contracts/external/cw-fund-distributor +# TODO: uncomment once cleaned up and audited +# cd contracts/distribution/cw-fund-distributor # cargo hack publish --no-dev-deps --allow-dirty # cd "$START_DIR" -cd contracts/external/dao-migrator +cd contracts/distribution/dao-rewards-distributor +cargo hack publish --no-dev-deps --allow-dirty +cd "$START_DIR" + +sleep 120 + +cd contracts/external/btsg-ft-factory cargo hack publish --no-dev-deps --allow-dirty cd "$START_DIR" +cd contracts/external/dao-migrator +cargo hack publish --no-dev-deps --allow-dirty +cd "$START_DIR" cd packages/dao-testing cargo publish diff --git a/scripts/src/dao.rs b/scripts/src/dao.rs new file mode 100644 index 000000000..539c17fd5 --- /dev/null +++ b/scripts/src/dao.rs @@ -0,0 +1,43 @@ +use cw_orch::prelude::*; +use dao_cw_orch::DaoDaoCore; + +use crate::{ + DaoDistributionSuite, DaoExternalSuite, DaoProposalSuite, DaoStakingSuite, DaoVotingSuite, + GaugeSuite, +}; + +// full dao suite +pub struct DaoDao { + pub dao_core: DaoDaoCore, + pub proposal_suite: DaoProposalSuite, + pub voting_suite: DaoVotingSuite, + pub staking_suite: DaoStakingSuite, + pub distribution_suite: DaoDistributionSuite, + pub gauge_suite: GaugeSuite, + pub external_suite: DaoExternalSuite, +} + +impl DaoDao { + pub fn new(chain: Chain) -> DaoDao { + DaoDao:: { + dao_core: DaoDaoCore::new("dao_dao_core", chain.clone()), + proposal_suite: DaoProposalSuite::new(chain.clone()), + voting_suite: DaoVotingSuite::new(chain.clone()), + staking_suite: DaoStakingSuite::new(chain.clone()), + distribution_suite: DaoDistributionSuite::new(chain.clone()), + gauge_suite: GaugeSuite::new(chain.clone()), + external_suite: DaoExternalSuite::new(chain.clone()), + } + } + + pub fn upload(&self) -> Result<(), CwOrchError> { + self.dao_core.upload()?; + self.proposal_suite.upload()?; + self.voting_suite.upload()?; + self.staking_suite.upload()?; + self.distribution_suite.upload()?; + self.gauge_suite.upload()?; + self.external_suite.upload()?; + Ok(()) + } +} diff --git a/scripts/src/deploy/dao.rs b/scripts/src/deploy/dao.rs new file mode 100644 index 000000000..94d579bfe --- /dev/null +++ b/scripts/src/deploy/dao.rs @@ -0,0 +1,42 @@ +use crate::DaoDao; +use cw_orch::prelude::*; +// distribution suite +impl cw_orch::contract::Deploy for DaoDao { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let suite = DaoDao::new(chain.clone()); + suite.upload()?; + Ok(suite) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + let mut cs: Vec>> = vec![]; + let res: Vec>> = vec![Box::new(&mut self.dao_core)]; + + cs.extend(res); + cs.extend(self.distribution_suite.get_contracts_mut()); + cs.extend(self.proposal_suite.get_contracts_mut()); + cs.extend(self.staking_suite.get_contracts_mut()); + cs.extend(self.voting_suite.get_contracts_mut()); + cs.extend(self.external_suite.get_contracts_mut()); + cs + } + + fn load_from(chain: Chain) -> Result { + let suite = Self::new(chain.clone()); + Ok(suite) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: DaoDao = DaoDao::store_on(chain.clone())?; + Ok(suite) + } +} diff --git a/scripts/src/deploy/distribution.rs b/scripts/src/deploy/distribution.rs new file mode 100644 index 000000000..b7abe273f --- /dev/null +++ b/scripts/src/deploy/distribution.rs @@ -0,0 +1,37 @@ +use crate::DaoDistributionSuite; +use cw_orch::prelude::*; + +// distribution suite +impl cw_orch::contract::Deploy for DaoDistributionSuite { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let suite = DaoDistributionSuite::new(chain.clone()); + suite.upload()?; + Ok(suite) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + vec![ + Box::new(&mut self.fund_distr), + Box::new(&mut self.reward_distr), + ] + } + + fn load_from(chain: Chain) -> Result { + let suite = Self::new(chain.clone()); + Ok(suite) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: DaoDistributionSuite = DaoDistributionSuite::store_on(chain.clone())?; + Ok(suite) + } +} diff --git a/scripts/src/deploy/external.rs b/scripts/src/deploy/external.rs new file mode 100644 index 000000000..d09e2e8d3 --- /dev/null +++ b/scripts/src/deploy/external.rs @@ -0,0 +1,43 @@ +use crate::external::*; +use cw_orch::prelude::*; + +// admin factory +impl cw_orch::contract::Deploy for DaoExternalSuite { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let suite = DaoExternalSuite::new(chain.clone()); + suite.upload()?; + Ok(suite) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + vec![ + Box::new(&mut self.admin_factory), + Box::new(&mut self.btsg_ft_factory), + Box::new(&mut self.payroll_factory), + Box::new(&mut self.cw_tokenswap), + Box::new(&mut self.cw_tokenfactory_issuer), + Box::new(&mut self.cw_vesting), + Box::new(&mut self.cw721_roles), + Box::new(&mut self.migrator), + ] + } + + fn load_from(chain: Chain) -> Result { + let suite = Self::new(chain.clone()); + Ok(suite) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: DaoExternalSuite = DaoExternalSuite::store_on(chain.clone())?; + Ok(suite) + } +} diff --git a/scripts/src/deploy/gauges/gauges.rs b/scripts/src/deploy/gauges/gauges.rs new file mode 100644 index 000000000..1084ad0f6 --- /dev/null +++ b/scripts/src/deploy/gauges/gauges.rs @@ -0,0 +1,78 @@ +use crate::gauges::*; +use cw_orch::prelude::*; + +// DAO-cw4 w/ gauges +impl cw_orch::contract::Deploy for DaoDaoCw4Gauge { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let gauge = DaoDaoCw4Gauge::new(chain); + gauge.upload()?; + Ok(gauge) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + let mut cs: Vec>> = vec![]; + let res: Vec>> = vec![ + Box::new(&mut self.dao_core), + Box::new(&mut self.prop_single), + Box::new(&mut self.cw4_vote), + ]; + + cs.extend(res); + cs.extend(self.gauge_suite.get_contracts_mut()); + cs + } + + fn load_from(chain: Chain) -> Result { + let gauge = Self::new(chain.clone()); + Ok(gauge) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: DaoDaoCw4Gauge = DaoDaoCw4Gauge::store_on(chain.clone())?; + Ok(suite) + } +} + +// Gauge Suite +impl cw_orch::contract::Deploy for GaugeSuite { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let gauge = GaugeSuite::new(chain.clone()); + gauge.upload()?; + Ok(gauge) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + vec![ + Box::new(&mut self.orchestrator), + Box::new(&mut self.adapter), + ] + } + + fn load_from(chain: Chain) -> Result { + let gauge = Self::new(chain.clone()); + Ok(gauge) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: GaugeSuite = GaugeSuite::store_on(chain.clone())?; + Ok(suite) + } +} \ No newline at end of file diff --git a/scripts/src/deploy/gauges/mod.rs b/scripts/src/deploy/gauges/mod.rs new file mode 100644 index 000000000..f4be8ead0 --- /dev/null +++ b/scripts/src/deploy/gauges/mod.rs @@ -0,0 +1 @@ +mod gauges; \ No newline at end of file diff --git a/scripts/src/deploy/mod.rs b/scripts/src/deploy/mod.rs new file mode 100644 index 000000000..06e5c0983 --- /dev/null +++ b/scripts/src/deploy/mod.rs @@ -0,0 +1,7 @@ +mod distribution; +mod dao; +mod external; +mod propose; +mod staking; +mod voting; +mod gauges; diff --git a/scripts/src/deploy/propose.rs b/scripts/src/deploy/propose.rs new file mode 100644 index 000000000..0c17d9c76 --- /dev/null +++ b/scripts/src/deploy/propose.rs @@ -0,0 +1,80 @@ +use crate::propose::*; +use cw_orch::prelude::*; + +// pre-proposal suite +impl cw_orch::contract::Deploy for DaoPreProposeSuite { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let suite = DaoPreProposeSuite::new(chain.clone()); + suite.upload()?; + Ok(suite) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + vec![ + Box::new(&mut self.pre_prop_approval_single), + Box::new(&mut self.pre_prop_approver), + Box::new(&mut self.pre_prop_multiple), + Box::new(&mut self.pre_prop_single), + ] + } + + fn load_from(chain: Chain) -> Result { + let factory = Self::new(chain.clone()); + Ok(factory) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: DaoPreProposeSuite = DaoPreProposeSuite::store_on(chain.clone())?; + Ok(suite) + } +} + +// proposal suite +impl cw_orch::contract::Deploy for DaoProposalSuite { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let suite = DaoProposalSuite::new(chain.clone()); + suite.upload()?; + Ok(suite) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + let mut boxs = vec![]; + let prop: Vec>> = vec![ + Box::new(&mut self.prop_single), + Box::new(&mut self.prop_multiple), + Box::new(&mut self.prop_condocert), + ]; + + boxs.extend(prop); + boxs.extend(self.pre_prop_suite.get_contracts_mut()); + boxs + } + + fn load_from(chain: Chain) -> Result { + let factory = Self::new(chain.clone()); + Ok(factory) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: DaoProposalSuite = DaoProposalSuite::store_on(chain.clone())?; + Ok(suite) + } +} diff --git a/scripts/src/deploy/staking.rs b/scripts/src/deploy/staking.rs new file mode 100644 index 000000000..f3f311460 --- /dev/null +++ b/scripts/src/deploy/staking.rs @@ -0,0 +1,38 @@ +use crate::staking::*; +use cw_orch::prelude::*; + +// staking suite +impl cw_orch::contract::Deploy for DaoStakingSuite { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let suite = DaoStakingSuite::new(chain.clone()); + suite.upload()?; + Ok(suite) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + vec![ + Box::new(&mut self.cw20_stake), + Box::new(&mut self.exteral_rewards), + Box::new(&mut self.rewards_distributor), + ] + } + + fn load_from(chain: Chain) -> Result { + let factory = Self::new(chain.clone()); + Ok(factory) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: DaoStakingSuite = DaoStakingSuite::store_on(chain.clone())?; + Ok(suite) + } +} diff --git a/scripts/src/deploy/voting.rs b/scripts/src/deploy/voting.rs new file mode 100644 index 000000000..c64185de3 --- /dev/null +++ b/scripts/src/deploy/voting.rs @@ -0,0 +1,40 @@ +use crate::voting::*; +use cw_orch::prelude::*; + +// staking suite +impl cw_orch::contract::Deploy for DaoVotingSuite { + // We don't have a custom error type + type Error = CwOrchError; + type DeployData = Addr; + + fn store_on(chain: Chain) -> Result { + let suite = DaoVotingSuite::new(chain.clone()); + suite.upload()?; + Ok(suite) + } + + fn deployed_state_file_path() -> Option { + None + } + + fn get_contracts_mut(&mut self) -> Vec>> { + vec![ + Box::new(&mut self.voting_cw4), + Box::new(&mut self.voting_cw20_staked), + Box::new(&mut self.voting_cw721_roles), + Box::new(&mut self.voting_cw721_staked), + Box::new(&mut self.voting_token_staked), + ] + } + + fn load_from(chain: Chain) -> Result { + let factory = Self::new(chain.clone()); + Ok(factory) + } + + fn deploy_on(chain: Chain, _data: Self::DeployData) -> Result { + // ########### Upload ############## + let suite: DaoVotingSuite = DaoVotingSuite::store_on(chain.clone())?; + Ok(suite) + } +} diff --git a/scripts/src/distribution.rs b/scripts/src/distribution.rs new file mode 100644 index 000000000..81a7580a6 --- /dev/null +++ b/scripts/src/distribution.rs @@ -0,0 +1,23 @@ +use cw_orch::prelude::*; +use dao_cw_orch::*; + +// cw-funds-distributor +pub struct DaoDistributionSuite { + pub fund_distr: DaoFundsDistributor, + pub reward_distr: DaoRewardsDistributor, +} + +impl DaoDistributionSuite { + pub fn new(chain: Chain) -> DaoDistributionSuite { + DaoDistributionSuite:: { + fund_distr: DaoFundsDistributor::new("cw_funds_distributor", chain.clone()), + reward_distr: DaoRewardsDistributor::new("dao_rewards_distributor", chain.clone()), + } + } + + pub fn upload(&self) -> Result<(), CwOrchError> { + self.fund_distr.upload()?; + self.reward_distr.upload()?; + Ok(()) + } +} diff --git a/scripts/src/external.rs b/scripts/src/external.rs new file mode 100644 index 000000000..b66c00ae1 --- /dev/null +++ b/scripts/src/external.rs @@ -0,0 +1,45 @@ +use cw_orch::prelude::*; +use dao_cw_orch::*; + +// external suite +pub struct DaoExternalSuite { + pub admin_factory: DaoExternalAdminFactory, + pub btsg_ft_factory: DaoExternalFantokenFactory, + pub payroll_factory: DaoExternalPayrollFactory, + pub cw_tokenswap: DaoExternalTokenSwap, + pub cw_tokenfactory_issuer: DaoExternalTokenfactoryIssuer, + pub cw_vesting: DaoExternalCwVesting, + pub cw721_roles: DaoExternalCw721Roles, + pub migrator: DaoExternalMigrator, +} + +impl DaoExternalSuite { + pub fn new(chain: Chain) -> DaoExternalSuite { + DaoExternalSuite:: { + admin_factory: DaoExternalAdminFactory::new("cw_admin_factory", chain.clone()), + btsg_ft_factory: DaoExternalFantokenFactory::new("btsg_ft_factory", chain.clone()), + payroll_factory: DaoExternalPayrollFactory::new("cw_payroll", chain.clone()), + cw_tokenswap: DaoExternalTokenSwap::new("cw_tokenswap", chain.clone()), + cw_tokenfactory_issuer: DaoExternalTokenfactoryIssuer::new( + "cw_tokenfactory", + chain.clone(), + ), + cw_vesting: DaoExternalCwVesting::new("cw_vesting", chain.clone()), + cw721_roles: DaoExternalCw721Roles::new("cw721_roles", chain.clone()), + migrator: DaoExternalMigrator::new("dao_migrator", chain.clone()), + } + } + + pub fn upload(&self) -> Result<(), CwOrchError> { + self.admin_factory.upload()?; + self.btsg_ft_factory.upload()?; + self.payroll_factory.upload()?; + self.cw_tokenswap.upload()?; + self.cw_tokenfactory_issuer.upload()?; + self.cw_vesting.upload()?; + self.cw721_roles.upload()?; + self.migrator.upload()?; + + Ok(()) + } +} \ No newline at end of file diff --git a/scripts/src/gauges.rs b/scripts/src/gauges.rs new file mode 100644 index 000000000..70c3ee6ad --- /dev/null +++ b/scripts/src/gauges.rs @@ -0,0 +1,57 @@ +use cw_orch::prelude::*; +use dao_cw_orch::{ + DaoDaoCore, DaoGaugeAdapter, DaoGaugeAdapterGeneric, DaoGaugeOrchestrator, DaoProposalSingle, + DaoVotingCw4, +}; + +// gauge suite +pub struct GaugeSuite { + pub adapter: DaoGaugeAdapter, + pub orchestrator: DaoGaugeOrchestrator, + pub test_adapter: DaoGaugeAdapterGeneric, +} + +impl GaugeSuite { + pub fn new(chain: Chain) -> GaugeSuite { + GaugeSuite:: { + adapter: DaoGaugeAdapter::new("gauge_adapter", chain.clone()), + orchestrator: DaoGaugeOrchestrator::new("gauge_orchestrator", chain.clone()), + test_adapter: DaoGaugeAdapterGeneric::new("dao_gauge_adapter", chain.clone()), + } + } + + pub fn upload(&self) -> Result<(), CwOrchError> { + self.adapter.upload()?; + self.orchestrator.upload()?; + self.test_adapter.upload()?; + Ok(()) + } +} + +/// DAO-cw4-voting w/ gauges +pub struct DaoDaoCw4Gauge { + pub dao_core: DaoDaoCore, + pub prop_single: DaoProposalSingle, + pub cw4_vote: DaoVotingCw4, + pub gauge_suite: GaugeSuite, + pub cw4_group: Option, +} + +impl DaoDaoCw4Gauge { + pub fn new(chain: Chain) -> DaoDaoCw4Gauge { + DaoDaoCw4Gauge:: { + dao_core: DaoDaoCore::new("dao_dao_core", chain.clone()), + prop_single: DaoProposalSingle::new("dao_prop_single", chain.clone()), + cw4_vote: DaoVotingCw4::new("dao_cw4_voting", chain.clone()), + gauge_suite: GaugeSuite::new(chain.clone()), + cw4_group: None, + } + } + pub fn upload(&self) -> Result<(), CwOrchError> { + self.dao_core.upload()?; + self.prop_single.upload()?; + self.cw4_vote.upload()?; + self.gauge_suite.upload()?; + Ok(()) + } +} diff --git a/scripts/src/lib.rs b/scripts/src/lib.rs new file mode 100644 index 000000000..cff48224a --- /dev/null +++ b/scripts/src/lib.rs @@ -0,0 +1,22 @@ +#[allow(dead_code)] +fn main() {} + +mod deploy; +mod dao; +mod external; +mod gauges; +mod distribution; +mod propose; +mod staking; +mod voting; + +pub use dao::*; +pub use external::*; +pub use distribution::*; +pub use propose::*; +pub use staking::*; +pub use voting::*; +pub use gauges::*; + +#[cfg(test)] +mod tests; diff --git a/scripts/src/propose.rs b/scripts/src/propose.rs new file mode 100644 index 000000000..6c72bb424 --- /dev/null +++ b/scripts/src/propose.rs @@ -0,0 +1,68 @@ +use cw_orch::prelude::*; +use dao_cw_orch::*; + +pub struct DaoPreProposeSuite { + pub pre_prop_approval_single: DaoPreProposeApprovalSingle, + pub pre_prop_approver: DaoPreProposeApprover, + pub pre_prop_multiple: DaoPreProposeMultiple, + pub pre_prop_single: DaoPreProposeSingle, +} + +impl DaoPreProposeSuite { + pub fn new(chain: Chain) -> DaoPreProposeSuite { + DaoPreProposeSuite:: { + pre_prop_approval_single: DaoPreProposeApprovalSingle::new( + "dao_pre_propose_approval_single", + chain.clone(), + ), + pre_prop_approver: DaoPreProposeApprover::new( + "dao_pre_propose_approver", + chain.clone(), + ), + pre_prop_multiple: DaoPreProposeMultiple::new( + "dao_pre_propose_multiple", + chain.clone(), + ), + pre_prop_single: DaoPreProposeSingle::new("dao_pre_propose_single", chain.clone()), + } + } + + pub fn upload(&self) -> Result<(), CwOrchError> { + self.pre_prop_approval_single.upload()?; + self.pre_prop_approver.upload()?; + self.pre_prop_multiple.upload()?; + self.pre_prop_single.upload()?; + + Ok(()) + } +} + +// proposal suite +pub struct DaoProposalSuite { + pub prop_single: DaoProposalSingle, + pub prop_multiple: DaoProposalMultiple, + pub prop_condocert: DaoProposalCondorcet, + pub prop_sudo: DaoProposalSudo, + pub pre_prop_suite: DaoPreProposeSuite, +} + +impl DaoProposalSuite { + pub fn new(chain: Chain) -> DaoProposalSuite { + DaoProposalSuite:: { + prop_single: DaoProposalSingle::new("dao_proposal_single", chain.clone()), + prop_multiple: DaoProposalMultiple::new("dao_proposal_multiple", chain.clone()), + prop_condocert: DaoProposalCondorcet::new("dao_proposal_condocert", chain.clone()), + prop_sudo: DaoProposalSudo::new("dao_proposal_sudo", chain.clone()), + pre_prop_suite: DaoPreProposeSuite::new(chain.clone()), + } + } + + pub fn upload(&self) -> Result<(), CwOrchError> { + self.prop_single.upload()?; + self.prop_multiple.upload()?; + self.prop_condocert.upload()?; + self.prop_sudo.upload()?; + self.pre_prop_suite.upload()?; + Ok(()) + } +} diff --git a/scripts/src/staking.rs b/scripts/src/staking.rs new file mode 100644 index 000000000..f930fb518 --- /dev/null +++ b/scripts/src/staking.rs @@ -0,0 +1,31 @@ +use cw_orch::prelude::*; +use dao_cw_orch::*; + +pub struct DaoStakingSuite { + pub cw20_stake: DaoStakingCw20, + pub exteral_rewards: DaoStakingCw20ExternalRewards, + pub rewards_distributor: DaoStakingCw20RewardDistributor, +} + +impl DaoStakingSuite { + pub fn new(chain: Chain) -> DaoStakingSuite { + DaoStakingSuite:: { + cw20_stake: DaoStakingCw20::new("cw20_stake", chain.clone()), + exteral_rewards: DaoStakingCw20ExternalRewards::new( + "cw20_external_rewards", + chain.clone(), + ), + rewards_distributor: DaoStakingCw20RewardDistributor::new( + "cw20_reward_distributor", + chain.clone(), + ), + } + } + + pub fn upload(&self) -> Result<(), CwOrchError> { + self.cw20_stake.upload()?; + self.exteral_rewards.upload()?; + self.rewards_distributor.upload()?; + Ok(()) + } +} diff --git a/scripts/src/tests/daos/mod.rs b/scripts/src/tests/daos/mod.rs new file mode 100644 index 000000000..03327b80c --- /dev/null +++ b/scripts/src/tests/daos/mod.rs @@ -0,0 +1,31 @@ +use cosmwasm_std::{Addr, Event}; + +pub mod voting; + + +/// grabs prop module from app response +pub fn extract_dao_events(events: &Vec, prop_type: &str) -> Option { + for event in events { + if event.ty == "wasm" { + for attribute in &event.attributes { + if attribute.key == prop_type { + return Some(Addr::unchecked(&attribute.value)); + } + } + } + } + None +} +// grabs prop module from app response +// pub fn extract_prop_id(events: &Vec) -> Option { +// for event in events { +// if event.ty == "wasm" { +// for attribute in &event.attributes { +// if attribute.key == "proposal_id" { +// return Some(u64::from_str_radix(&attribute.value, 10).unwrap()); +// } +// } +// } +// } +// None +// } \ No newline at end of file diff --git a/scripts/src/tests/daos/voting/mod.rs b/scripts/src/tests/daos/voting/mod.rs new file mode 100644 index 000000000..d26fe6309 --- /dev/null +++ b/scripts/src/tests/daos/voting/mod.rs @@ -0,0 +1,69 @@ +use crate::{tests::Cw4VotingInitMsg, DaoDaoCw4Gauge}; +use cosmwasm_std::{to_json_binary, Uint128}; +use cw4::Member; +use cw_orch::{anyhow, prelude::*}; +use dao_interface::state::ModuleInstantiateInfo; +use dao_voting::{pre_propose::PreProposeInfo, threshold::Threshold}; +use dao_voting_cw4::msg::GroupContract; + +use super::extract_dao_events; + +pub fn dao_cw4_voting_template( + mock: MockBech32, + suite: &mut DaoDaoCw4Gauge, + initial_members: Vec, +) -> anyhow::Result> { + // setup cw4 stuff + + let res = suite.dao_core.instantiate( + &dao_interface::msg::InstantiateMsg { + admin: None, + name: "Cw4VotingDao".into(), + description: "template for dao with cw4 voting module".into(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: suite.cw4_vote.code_id().unwrap(), + msg: to_json_binary(&Cw4VotingInitMsg { + group_contract: GroupContract::New { + cw4_group_code_id: suite.cw4_group.unwrap(), + initial_members, + }, + })?, + admin: None, + funds: vec![], + label: "cw4-voting".into(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: suite.prop_single.code_id()?, + msg: to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + threshold: Threshold::AbsoluteCount { + threshold: Uint128::one(), + }, + max_voting_period: cw_utils::Duration::Height(mock.block_info()?.height + 10), + min_voting_period: None, + only_members_execute: true, + allow_revoting: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + close_proposal_on_execution_failure: true, + veto: None, + })?, + admin: None, + funds: vec![], + label: "single-proposal".into(), + }], + initial_items: None, + dao_uri: None, + }, + None, + None, + )?; + + // grabs the daos created prop modules. + // Here we only expect one contract per module but more complex daos may have more than one, will need to update. + let prop_addr = extract_dao_events(&res.events, "prop_module").unwrap(); + let voting_addr = extract_dao_events(&res.events, "voting_module").unwrap(); + + Ok(vec![prop_addr, voting_addr]) +} diff --git a/scripts/src/tests/distribution/mod.rs b/scripts/src/tests/distribution/mod.rs new file mode 100644 index 000000000..83ffe18f1 --- /dev/null +++ b/scripts/src/tests/distribution/mod.rs @@ -0,0 +1,15 @@ +use cw_orch::{anyhow, prelude::*}; + +use crate::{ + distribution::DaoDistributionSuite, + tests::{ADMIN, PREFIX}, +}; + +#[test] +fn test_distribution_suite() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let admin = mock.addr_make(ADMIN); + let _app = DaoDistributionSuite::deploy_on(mock.clone(), admin.clone())?; + mock.next_block()?; + Ok(()) +} diff --git a/scripts/src/tests/external/admin_factory.rs b/scripts/src/tests/external/admin_factory.rs new file mode 100644 index 000000000..abf9304bf --- /dev/null +++ b/scripts/src/tests/external/admin_factory.rs @@ -0,0 +1,18 @@ + + +mod test { + + + #[test] + fn test_set_self_admin_instantiate2() { + + } + #[test] + fn addr_canonicalize() { + + } + #[test] + fn addr_humanize() { + + } +} \ No newline at end of file diff --git a/scripts/src/tests/external/btsg_ft_factory.rs b/scripts/src/tests/external/btsg_ft_factory.rs new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/src/tests/external/cw721_roles.rs b/scripts/src/tests/external/cw721_roles.rs new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/src/tests/external/dao_migration.rs b/scripts/src/tests/external/dao_migration.rs new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/src/tests/external/mod.rs b/scripts/src/tests/external/mod.rs new file mode 100644 index 000000000..3b267d7e2 --- /dev/null +++ b/scripts/src/tests/external/mod.rs @@ -0,0 +1,21 @@ +use cw_orch::{anyhow, prelude::*}; +use crate::DaoExternalSuite; +use super::{ADMIN, PREFIX}; + +pub mod admin_factory; +pub mod btsg_ft_factory; +pub mod cw721_roles; +pub mod dao_migration; +pub mod payroll; +pub mod token_swap; +pub mod tokenfactory_issuer; +pub mod vesting; + +#[test] +fn test_external_suite() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let admin = mock.addr_make(ADMIN); + let _app = DaoExternalSuite::deploy_on(mock.clone(), admin.clone())?; + mock.next_block().unwrap(); + Ok(()) +} diff --git a/scripts/src/tests/external/payroll.rs b/scripts/src/tests/external/payroll.rs new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/src/tests/external/token_swap.rs b/scripts/src/tests/external/token_swap.rs new file mode 100644 index 000000000..1f5a44ba0 --- /dev/null +++ b/scripts/src/tests/external/token_swap.rs @@ -0,0 +1,31 @@ +use cw_orch::{anyhow, prelude::*}; +use cw_token_swap::msg::{Counterparty, InstantiateMsg, TokenInfo}; +use dao_cw_orch::DaoExternalTokenSwap; + +fn _setup_tokenswap_helper( + app: DaoExternalTokenSwap, + sender: String, + counterparty: String, +) -> anyhow::Result<()> { + app.instantiate( + &InstantiateMsg { + counterparty_one: Counterparty { + address: sender, + promise: TokenInfo::Native { + denom: "juno".to_string(), + amount: 1_000u128.into(), + }, + }, + counterparty_two: Counterparty { + address: counterparty, + promise: TokenInfo::Native { + denom: "juno".to_string(), + amount: 1_000u128.into(), + }, + }, + }, + None, + None, + )?; + Ok(()) +} diff --git a/scripts/src/tests/external/tokenfactory_issuer.rs b/scripts/src/tests/external/tokenfactory_issuer.rs new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/src/tests/external/vesting.rs b/scripts/src/tests/external/vesting.rs new file mode 100644 index 000000000..e69de29bb diff --git a/contracts/gauges/gauge-adapter/src/multitest/suite.rs b/scripts/src/tests/gauges/adapter/mod.rs similarity index 73% rename from contracts/gauges/gauge-adapter/src/multitest/suite.rs rename to scripts/src/tests/gauges/adapter/mod.rs index a667097fd..cc4a26f86 100644 --- a/contracts/gauges/gauge-adapter/src/multitest/suite.rs +++ b/scripts/src/tests/gauges/adapter/mod.rs @@ -1,11 +1,13 @@ use abstract_cw_plus_interface::cw20_base::Cw20Base; use cosmwasm_std::{Addr, Coin, Uint128}; -use cw_orch::{interface, mock::cw_multi_test::AppResponse, prelude::*}; +use cw_orch::{anyhow, interface, mock::cw_multi_test::AppResponse, prelude::*}; use cw_orch_core::CwEnvError; -use abstract_cw20::{Cw20Coin as AbsCw20Coin, MinterResponse}; +mod tests; -use crate::{ +use abstract_cw20::{Cw20Coin as AbsCw20Coin, MinterResponse}; +// use scripts::DaoDao; +use gauge_adapter::{ contract::{execute, instantiate, migrate, query}, msg::{AdapterQueryMsg as QueryMsg, AssetUnchecked, ExecuteMsg, InstantiateMsg, MigrateMsg}, }; @@ -44,6 +46,35 @@ pub fn setup_gauge_adapter( adapter } +impl GaugeAdapter { + pub fn native_submission_helper( + &self, + sender: Addr, + recipient: Addr, + native_tokens: Option, + ) -> anyhow::Result { + if let Some(assets) = native_tokens.clone() { + self.call_as(&sender).execute( + &gauge_adapter::msg::ExecuteMsg::CreateSubmission { + name: "DAOers".to_string(), + url: "https://daodao.zone".to_string(), + address: recipient.to_string(), + }, + Some(&[assets]), + ) + } else { + self.call_as(&sender).execute( + &gauge_adapter::msg::ExecuteMsg::CreateSubmission { + name: "DAOers".to_string(), + url: "https://daodao.zone".to_string(), + address: recipient.to_string(), + }, + None, + ) + } + } +} + pub fn setup_cw20_reward_gauge_adapter( mock: MockBech32, required_deposit: Option, @@ -62,34 +93,6 @@ pub fn setup_cw20_reward_gauge_adapter( (adapter, cw20) } -// -pub fn native_submission_helper( - adapter: GaugeAdapter, - sender: Addr, - recipient: Addr, - native_tokens: Option, -) -> Result { - if let Some(assets) = native_tokens.clone() { - adapter.call_as(&sender).execute( - &crate::msg::ExecuteMsg::CreateSubmission { - name: "DAOers".to_string(), - url: "https://daodao.zone".to_string(), - address: recipient.to_string(), - }, - Some(&[assets]), - ) - } else { - adapter.call_as(&sender).execute( - &crate::msg::ExecuteMsg::CreateSubmission { - name: "DAOers".to_string(), - url: "https://daodao.zone".to_string(), - address: recipient.to_string(), - }, - None, - ) - } -} - pub fn cw20_helper(mock: MockBech32) -> Cw20Base { let cw20 = Cw20Base::new("cw20", mock.clone()); cw20.upload().unwrap(); diff --git a/scripts/src/tests/gauges/adapter/tests.rs b/scripts/src/tests/gauges/adapter/tests.rs new file mode 100644 index 000000000..6de64fb5b --- /dev/null +++ b/scripts/src/tests/gauges/adapter/tests.rs @@ -0,0 +1,570 @@ +use abstract_cw20::{msg::Cw20ExecuteMsgFns, Cw20ExecuteMsg as AbsCw20ExecuteMsg}; +use abstract_cw20_base::msg::QueryMsgFns; +use cosmwasm_std::{coin, to_json_binary, Addr, CosmosMsg, Decimal, Uint128, WasmMsg}; + +use crate::tests::gauges::adapter::{setup_gauge_adapter,setup_cw20_reward_gauge_adapter,cw20_helper}; +use cw_denom::UncheckedDenom; +use cw_orch::{contract::interface_traits::CwOrchExecute, mock::MockBech32, prelude::*}; +use gauge_adapter::{ + msg::{ + AdapterQueryMsg, AdapterQueryMsgFns, AllSubmissionsResponse, AssetUnchecked, ExecuteMsg, + ExecuteMsgFns, ReceiveMsg, SubmissionResponse, + }, + ContractError, +}; +mod intention_proposal { + + use super::*; + #[test] + fn create_default_submission() { + let mock = MockBech32::new("mock"); + let treasury = &mock.addr_make("community_pool"); + let adapter = setup_gauge_adapter(mock.clone(), None); + + // this one is created by default during instantiation + assert_eq!( + SubmissionResponse { + sender: adapter.address().unwrap(), + name: "Unimpressed".to_owned(), + url: "Those funds go back to the community pool".to_owned(), + address: treasury.clone(), + }, + adapter + .query(&gauge_adapter::msg::AdapterQueryMsg::Submission { + address: treasury.to_string() + }) + .unwrap() + ) + } + + #[test] + fn create_submission_no_required_deposit() { + let mock = MockBech32::new("mock"); + let adapter = setup_gauge_adapter(mock.clone(), None); + + let recipient = mock.addr_make("recipient"); + mock.add_balance(&mock.sender, vec![coin(1_000, "juno")]) + .unwrap(); + + // Fails send funds along with the tx. + let err = adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + Some(coin(1_000, "juno")), + ) + .unwrap_err(); + + assert_eq!( + ContractError::InvalidDepositAmount { + correct_amount: Uint128::zero() + }, + err.downcast().unwrap(), + ); + + // Valid submission. + let result = adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + None, + ); + assert!(result.is_ok()); + + assert_eq!( + SubmissionResponse { + sender: mock.sender, + name: "DAOers".to_owned(), + url: "https://daodao.zone".to_owned(), + address: recipient.clone(), + }, + adapter + .query(&gauge_adapter::msg::AdapterQueryMsg::Submission { + address: recipient.to_string() + }) + .unwrap(), + ) + } + + #[test] + fn overwrite_existing_submission() { + let mock = MockBech32::new("mock"); + let adapter = setup_gauge_adapter(mock.clone(), None); + let recipient = mock.addr_make("recipient"); + adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + None, + ) + .unwrap(); + assert_eq!( + SubmissionResponse { + sender: mock.sender.clone(), + name: "DAOers".to_owned(), + url: "https://daodao.zone".to_string(), + address: recipient.clone(), + }, + adapter.submission(recipient.to_string()).unwrap() + ); + + // Try to submit to the same address with different user + let err = adapter.native_submission_helper( + Addr::unchecked("anotheruser"), + recipient.clone(), + None, + ) + .unwrap_err(); + + assert_eq!( + ContractError::UnauthorizedSubmission {}, + err.downcast().unwrap() + ); + + // Overwriting submission as same author works + adapter.native_submission_helper( mock.sender, recipient.clone(), None).unwrap(); + + let response = adapter.submission(recipient.to_string()).unwrap(); + assert_eq!(response.url, "https://daodao.zone".to_owned()); + } + + #[test] + fn create_submission_required_deposit() { + let mock = MockBech32::new("mock"); + let adapter = setup_gauge_adapter( + mock.clone(), + Some(AssetUnchecked { + denom: UncheckedDenom::Native("juno".into()), + amount: 1_000u128.into(), + }), + ); + + let recipient = mock.addr_make("recipient"); + mock.add_balance(&mock.sender.clone(), vec![coin(1_000, "wynd")]) + .unwrap(); + mock.add_balance(&mock.sender.clone(), vec![coin(1_000, "juno")]) + .unwrap(); + // Fails if no funds sent. + let err = adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + None, + ) + .unwrap_err(); + + assert_eq!( + ContractError::PaymentError(cw_utils::PaymentError::NoFunds {}), + err.downcast().unwrap() + ); + + // Fails if correct denom but not enough amount. + // Fails if no funds sent. + let err = adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + Some(Coin { + denom: "juno".into(), + amount: 999u128.into(), + }), + ) + .unwrap_err(); + + assert_eq!( + ContractError::InvalidDepositAmount { + correct_amount: Uint128::new(1_000) + }, + err.downcast().unwrap() + ); + + // Fails if enough amount but incorrect denom. + let err = adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + Some(Coin { + denom: "wynd".into(), + amount: 1_000u128.into(), + }), + ) + .unwrap_err(); + + assert_eq!( + ContractError::InvalidDepositType {}, + err.downcast().unwrap() + ); + + // Valid submission. + adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + Some(Coin { + denom: "juno".into(), + amount: 1_000u128.into(), + }), + ) + .unwrap(); + + assert_eq!( + SubmissionResponse { + sender: mock.sender.clone(), + name: "DAOers".to_owned(), + url: "https://daodao.zone".to_owned(), + address: recipient.clone(), + }, + adapter + .query(&AdapterQueryMsg::Submission { + address: recipient.to_string() + }) + .unwrap() + ) + } + + #[test] + fn create_receive_required_deposit() { + let mock = MockBech32::new("mock"); + let cw20 = cw20_helper(mock.clone()); + let bad_cw20 = cw20_helper(mock.clone()); + let cw20_addr = cw20.address().unwrap(); + let bad_cw20_addr = bad_cw20.address().unwrap(); + println!("good cw20: {:#?}", cw20_addr); + println!("bad cw20: {:#?}", bad_cw20_addr); + let adapter = setup_gauge_adapter( + mock.clone(), + Some(AssetUnchecked { + denom: UncheckedDenom::Cw20(cw20_addr.to_string()), + amount: 1_000u128.into(), + }), + ); + + let recipient = mock.sender_addr().to_string(); + + let binary_msg = to_json_binary(&ReceiveMsg::CreateSubmission { + name: "DAOers".into(), + url: "https://daodao.zone".into(), + address: recipient.clone(), + }) + .unwrap(); + // Fails by sending wrong cw20. + let err = adapter + .call_as(&Addr::unchecked( + "mock1mzdhwvvh22wrt07w59wxyd58822qavwkx5lcej7aqfkpqqlhaqfsetqc4t", + )) + .execute( + &ExecuteMsg::Receive(cw20::Cw20ReceiveMsg { + sender: recipient.to_string(), + amount: Uint128::from(1_000u128), + msg: binary_msg.clone(), + }), + None, + ) + .unwrap_err(); + + assert_eq!( + ContractError::InvalidDepositType {}, + err.downcast().unwrap(), + ); + + // Fails by sending less tokens than required. + let err = adapter + .call_as(&cw20.address().unwrap()) + .execute( + &ExecuteMsg::Receive(cw20::Cw20ReceiveMsg { + sender: recipient.to_string(), + amount: Uint128::from(999u128), + msg: binary_msg.clone(), + }), + None, + ) + .unwrap_err(); + + assert_eq!( + ContractError::InvalidDepositAmount { + correct_amount: Uint128::new(1_000) + }, + err.downcast().unwrap() + ); + + // Valid submission. + adapter + .call_as(&cw20.address().unwrap()) + .execute( + &ExecuteMsg::Receive(cw20::Cw20ReceiveMsg { + sender: recipient.to_string(), + amount: Uint128::from(1_000u128), + msg: binary_msg, + }), + None, + ) + .unwrap(); + + assert_eq!( + SubmissionResponse { + sender: mock.sender.clone(), + name: "DAOers".to_owned(), + url: "https://daodao.zone".to_owned(), + address: Addr::unchecked(recipient.clone()), + }, + adapter + .query(&AdapterQueryMsg::Submission { + address: recipient.to_string() + }) + .unwrap() + ); + assert_eq!( + 2, + adapter + .query::(&AdapterQueryMsg::AllSubmissions {}) + .unwrap() + .submissions + .len() + ) + } + + #[test] + fn return_deposits_no_required_deposit() { + let mock = MockBech32::new("mock"); + let adapter = setup_gauge_adapter(mock.clone(), None); + + let err = adapter + .execute(&ExecuteMsg::ReturnDeposits {}, None) + .unwrap_err(); + + assert_eq!(ContractError::NoDepositToRefund {}, err.downcast().unwrap()) + } + + #[test] + fn return_deposits_no_admin() { + let mock = MockBech32::new("mock"); + let bad_addr = mock.addr_make("einstien"); + let adapter = setup_gauge_adapter( + mock.clone(), + Some(AssetUnchecked { + denom: UncheckedDenom::Native("juno".into()), + amount: 1_000u128.into(), + }), + ); + + let err = adapter + .call_as(&bad_addr) + .execute(&ExecuteMsg::ReturnDeposits {}, None) + .unwrap_err(); + + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()) + } + + #[test] + fn return_deposits_required_native_deposit() { + let mock = MockBech32::new("mock"); + let adapter = setup_gauge_adapter( + mock.clone(), + Some(AssetUnchecked { + denom: UncheckedDenom::Native("juno".into()), + amount: 1_000u128.into(), + }), + ); + mock.add_balance(&mock.sender, vec![coin(1_000u128, "juno")]) + .unwrap(); + let recipient = mock.addr_make("recipient"); + // Valid submission. + adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + Some(coin(1_000u128, "juno")), + ) + .unwrap(); + + assert_eq!( + mock.query_balance(&mock.sender.clone(), "juno").unwrap(), + Uint128::zero() + ); + assert_eq!( + mock.query_balance(&recipient, "juno").unwrap(), + Uint128::zero() + ); + assert_eq!( + mock.query_balance(&adapter.address().unwrap(), "juno") + .unwrap(), + Uint128::from(1000u128) + ); + + adapter + .execute(&ExecuteMsg::ReturnDeposits {}, None) + .unwrap(); + assert_eq!( + mock.query_balance(&mock.sender.clone(), "juno").unwrap(), + Uint128::from(1000u128) + ); + assert_eq!( + mock.query_balance(&recipient, "juno").unwrap(), + Uint128::zero() + ); + assert_eq!( + mock.query_balance(&adapter.address().unwrap(), "juno") + .unwrap(), + Uint128::zero() + ); + } + + #[test] + fn return_deposits_required_native_deposit_multiple_deposits() { + let mock = MockBech32::new("mock"); + let adapter = setup_gauge_adapter( + mock.clone(), + Some(AssetUnchecked { + denom: UncheckedDenom::Native("juno".into()), + amount: 1_000u128.into(), + }), + ); + + let recipient = mock.addr_make("recipient"); + let einstien = mock + .addr_make_with_balance("einstien", vec![coin(1_000u128, "juno")]) + .unwrap(); + mock.add_balance(&mock.sender, vec![coin(1_000u128, "juno")]) + .unwrap(); + // Valid submission. + adapter.native_submission_helper( + mock.sender.clone(), + recipient.clone(), + Some(coin(1_000u128, "juno")), + ) + .unwrap(); + // Valid submission. + adapter.native_submission_helper( + einstien.clone(), + einstien.clone(), + Some(coin(1_000u128, "juno")), + ) + .unwrap(); + adapter.return_deposits().unwrap(); + assert_eq!( + mock.query_balance(&mock.sender.clone(), "juno").unwrap(), + Uint128::from(1000u128) + ); + assert_eq!( + mock.query_balance(&einstien, "juno").unwrap(), + Uint128::from(1000u128) + ); + assert_eq!( + mock.query_balance(&recipient, "juno").unwrap(), + Uint128::zero() + ); + assert_eq!( + mock.query_balance(&adapter.address().unwrap(), "juno") + .unwrap(), + Uint128::zero() + ); + } + + #[test] + fn return_deposits_required_cw20_deposit() { + let mock = MockBech32::new("mock"); + let cw20 = cw20_helper(mock.clone()); + let recipient = mock.addr_make("recipient"); + let adapter = setup_gauge_adapter( + mock.clone(), + Some(AssetUnchecked { + denom: UncheckedDenom::Cw20(cw20.addr_str().unwrap()), + amount: 1_000u128.into(), + }), + ); + let binary_msg = to_json_binary(&ReceiveMsg::CreateSubmission { + name: "DAOers".into(), + url: "https://daodao.zone".into(), + address: recipient.to_string(), + }) + .unwrap(); + + // Valid submission. + cw20.send(1_000u128.into(), adapter.addr_str().unwrap(), binary_msg) + .unwrap(); + + assert_eq!( + cw20.balance(mock.sender.to_string()).unwrap().balance, + Uint128::from(999_000u128) + ); + assert_eq!( + cw20.balance(recipient.to_string()).unwrap().balance, + Uint128::zero() + ); + assert_eq!( + cw20.balance(adapter.address().unwrap().to_string()) + .unwrap() + .balance, + Uint128::from(1_000u128), + ); + + adapter.return_deposits().unwrap(); + + assert_eq!( + cw20.balance(mock.sender.to_string()).unwrap().balance, + Uint128::from(1_000_000u128), + ); + // Tokens are sent back to submission sender, not recipient. + assert_eq!( + cw20.balance(recipient.to_string()).unwrap().balance, + Uint128::zero(), + ); + assert_eq!( + cw20.balance(adapter.address().unwrap().to_string()) + .unwrap() + .balance, + Uint128::zero(), + ); + } + + #[test] + fn sample_gauge_msgs_cw20() { + let mock = MockBech32::new("mock"); + let addr_1 = mock.addr_make("addr1"); + let addr_2 = mock.addr_make("addr2"); + let addr_3 = mock.addr_make("addr3"); + let reward = Uint128::new(1_000_000); + let (adapter, cw20) = setup_cw20_reward_gauge_adapter(mock.clone(), None); + + adapter + .create_submission(addr_1.to_string(), "name", "https://test.url") + .unwrap(); + adapter + .create_submission(addr_2.to_string(), "name", "https://test.url") + .unwrap(); + + let selected = vec![ + (addr_1.to_string(), Decimal::percent(41)), + (addr_2.to_string(), Decimal::percent(33)), + (addr_3.to_string(), Decimal::percent(26)), + ]; + + let res: gauge_adapter::msg::SampleGaugeMsgsResponse = + adapter.sample_gauge_msgs(selected.clone()).unwrap(); + assert_eq!(res.execute.len(), 3); + assert_eq!( + res.execute, + [ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw20.addr_str().unwrap(), + msg: to_json_binary(&AbsCw20ExecuteMsg::Transfer { + recipient: addr_1.to_string(), + amount: reward * Decimal::percent(41) + }) + .unwrap(), + funds: vec![] + }), + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw20.addr_str().unwrap(), + msg: to_json_binary(&AbsCw20ExecuteMsg::Transfer { + recipient: addr_2.to_string(), + amount: reward * Decimal::percent(33) + }) + .unwrap(), + funds: vec![] + }), + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw20.addr_str().unwrap(), + msg: to_json_binary(&AbsCw20ExecuteMsg::Transfer { + recipient: addr_3.to_string(), + amount: reward * Decimal::percent(26) + }) + .unwrap(), + funds: vec![] + }), + ] + ); + } +} diff --git a/scripts/src/tests/gauges/helpers.rs b/scripts/src/tests/gauges/helpers.rs new file mode 100644 index 000000000..51dde95ee --- /dev/null +++ b/scripts/src/tests/gauges/helpers.rs @@ -0,0 +1,39 @@ +use cosmwasm_std::Decimal; + +pub const EPOCH: u64 = 60 * 60 * 24 * 7; +pub const RESET_EPOCH: u64 = 30 * 86_400; + +pub fn simple_vote( + voter: &str, + option: &str, + percentage: u64, + cast: impl Into>, +) -> gauge_orchestrator::msg::VoteInfo { + gauge_orchestrator::msg::VoteInfo { + voter: voter.to_string(), + votes: vec![gauge_orchestrator::state::Vote { + option: option.to_string(), + weight: Decimal::percent(percentage), + }], + cast: cast.into(), + } +} + +pub fn multi_vote( + voter: &str, + votes: &[(&str, u64)], + cast: impl Into>, +) -> gauge_orchestrator::msg::VoteInfo { + let votes = votes + .iter() + .map(|(opt, percentage)| gauge_orchestrator::state::Vote { + option: opt.to_string(), + weight: Decimal::percent(*percentage), + }) + .collect(); + gauge_orchestrator::msg::VoteInfo { + voter: voter.to_string(), + votes, + cast: cast.into(), + } +} diff --git a/scripts/src/tests/gauges/mod.rs b/scripts/src/tests/gauges/mod.rs new file mode 100644 index 000000000..a4bb69a38 --- /dev/null +++ b/scripts/src/tests/gauges/mod.rs @@ -0,0 +1,4 @@ +pub mod adapter; +pub mod helpers; +pub mod orchestrator; +pub mod suite; diff --git a/scripts/src/tests/gauges/orchestrator.rs b/scripts/src/tests/gauges/orchestrator.rs new file mode 100644 index 000000000..3b2199ff6 --- /dev/null +++ b/scripts/src/tests/gauges/orchestrator.rs @@ -0,0 +1,2656 @@ +use crate::tests::{gauges::helpers::EPOCH, PREFIX}; +use crate::DaoDaoCw4Gauge; +use cosmwasm_std::{coin, coins, Decimal, Uint128}; +use cw4::Member; +use cw_orch::{anyhow, prelude::*}; +use dao_gauge_adapter::contract::ExecuteMsg as AdapterExecuteMsg; +use dao_voting_cw4::msg::QueryMsgFns as _; +use gauge_orchestrator::{ + msg::{ + ExecuteMsg as GaugeExecuteMsg, ExecuteMsgFns as OrchExecuteMsgFns, GaugeMigrationConfig, + GaugeResponse, MigrateMsg, QueryMsgFns as GaugeOrchQueryMsgFns, + }, + state::Vote as GaugeVote, + ContractError, +}; +mod gauge { + use super::*; + #[test] + fn test_create_gauge() -> anyhow::Result<(), CwOrchError> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + // confirm gauge is created + let res = dao.gauge_suite.orchestrator.gauge(0)?; + let adapter = dao.gauge_suite.adapter.addr_str()?; + assert_eq!( + res, + GaugeResponse { + id: 0, + title: "default-gauge".to_owned(), + adapter: adapter, + epoch_size: EPOCH, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 7 * 86400, + reset: None, + total_epochs: None, + } + ); + + Ok(()) + } + + #[test] + fn test_gauge_can_upgrade_from_self() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + // now let's migrate the gauge and make sure nothing breaks + let dao_addr = dao.dao_core.address()?; + let gauge_code_id = dao.gauge_suite.orchestrator.code_id()?; + dao.gauge_suite + .orchestrator + .call_as(&dao_addr) + .migrate(&Empty {}, gauge_code_id)?; + // confirm contract still functions + let res = dao.gauge_suite.orchestrator.gauge(0)?; + let adapter = dao.gauge_suite.adapter.addr_str()?; + assert_eq!( + res, + GaugeResponse { + id: 0, + title: "default-gauge".to_owned(), + adapter: adapter, + epoch_size: 60 * 60 * 24 * 7, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 7 * 86400, + reset: None, + total_epochs: None, + } + ); + Ok(()) + } + #[test] + fn test_gauge_migrate_with_next_epochs() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let gauge_code_id = dao.gauge_suite.orchestrator.code_id()?; + let dao_addr = dao.dao_core.address()?; + let gauge_addr = dao.gauge_suite.orchestrator.address()?; + // now let's migrate the gauge and make sure nothing breaks + let gauge_id = 0; + // change next epoch from 7 to 14 days + mock.call_as(&dao_addr).migrate( + &MigrateMsg { + gauge_config: Some(vec![( + gauge_id, + GaugeMigrationConfig { + next_epoch: Some(mock.block_info()?.time.seconds() + 14 * 86400), + reset: None, + }, + )]), + }, + gauge_code_id.clone(), + &gauge_addr.clone(), + )?; + // confirm update + let response = dao.gauge_suite.orchestrator.gauge(0).unwrap(); + assert_eq!( + response, + GaugeResponse { + id: 0, + title: "default-gauge".to_owned(), + adapter: dao.gauge_suite.adapter.addr_str()?, + epoch_size: EPOCH, + total_epochs: None, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 14 * 86400, + reset: None, + } + ); + // try to migrate updating next epoch on nonexisting gauge_id + mock.migrate( + &MigrateMsg { + gauge_config: Some(vec![( + 420, + GaugeMigrationConfig { + next_epoch: Some(mock.block_info()?.time.seconds() + 14 * 86400), + reset: None, + }, + )]), + }, + gauge_code_id.clone(), + &gauge_addr.clone(), + ) + .unwrap_err(); + Ok(()) + } + + // /// attach adaptor in instantiate + #[test] + fn test_execute_gauge() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + // addresses + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let dao_addr = dao.dao_core.address()?; + + let gauge_id = 0u64; + + let res = dao + .gauge_suite + .orchestrator + .list_options(gauge_id, None, None)?; + println!("{:#?}", res.options); + + // vote for one of the options in gauge + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + // confirm gauge recieved vote + let selected_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?; + assert_eq!( + selected_set.votes, + vec![(voter1.to_string(), Uint128::new(200))] + ); + // before advancing specified epoch tally won't get sampled + mock.wait_seconds(EPOCH)?; + + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + // assert rewards have been distriubuted + assert_eq!( + mock.balance(voter1, Some("ujuno".into()))?[0].amount, + Uint128::from(1000u128), + ); + + Ok(()) + } + + #[test] + fn test_query_last_execution() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + // addresses + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let dao_addr = dao.dao_core.address()?; + let gauge_id = 0; + mock.add_balance(&dao_addr, coins(1000, "ujuno"))?; + + // confirm gauge is not executed yet + assert_eq!( + dao.gauge_suite + .orchestrator + .last_executed_set(gauge_id)? + .votes, + None, + ); + + assert_eq!( + dao.gauge_suite + .orchestrator + .last_executed_set(gauge_id)? + .votes, + None, + "not executed yet" + ); + // vote + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }]), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![ + GaugeVote { + option: dao_addr.to_string(), + weight: Decimal::percent(40), + }, + GaugeVote { + option: voter2.to_string(), + weight: Decimal::percent(60), + }, + ]), + )?; + + // wait until epoch passes + mock.wait_seconds(EPOCH)?; + + // run gauge once + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + // should return the executed set now + let expected_votes = Some(vec![ + (voter1.to_string(), Uint128::from(100u128)), + (voter2.to_string(), Uint128::from(60u128)), + (dao_addr.to_string(), Uint128::from(40u128)), + ]); + + assert_eq!( + dao.gauge_suite + .orchestrator + .last_executed_set(gauge_id)? + .votes, + expected_votes, + ); + + // change votes + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter2.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + // change votes + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .place_votes(gauge_id, Some(vec![].into()))?; + // wait until epoch passes + + mock.wait_seconds(EPOCH)?; + // should not change last execution yet + assert_eq!( + dao.gauge_suite + .orchestrator + .last_executed_set(gauge_id)? + .votes, + expected_votes, + ); + // execute + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + // now it should be changed + assert_eq!( + dao.gauge_suite + .orchestrator + .last_executed_set(gauge_id)? + .votes, + Some(vec![(voter2.to_string(), Uint128::from(100u128))]) + ); + + Ok(()) + } + + #[test] + fn test_execute_gauge_twice_same_epoch() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + // addresses + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let dao_addr = dao.dao_core.address()?; + let gauge_id = 0; + mock.add_balance(&dao_addr, coins(1000, "ujuno"))?; + + // vote for one of the options in the gauge + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + // voter1 was option voted for with two 100 voting powers combined + assert_eq!( + dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes, + vec![(voter1.to_string(), Uint128::new(200u128))] + ); + + // before advancing specified epoch tally won't get sampled + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: 0 }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + assert_eq!( + mock.balance(voter1.clone(), Some("ujuno".to_string()))?[0] + .amount + .u128(), + 1000u128, + ); + // execution twice same time won't work + let err = mock + .call_as(&dao_addr) + .execute( + &GaugeExecuteMsg::Execute { gauge: 0 }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + ) + .unwrap_err(); + + let next_epoch = mock.block_info()?.time.seconds() + EPOCH; + assert_eq!( + ContractError::EpochNotReached { + gauge_id: 0, + current_epoch: mock.block_info()?.time.seconds(), + next_epoch + }, + err.downcast().unwrap() + ); + // just before next epoch fails as well + mock.wait_seconds(EPOCH - 1)?; + // execution twice same time won't work + let err = mock + .call_as(&dao_addr) + .execute( + &GaugeExecuteMsg::Execute { gauge: 0 }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + ) + .unwrap_err(); + + assert_eq!( + ContractError::EpochNotReached { + gauge_id: 0, + current_epoch: mock.block_info()?.time.seconds(), + next_epoch + }, + err.downcast().unwrap() + ); + // another epoch is fine + mock.wait_seconds(EPOCH)?; + + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: 0 }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + // confirm balance + assert_eq!( + mock.balance(voter1.to_string(), Some("ujuno".to_string()))?[0] + .amount + .u128(), + 2000u128 + ); + Ok(()) + } + + #[test] + fn test_execute_stopped_gauge() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + // addresses + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let dao_addr = dao.dao_core.address()?; + let gauge_id = 0; + mock.add_balance(&dao_addr, coins(1000, "ujuno"))?; + + let not_owner = mock.addr_make("not-owner"); + + // stop the gauge by not-owner + let err = dao + .gauge_suite + .orchestrator + .call_as(¬_owner) + .stop_gauge(0) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + dao.gauge_suite + .orchestrator + .call_as(&dao_addr) + .stop_gauge(0) + .unwrap(); + + // vote for one of the options in the gauge + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + // Despite gauge being stopped, votes have been recorded + assert_eq!( + dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes, + vec![(voter1.to_string(), Uint128::new(200u128))] + ); + + // before advancing specified epoch tally won't get sampled + mock.wait_seconds(EPOCH)?; + let err = mock + .call_as(&dao_addr) + .execute( + &GaugeExecuteMsg::Execute { gauge: 0 }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + ) + .unwrap_err(); + assert_eq!(ContractError::GaugeStopped(0), err.downcast().unwrap()); + Ok(()) + } + + #[test] + fn test_update_gauge() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + // addresses + let dao_addr = dao.dao_core.address()?; + mock.add_balance(&dao_addr, coins(1000, "ujuno"))?; + + // setup another gauge + let second_gauge_adapter = dao.init_testing_adapter(&[ + &mock.addr_make("voter1").to_string(), + &mock.addr_make("voter2").to_string(), + ])?; + dao.add_adapter_to_gauge(second_gauge_adapter.clone())?; + + let res = dao.gauge_suite.orchestrator.list_gauges(None, None)?; + assert_eq!( + res.gauges, + vec![ + GaugeResponse { + id: 0, + title: "default-gauge".to_owned(), + adapter: dao.gauge_suite.adapter.addr_str()?, + epoch_size: EPOCH, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 7 * 86400, + reset: None, + total_epochs: None, + }, + GaugeResponse { + id: 1, + title: "default-gauge".to_owned(), + adapter: second_gauge_adapter.adapter.to_string(), + total_epochs: None, + epoch_size: EPOCH, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 7 * 86400, + reset: None, + } + ] + ); + + // update parameters on the first gauge + let fake_owner = mock.addr_make("not-owner"); + let new_epoch = EPOCH * 2; + let epoch_limit = 8u64; + let new_min_percent = Some(Decimal::percent(10)); + let new_max_options = 15; + let new_max_available_percentage = Some(Decimal::percent(5)); + dao.gauge_suite + .orchestrator + .call_as(&dao_addr) + .update_gauge( + 0, + Some(new_epoch), + Some(epoch_limit), + new_max_available_percentage, + Some(new_max_options), + new_min_percent, + )?; + + let res = dao.gauge_suite.orchestrator.list_gauges(None, None)?; + assert_eq!( + res.gauges, + vec![ + GaugeResponse { + id: 0, + title: "default-gauge".to_owned(), + adapter: dao.gauge_suite.adapter.addr_str()?, + epoch_size: new_epoch, + total_epochs: None, + min_percent_selected: new_min_percent, + max_options_selected: new_max_options, + max_available_percentage: new_max_available_percentage, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 7 * 86400, + reset: None, + }, + GaugeResponse { + id: 1, + title: "default-gauge".to_owned(), + adapter: second_gauge_adapter.adapter.to_string(), + epoch_size: EPOCH, + total_epochs: None, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 7 * 86400, + reset: None, + } + ] + ); + + // clean setting of min_percent_selected on second gauge + dao.gauge_suite + .orchestrator + .call_as(&dao_addr) + .update_gauge(1, None, None, None, None, Some(Decimal::zero()))?; + + let res = dao.gauge_suite.orchestrator.list_gauges(None, None)?; + assert_eq!( + res.gauges[1], + GaugeResponse { + id: 1, + title: "default-gauge".to_owned(), + adapter: second_gauge_adapter.adapter.to_string(), + epoch_size: EPOCH, + total_epochs: None, + min_percent_selected: None, + max_options_selected: 10, + max_available_percentage: None, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 7 * 86400, + reset: None, + } + ); + + // Not owner cannot update gauges + let err = dao + .gauge_suite + .orchestrator + .call_as(&fake_owner) + .update_gauge(0, None, None, None, None, Some(Decimal::zero())) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let err = dao + .gauge_suite + .orchestrator + .call_as(&dao_addr) + .update_gauge(0, Some(50), None, None, None, None) + .unwrap_err(); + assert_eq!(ContractError::EpochSizeTooShort {}, err.downcast().unwrap()); + + let err = dao + .gauge_suite + .orchestrator + .call_as(&dao_addr) + .update_gauge( + 0, + None, + None, + None, + Some(new_max_options), + Some(Decimal::one()), + ) + .unwrap_err(); + assert_eq!( + ContractError::MinPercentSelectedTooBig {}, + err.downcast().unwrap() + ); + let err = dao + .gauge_suite + .orchestrator + .call_as(&dao_addr) + .update_gauge(0, None, None, None, Some(0), None) + .unwrap_err(); + assert_eq!( + ContractError::MaxOptionsSelectedTooSmall {}, + err.downcast().unwrap() + ); + let err = dao + .gauge_suite + .orchestrator + .call_as(&dao_addr) + .update_gauge(0, None, None, Some(Decimal::percent(101)), None, None) + .unwrap_err(); + assert_eq!( + ContractError::MaxAvailablePercentTooBig {}, + err.downcast().unwrap() + ); + Ok(()) + } +} + +mod reset { + use super::*; + use crate::tests::gauges::helpers::RESET_EPOCH; + use gauge_orchestrator::{msg::ResetMigrationConfig, state::Reset}; + + #[test] + fn test_basic_gauge_reset() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make("voter2"); + + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let dao_addr = dao.dao_core.address()?; + // setup second gauge adapter with reset configuration + let mut second_gauge = + dao.init_adapter_return_config(&[voter1.as_str(), voter2.as_str()])?; + second_gauge.reset_epoch = Some(RESET_EPOCH); + dao.add_adapter_to_gauge(second_gauge)?; + + let gauge_id = 1; + + // vote for one of the options in gauge + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + // voter1 was option voted for with two 100 voting powers combined + assert_eq!( + dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes, + vec![(voter1.to_string(), Uint128::new(200u128))] + ); + + // cannot reset before epoch has passed + assert_eq!( + ContractError::ResetEpochNotPassed {}, + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .reset_gauge(10, gauge_id) + .unwrap_err() + .downcast() + .unwrap() + ); + + // reset + mock.wait_seconds(RESET_EPOCH)?; + + dao.gauge_suite.orchestrator.reset_gauge(100, gauge_id)?; + // check that gauge was reset + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?; + assert_eq!(selected.votes, vec![]); + assert_eq!( + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter1.clone())? + .vote, + None + ); + assert_eq!( + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter2.clone())? + .vote, + None + ); + // options should still be there + assert_eq!( + dao.gauge_suite + .orchestrator + .list_options(gauge_id, None, None) + .unwrap() + .options, + vec![ + (voter2.to_string(), Uint128::new(0u128)), + (voter1.to_string(), Uint128::new(0u128)) + ] + ); + + // actually execute + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + assert_eq!( + mock.balance(dao_addr.clone(), Some("ujuno".into()))?[0].amount, + Uint128::from(10000u128) + ); + // vote again + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter2.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + // check that vote counts + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?; + assert_eq!( + selected.votes, + vec![(voter2.to_string(), Uint128::new(100u128))] + ); + // another epoch is fine + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + assert_eq!( + mock.balance(&voter2, Some("ujuno".into()))?[0].amount, + Uint128::from(1000u128) + ); + + Ok(()) + } + #[test] + fn test_gauge_migrate_with_reset() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let dao_addr = dao.dao_core.address()?; + let gauge_addr = dao.gauge_suite.orchestrator.address()?; + // now let's migrate the gauge and make sure nothing breaks + mock.call_as(&dao_addr) + .migrate( + &MigrateMsg { + gauge_config: Some(vec![( + 0, + GaugeMigrationConfig { + reset: Some(ResetMigrationConfig { + reset_epoch: RESET_EPOCH, + next_reset: mock.block_info()?.time.seconds() - 1, + }), + next_epoch: None, + }, + )]), + }, + 0, + &gauge_addr.clone(), + ) + .unwrap_err(); + // migrate to reset epoch + mock.call_as(&dao_addr).migrate( + &MigrateMsg { + gauge_config: Some(vec![( + 0, + GaugeMigrationConfig { + reset: Some(ResetMigrationConfig { + reset_epoch: RESET_EPOCH, + next_reset: mock.block_info()?.time.seconds() + 100, + }), + next_epoch: None, + }, + )]), + }, + dao.gauge_suite.orchestrator.code_id()?, + &gauge_addr.clone(), + )?; + + // check that gauge was migrated + let res = dao.gauge_suite.orchestrator.gauge(0)?; + assert_eq!( + res, + GaugeResponse { + id: 0, + title: "default-gauge".to_owned(), + adapter: dao.gauge_suite.adapter.addr_str()?, + epoch_size: EPOCH, + total_epochs: None, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + is_stopped: false, + next_epoch: mock.block_info()?.time.seconds() + 7 * 86400, + reset: Some(Reset { + last: None, + reset_each: RESET_EPOCH, + next: mock.block_info()?.time.seconds() + 100, + }) + } + ); + + Ok(()) + } + + #[test] + fn test_epoch_limit() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make("voter2"); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let mut second_gauge = dao.init_minimal_adapter()?; + // set # of epochs gauge will run to 3 + second_gauge.total_epochs = Some(3); + dao.add_adapter_to_gauge(second_gauge)?; + let dao_addr = dao.dao_core.address()?; + let gauge_id = 1; + + // vote + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: dao_addr.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: dao_addr.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + // check that vote was tallied + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!( + selected, + vec![(dao_addr.to_string(), Uint128::new(200u128))] + ); + // move forward in time + mock.wait_seconds(EPOCH)?; + // execute epoch 1 + dao.run_epoch(mock.clone(), gauge_id)?; + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!( + selected, + vec![(dao_addr.to_string(), Uint128::new(200u128))] + ); + // move forward in time + mock.wait_seconds(EPOCH)?; + // execute epoch 2 + dao.run_epoch(mock.clone(), gauge_id)?; + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!( + selected, + vec![(dao_addr.to_string(), Uint128::new(200u128))] + ); + // move forward in time + mock.wait_seconds(EPOCH)?; + // execute epoch 3 + dao.run_epoch(mock.clone(), gauge_id)?; + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!( + selected, + vec![(dao_addr.to_string(), Uint128::new(200u128))] + ); + + // move forward in time + mock.wait_seconds(EPOCH)?; + let res = dao.gauge_suite.orchestrator.gauge(gauge_id)?; + assert_eq!(res.is_stopped, true); + + // try to execute epoch 4 + mock.call_as(&dao_addr) + .execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + ) + .unwrap_err(); + + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!( + selected, + vec![(dao_addr.to_string(), Uint128::new(200u128))] + ); + Ok(()) + } + #[test] + fn test_gauge_migrate_keeps_last_reset() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make("voter2"); + let gauge_id = 1; + let dao_addr = dao.dao_core.address()?; + let gauge_addr = dao.gauge_suite.orchestrator.address()?; + + // setup second gauge adapter with reset configuration + let mut second_gauge = + dao.init_adapter_return_config(&[voter1.as_str(), voter2.as_str()])?; + second_gauge.reset_epoch = Some(RESET_EPOCH); + dao.add_adapter_to_gauge(second_gauge)?; + + // reset once before migration + mock.wait_seconds(RESET_EPOCH)?; + dao.gauge_suite + .orchestrator + .call_as(&dao_addr) + .reset_gauge(1, gauge_id)?; + + let gauge = dao.gauge_suite.orchestrator.gauge(gauge_id)?; + assert_eq!( + gauge.reset.unwrap().last, + Some(mock.block_info()?.time.seconds()) + ); + + // now let's migrate the gauge and make sure nothing breaks + // migrate to reset epoch + mock.call_as(&dao_addr).migrate( + &MigrateMsg { + gauge_config: Some(vec![( + 0, + GaugeMigrationConfig { + reset: Some(ResetMigrationConfig { + reset_epoch: RESET_EPOCH, + next_reset: mock.block_info()?.time.seconds() + 100, + }), + next_epoch: None, + }, + )]), + }, + dao.gauge_suite.orchestrator.code_id()?, + &gauge_addr.clone(), + )?; + + // migrate + Ok(()) + } + #[test] + fn test_partial_reset() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make("voter2"); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + // setup second gauge adapter with reset configuration + let mut second_gauge = + dao.init_adapter_return_config(&[voter1.as_str(), voter2.as_str()])?; + second_gauge.reset_epoch = Some(RESET_EPOCH); + dao.add_adapter_to_gauge(second_gauge)?; + + // addresses + let dao_addr = dao.dao_core.address()?; + mock.add_balance(&dao_addr, coins(1000, "ujuno"))?; + let gauge_id = 1; + + // vote for the gauge options + // vote again + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter1.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter2.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + // start resetting + mock.wait_seconds(RESET_EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::ResetGauge { + gauge: gauge_id, + batch_size: 1, + }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + // try to vote during reset + assert_eq!( + ContractError::GaugeResetting(gauge_id), + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: voter2.to_string(), + weight: Decimal::one(), + }] + .into(), + ), + ) + .unwrap_err() + .downcast() + .unwrap() + ); + + // check selected set query + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?; + assert_eq!(selected.votes, vec![]); + // check votes list + let votes = dao + .gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)?; + assert_eq!(votes.votes, vec![]); + + // finish resetting + dao.gauge_suite.orchestrator.reset_gauge(1, gauge_id)?; + + Ok(()) + } +} + +mod tally { + use cw4::{MemberChangedHookMsg, MemberDiff}; + + use super::*; + // use dao_voting_cw4::msg::QueryMsgFns; + + fn defualt_voters(mock: MockBech32, number: Vec) -> anyhow::Result> { + let mut voters = vec![]; + + for n in 0..number.len() { + let weight = n as u64; + let voter = mock + .addr_make_with_balance(format!("voter{}", n + 2).as_str(), coins(1000, "ujuno"))?; + voters.push(MemberDiff { + key: voter.to_string(), + old: None, + new: Some(weight), + }) + } + Ok(voters) + } + #[test] + fn test_multiple_options_one_gauge() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + let voter1 = mock.addr_make("voter3"); + let voter2 = mock.addr_make("voter4"); + let voter3 = mock.addr_make("voter5"); + let voter4 = mock.addr_make("voter6"); + let voter5 = mock.addr_make("voter7"); + + // create new gauge with more members + let members = defualt_voters(mock.clone(), vec![600, 120, 130, 140, 150])?; + let cw4 = dao.cw4_vote.address()?; + let gauge2 = + dao.init_testing_adapter(&["option1", "option2", "option3", "option4", "option5"])?; + dao.add_adapter_to_gauge(gauge2)?; + let gauge_id = 1; + + mock.call_as(&cw4).execute( + &gauge_orchestrator::msg::ExecuteMsg::MemberChangedHook(MemberChangedHookMsg { + diffs: members, + }), + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + mock.wait_blocks(1)?; + + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: "option1".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: "option2".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + dao.gauge_suite.orchestrator.call_as(&voter3).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: "option3".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter4).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: "option4".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter5).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: "option5".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?; + assert_eq!( + selected.votes, + vec![ + ("option1".to_owned(), Uint128::new(600)), + ("option5".to_owned(), Uint128::new(150)), + ("option4".to_owned(), Uint128::new(140)), + ("option3".to_owned(), Uint128::new(130)), + ("option2".to_owned(), Uint128::new(120)) + ] + ); + + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some( + vec![GaugeVote { + option: "option2".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?; + assert_eq!( + selected.votes, + vec![ + ("option2".to_owned(), Uint128::new(720)), + ("option5".to_owned(), Uint128::new(150)), + ("option4".to_owned(), Uint128::new(140)), + ("option3".to_owned(), Uint128::new(130)), + ] + ); + + Ok(()) + } + #[test] + fn test_multiple_options_two_gauges() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + let voter1 = mock.addr_make("voter3"); + let voter2 = mock.addr_make("voter4"); + let voter3 = mock.addr_make("voter5"); + let voter4 = mock.addr_make("voter6"); + let voter5 = mock.addr_make("voter7"); + + let gauge2 = dao.init_adapter_return_config(&["option1", "option2"])?; + dao.add_adapter_to_gauge(gauge2)?; + mock.wait_blocks(1)?; + let gauge3 = dao.init_adapter_return_config(&["option3", "option4", "option5"])?; + dao.add_adapter_to_gauge(gauge3)?; + mock.wait_blocks(1)?; + + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + 1, + Some( + vec![GaugeVote { + option: "option2".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + 1, + Some( + vec![GaugeVote { + option: "option2".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter3).place_votes( + 2, + Some( + vec![GaugeVote { + option: "option3".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter4).place_votes( + 2, + Some( + vec![GaugeVote { + option: "option5".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter5).place_votes( + 2, + Some( + vec![GaugeVote { + option: "option5".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + let selected = dao.gauge_suite.orchestrator.selected_set(1)?; + assert_eq!( + selected.votes, + vec![("option2".to_owned(), Uint128::new(720)),] + ); + let selected = dao.gauge_suite.orchestrator.selected_set(2)?; + assert_eq!( + selected.votes, + vec![ + ("option5".to_owned(), Uint128::new(290)), + ("option3".to_owned(), Uint128::new(130)), + ] + ); + + Ok(()) + } + + #[test] + fn test_not_voted_options_are_not_selected() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + let voter1 = mock.addr_make("voter3"); + let voter2 = mock.addr_make("voter4"); + + let gauge = + dao.init_adapter_return_config(&["option1", "option2", "option3", "option4"])?; + dao.add_adapter_to_gauge(gauge)?; + + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + 1, + Some( + vec![GaugeVote { + option: "option1".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + 1, + Some( + vec![GaugeVote { + option: "option2".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + let selected = dao.gauge_suite.orchestrator.selected_set(1)?; + assert_eq!( + selected.votes, + vec![ + ("option1".to_owned(), Uint128::new(600)), + ("option2".to_owned(), Uint128::new(120)), + ] + ); + + // first voter changes vote to option2 + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + 1, + Some( + vec![GaugeVote { + option: "option2".to_string(), + weight: Decimal::one(), + }] + .into(), + ), + )?; + + let selected = dao.gauge_suite.orchestrator.selected_set(1)?; + assert_eq!( + selected.votes, + vec![("option2".to_owned(), Uint128::new(720)),] + ); + + Ok(()) + } +} + +mod voting { + // use std::vec; + + use crate::tests::gauges::helpers::{multi_vote, simple_vote}; + use cw4::{MemberChangedHookMsg, MemberDiff}; + use cw4_group::msg::ExecuteMsg as Cw4ExecuteMsg; + use dao_hooks::nft_stake::NftStakeChangedHookMsg; + use gauge_orchestrator::msg::VoteInfo; + + use dao_gauge_adapter::contract::ExecuteMsgFns; + + use super::*; + + #[test] + fn test_add_option() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + + let dao_addr = dao.dao_core.address()?; + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let not_voter = mock.addr_make_with_balance("not-voter", coins(1000, "ujuno"))?; + + // gauge returns list all options; it does query adapter at initialization + let options = dao.gauge_suite.orchestrator.list_options(0, None, None)?; + assert_eq!(options.options.len(), 3); + + // add moe valid options to gauge adapter + + dao.gauge_suite + .test_adapter + .call_as(&dao_addr) + .add_valid_option("addedoption1")?; + dao.gauge_suite + .test_adapter + .call_as(&dao_addr) + .add_valid_option("addedoption2")?; + + // Voting members can add options + dao.gauge_suite + .orchestrator + .call_as(&voter1) + .add_option(0, "addedoption1")?; + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .add_option(0, "addedoption2")?; + + // added options are automatically voted for by creators + let options = dao.gauge_suite.orchestrator.list_options(0, None, None)?; + assert_eq!( + options.options, + vec![ + ("addedoption1".to_owned(), Uint128::zero()), + ("addedoption2".to_owned(), Uint128::zero()), + (voter2.to_string(), Uint128::zero()), + (voter1.to_string(), Uint128::zero()), + (dao_addr.to_string(), Uint128::zero()), + ] + ); + + // add another valid option to gauge adapter + dao.gauge_suite + .test_adapter + .call_as(&dao_addr) + .add_valid_option("addedoption3")?; + // Non-voting members cannot add options + let err = dao + .gauge_suite + .orchestrator + .call_as(¬_voter) + .add_option(0, "addedoption3") + .unwrap_err(); + assert_eq!( + ContractError::NoVotingPower(not_voter.to_string()), + err.downcast().unwrap() + ); + + Ok(()) + } + #[test] + fn test_remove_option() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let dao_addr = dao.dao_core.address()?; + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let gauge_id = 0; + + // gauge returns list all options; it does query adapter at initialization + let options = dao + .gauge_suite + .orchestrator + .list_options(gauge_id, None, None)?; + assert_eq!(options.options.len(), 3); + + // add new valid options to the gauge adapter + dao.gauge_suite + .test_adapter + .call_as(&dao_addr) + .add_valid_option("addedoption1")?; + dao.gauge_suite + .test_adapter + .call_as(&dao_addr) + .add_valid_option("addedoption2")?; + + // Voting members can add options + dao.gauge_suite + .orchestrator + .call_as(&voter1) + .add_option(gauge_id, "addedoption1")?; + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .add_option(gauge_id, "addedoption2")?; + + let options = dao + .gauge_suite + .orchestrator + .list_options(gauge_id, None, None)?; + assert_eq!(options.options.len(), 5); + + // owner can remove an option that has been added already + dao.gauge_suite + .orchestrator + .call_as(&dao_addr) + .remove_option(gauge_id, "addedoption1")?; + // Anyone else cannot remove options + let err = dao + .gauge_suite + .orchestrator + .call_as(&voter1) + .remove_option(gauge_id, "addedoption2") + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + // one has been removed + let options = dao + .gauge_suite + .orchestrator + .list_options(gauge_id, None, None)?; + assert_eq!(options.options.len(), 4); + + // invalidate added option + mock.call_as(&dao_addr).execute( + &AdapterExecuteMsg::InvalidateOption { + option: "addedoption2".into(), + }, + &vec![], + &dao.gauge_suite.test_adapter.address()?, + )?; + // owner can remove an option that is no longer valid + dao.gauge_suite + .orchestrator + .call_as(&dao_addr) + .remove_option(gauge_id, "addedoption2")?; + // Both options are now removed + let options = dao + .gauge_suite + .orchestrator + .list_options(gauge_id, None, None)?; + assert_eq!(options.options.len(), 3); + + Ok(()) + } + + #[test] + fn test_vote_for_option() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let dao_addr = dao.dao_core.address()?; + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let gauge_id = 0; + let adapter = dao.gauge_suite.test_adapter.address()?; + + // vote for option from adapter (voting members are by default + // options in adapter in this test suite) + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::percent(90), + }]), + )?; + assert_eq!( + VoteInfo { + voter: voter1.to_string(), + votes: vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::percent(90), + }], + cast: Some(mock.block_info()?.time.seconds()), + }, + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter1.clone())? + .vote + .unwrap(), + ); + // check tally is proper + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?; + assert_eq!( + selected.votes, + vec![(voter1.to_string(), Uint128::from(90u128))] + ); + // add new valid options to the gauge adapter + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option1".into(), + }, + &vec![], + &adapter.clone(), + )?; + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option2".into(), + }, + &vec![], + &adapter.clone(), + )?; + + // change vote for option added through gauge + dao.gauge_suite + .orchestrator + .call_as(&voter1) + .add_option(gauge_id, "option1")?; + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .add_option(gauge_id, "option2")?; + // voter2 drops vote as well + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![ + gauge_orchestrator::state::Vote { + option: "option1".to_string(), + weight: Decimal::percent(50), + }, + gauge_orchestrator::state::Vote { + option: "option2".to_string(), + weight: Decimal::percent(50), + }, + ]), + )?; + assert_eq!( + vec![ + multi_vote( + &voter2.to_string(), + &[("option1", 50), ("option2", 50)], + mock.block_info()?.time.seconds(), + ), + simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 90, + mock.block_info()?.time.seconds() + ), + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes + ); + + // placing vote again overwrites previous ones + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: "option1".to_string(), + weight: Decimal::percent(90), + }]), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: "option1".to_string(), + weight: Decimal::percent(90), + }]), + )?; + assert_eq!( + vec![ + simple_vote( + &voter2.to_string(), + "option1", + 90, + mock.block_info()?.time.seconds() + ), + simple_vote( + &voter1.to_string(), + "option1", + 90, + mock.block_info()?.time.seconds() + ), + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes, + ); + + // vote for non-existing option + let err = dao + .gauge_suite + .orchestrator + .call_as(&voter2) + .place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: "option420".to_string(), + weight: Decimal::percent(90), + }]), + ) + .unwrap_err(); + assert_eq!( + ContractError::OptionDoesNotExists { + option: "option420".to_owned(), + gauge_id, + }, + err.downcast().unwrap() + ); + + Ok(()) + } + + #[test] + fn test_remove_vote() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let gauge_id = 0; + + // vote for option from adapter (voting members are by default + // options in adapter in this test suite) + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::one(), + }]), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::one(), + }]), + )?; + + assert_eq!( + vec![ + simple_vote( + &voter2.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() + ), + simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() + ), + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes + ); + + // remove vote + dao.gauge_suite + .orchestrator + .call_as(&voter1) + .place_votes(gauge_id, None)?; + assert_eq!( + vec![simple_vote( + &voter2.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() + )], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes + ); + assert_eq!( + None, + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter1.to_string())? + .vote + ); + assert_eq!( + Some(simple_vote( + &voter2.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() + )), + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter2.to_string())? + .vote + ); + + // remove nonexisting vote + let err = dao + .gauge_suite + .orchestrator + .call_as(&voter1) + .place_votes(gauge_id, None) + .unwrap_err(); + assert_eq!( + ContractError::CannotRemoveNonexistingVote {}, + err.downcast().unwrap() + ); + + Ok(()) + } + #[test] + fn test_votes_stays_the_same_after_execution() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let dao_addr = dao.dao_core.address()?; + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let gauge_id = 0; + + // vote for one of the options in gauge + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::one(), + }]), + )?; + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::one(), + }]), + )?; + + // voter1 was option voted for with two 100 voting powers combined + let selected = dao.gauge_suite.orchestrator.selected_set(gauge_id)?; + assert_eq!( + selected.votes, + vec![(voter1.to_string(), Uint128::new(200))] + ); + // before advancing specified epoch tally won't get sampled + mock.wait_seconds(EPOCH)?; + assert_eq!( + vec![ + simple_vote( + &voter2.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() - EPOCH + ), + simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() - EPOCH + ) + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes + ); + + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + assert_eq!( + vec![ + simple_vote( + &voter2.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() - EPOCH + ), + simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() - EPOCH + ), + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes + ); + + assert_eq!( + Some(simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() - EPOCH + )), + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter1.to_string())? + .vote + ); + assert_eq!( + Some(simple_vote( + &voter2.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() - EPOCH + )), + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter2.to_string())? + .vote + ); + + Ok(()) + } + #[test] + fn test_vote_for_max_capped_option() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.default_gauge_setup(mock.clone())?; + let dao_addr = dao.dao_core.address()?; + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + + let mut gauge = dao.init_adapter_return_config(&[voter1.as_str(), voter2.as_str()])?; + gauge.max_available_percentage = Some(Decimal::percent(10)); + dao.add_adapter_to_gauge(gauge)?; + let gauge_id = 1; + + // wait until epoch passes + mock.wait_seconds(EPOCH)?; + + // add more valid options to gauge adapter + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option1".into(), + }, + &vec![], + &dao.gauge_suite.test_adapter.address()?, + )?; + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option2".into(), + }, + &vec![], + &dao.gauge_suite.test_adapter.address()?, + )?; + + // change vote for option added through gauge + dao.gauge_suite + .orchestrator + .call_as(&voter1) + .add_option(gauge_id, "option1")?; + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .add_option(gauge_id, "option2")?; + + // vote 100% voting power on 'voter1' option (100 weight) + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: "option1".to_string(), + weight: Decimal::one(), + }]), + )?; + + // vote 10% voting power on 'voter2' option (10 weight) + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: "option2".to_string(), + weight: Decimal::percent(10), + }]), + )?; + + assert_eq!( + vec![ + multi_vote( + &voter2.to_string(), + &[("option2", 10)], + mock.block_info()?.time.seconds(), + ), + multi_vote( + &voter1.to_string(), + &[("option1", 100)], + mock.block_info()?.time.seconds(), + ), + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes + ); + + let selected_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + // Despite 'option1' having 100 voting power and option2 having 10 voting power, + // because of max vote cap set to 10% now 'option1' will have its power decreased to 10% * 110 + // 'option2' stays at 10 voting power as it was below 10% of total votes + assert_eq!( + selected_set, + vec![ + ("option1".to_owned(), Uint128::new(11)), + ("option2".to_owned(), Uint128::new(10)) + ] + ); + + Ok(()) + } + #[test] + fn test_membership_voting_power_change() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.custom_gauge_setup( + mock.clone(), + vec![coin(100, voter1.to_string()), coin(200, voter2.to_string())], + &[voter1.as_str(), voter2.as_str()], + )?; + let dao_addr = dao.dao_core.address()?; + let test_adapter = dao.gauge_suite.test_adapter.address()?; + let gauge_id = 0; + + // vote for option from adapter (voting members are by default + // options in adapter in this test suite) + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::percent(90), + }]), + )?; + + assert_eq!( + Some(simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 90, + mock.block_info()?.time.seconds() + )), + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter1.to_string())? + .vote + ); + // check tally is proper + let selected_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!(selected_set, vec![(voter1.to_string(), Uint128::new(90))]); + // add new valid options to the gauge adapter + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option1".into(), + }, + &vec![], + &test_adapter.clone(), + )?; + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option2".into(), + }, + &vec![], + &test_adapter.clone(), + )?; + + // change vote for option added through gauge + dao.gauge_suite + .orchestrator + .call_as(&voter1) + .add_option(gauge_id, "option1")?; + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .add_option(gauge_id, "option2")?; + + // voter2 drops vote1 + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![ + gauge_orchestrator::state::Vote { + option: "option1".to_string(), + weight: Decimal::percent(50), + }, + gauge_orchestrator::state::Vote { + option: "option2".to_string(), + weight: Decimal::percent(50), + }, + ]), + )?; + assert_eq!( + vec![ + multi_vote( + &voter2.to_string(), + &[("option1", 50), ("option2", 50)], + mock.block_info()?.time.seconds(), + ), + simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 90, + mock.block_info()?.time.seconds() + ), + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes + ); + + // execute after epoch passes + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + // confirm gauge recieved vote + let pre_voter1_takeover_gauge_set = + dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + + // voter1 option is least popular + assert_eq!( + pre_voter1_takeover_gauge_set, + vec![ + ("option2".to_string(), Uint128::new(100)), + ("option1".to_string(), Uint128::new(100)), + (voter1.to_string(), Uint128::new(90)), + ] + ); + + // Force update members, giving voter 1 more power + mock.call_as(&dao_addr).execute( + &Cw4ExecuteMsg::UpdateMembers { + remove: vec![], + add: vec![Member { + addr: voter1.to_string(), + weight: 1000, + }], + }, + &vec![], + &dao.cw4_vote.group_contract()?, + )?; + let cw4 = dao.cw4_vote.address()?; + dao.gauge_suite + .orchestrator + .call_as(&cw4) + .member_changed_hook(MemberChangedHookMsg::new(vec![MemberDiff { + key: voter1.to_string(), + old: Some(100u64), + new: Some(1000u64), + }]))?; + mock.wait_blocks(1)?; + + let current_gauge_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + + // Currect selected set should be different than before voter1 got power + assert_ne!(pre_voter1_takeover_gauge_set, current_gauge_set); + + // Voter1 option is now most popular + assert_eq!( + current_gauge_set, + vec![ + (voter1.to_string(), Uint128::new(900)), + ("option2".to_string(), Uint128::new(100)), + ("option1".to_string(), Uint128::new(100)) + ] + ); + + // Execute after epoch passes + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + // Force update members, kick out voter 1 + mock.call_as(&dao_addr).execute( + &Cw4ExecuteMsg::UpdateMembers { + remove: vec![voter1.to_string()], + add: vec![], + }, + &vec![], + &dao.cw4_vote.group_contract()?, + )?; + dao.gauge_suite + .orchestrator + .call_as(&cw4) + .member_changed_hook(MemberChangedHookMsg::new(vec![MemberDiff { + key: voter1.to_string(), + old: Some(1000u64), + new: None, + }]))?; + mock.wait_blocks(1)?; + + // Execute after epoch passes + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + let current_gauge_set = dao + .gauge_suite + .orchestrator + .last_executed_set(gauge_id)? + .votes; + // Voter1 option is now most popular + assert_eq!( + current_gauge_set, + Some(vec![ + ("option2".to_string(), Uint128::new(100)), + ("option1".to_string(), Uint128::new(100)) + ]) + ); + + Ok(()) + } + #[test] + fn test_token_staking_voting_power_change() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.custom_gauge_setup( + mock.clone(), + vec![coin(100, voter1.to_string()), coin(200, voter2.to_string())], + &[voter1.as_str(), voter2.as_str()], + )?; + let dao_addr = dao.dao_core.address()?; + let test_adapter = dao.gauge_suite.test_adapter.address()?; + let gauge_id = 0; + + // vote for option from adapter (voting members are by default + // options in adapter in this test suite) + + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::percent(90), + }]), + )?; + + assert_eq!( + Some(simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 90, + mock.block_info()?.time.seconds() + )), + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter1.to_string())? + .vote + ); + // check tally is proper + let selected_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!(selected_set, vec![(voter1.to_string(), Uint128::new(90))]); + // add new valid options to the gauge adapter + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option1".into(), + }, + &vec![], + &test_adapter.clone(), + )?; + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option2".into(), + }, + &vec![], + &test_adapter.clone(), + )?; + + // change vote for option added through gauge + dao.gauge_suite + .orchestrator + .call_as(&voter1) + .add_option(gauge_id, "option1")?; + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .add_option(gauge_id, "option2")?; + + // voter2 drops vote1 + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![ + gauge_orchestrator::state::Vote { + option: "option1".to_string(), + weight: Decimal::percent(50), + }, + gauge_orchestrator::state::Vote { + option: "option2".to_string(), + weight: Decimal::percent(50), + }, + ]), + )?; + assert_eq!( + vec![ + multi_vote( + &voter2.to_string(), + &[("option1", 50), ("option2", 50)], + mock.block_info()?.time.seconds(), + ), + simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 90, + mock.block_info()?.time.seconds() + ), + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes, + ); + + // execute after epoch passes + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + mock.next_block()?; + + // confirm gauge recieved vote + let selected_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + + // voter1 option is least popular + assert_eq!( + selected_set, + vec![ + ("option2".to_string(), Uint128::new(100)), + ("option1".to_string(), Uint128::new(100)), + (voter1.to_string(), Uint128::new(90)) + ] + ); + + // Use hook caller to mock voter1 staking + let cw4 = dao.cw4_vote.address()?; + dao.gauge_suite + .orchestrator + .call_as(&cw4) + .stake_change_hook(dao_hooks::stake::StakeChangedHookMsg::Stake { + addr: voter1.clone(), + amount: Uint128::new(900), + })?; + + // Currect selected set should be different than before voter1 got power + let current_gauge_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!( + current_gauge_set, + vec![ + (voter1.to_string(), Uint128::new(900)), + ("option2".to_string(), Uint128::new(100)), + ("option1".to_string(), Uint128::new(100)) + ] + ); + + // Execute after epoch passes + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + + // Mock voter 1 unstaking + dao.gauge_suite + .orchestrator + .call_as(&cw4) + .stake_change_hook(dao_hooks::stake::StakeChangedHookMsg::Unstake { + addr: voter1.clone(), + amount: Uint128::new(1000), + })?; + mock.next_block()?; + + // Currect selected set should be different than before voter1 got power + let current_gauge_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!( + current_gauge_set, + vec![ + ("option2".to_string(), Uint128::new(100)), + ("option1".to_string(), Uint128::new(100)) + ] + ); + + Ok(()) + } + #[test] + fn test_nft_staking_voting_power_change() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let voter1 = mock.addr_make_with_balance("voter1", coins(1000, "ujuno"))?; + let voter2 = mock.addr_make_with_balance("voter2", coins(1000, "ujuno"))?; + let mut dao = DaoDaoCw4Gauge::new(mock.clone()); + dao.upload_with_cw4(mock.clone())?; + dao.custom_gauge_setup( + mock.clone(), + vec![coin(1, voter1.to_string()), coin(2, voter2.to_string())], + &[voter1.as_str(), voter2.as_str()], + )?; + let dao_addr = dao.dao_core.address()?; + let test_adapter = dao.gauge_suite.test_adapter.address()?; + let cw4 = dao.cw4_vote.address()?; + let gauge_id = 0; + + // vote for option from adapter (voting members are by default + // options in adapter in this test suite) + dao.gauge_suite.orchestrator.call_as(&voter1).place_votes( + gauge_id, + Some(vec![gauge_orchestrator::state::Vote { + option: voter1.to_string(), + weight: Decimal::percent(100), + }]), + )?; + + assert_eq!( + Some(simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() + )), + dao.gauge_suite + .orchestrator + .vote(gauge_id, voter1.to_string())? + .vote + ); + // check tally is proper + let selected_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!(selected_set, vec![(voter1.to_string(), Uint128::one())]); + // add new valid options to the gauge adapter + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option1".into(), + }, + &vec![], + &test_adapter.clone(), + )?; + mock.call_as(&dao_addr).execute( + &dao_gauge_adapter::contract::ExecuteMsg::AddValidOption { + option: "option2".into(), + }, + &vec![], + &test_adapter.clone(), + )?; + + // change vote for option added through gauge + dao.gauge_suite + .orchestrator + .call_as(&voter1) + .add_option(gauge_id, "option1")?; + dao.gauge_suite + .orchestrator + .call_as(&voter2) + .add_option(gauge_id, "option2")?; + + // voter2 drops vote1 + dao.gauge_suite.orchestrator.call_as(&voter2).place_votes( + gauge_id, + Some(vec![ + gauge_orchestrator::state::Vote { + option: "option1".to_string(), + weight: Decimal::percent(50), + }, + gauge_orchestrator::state::Vote { + option: "option2".to_string(), + weight: Decimal::percent(50), + }, + ]), + )?; + assert_eq!( + vec![ + multi_vote( + &voter2.to_string(), + &[("option1", 50), ("option2", 50)], + mock.block_info()?.time.seconds(), + ), + simple_vote( + &voter1.to_string(), + &voter1.to_string(), + 100, + mock.block_info()?.time.seconds() + ), + ], + dao.gauge_suite + .orchestrator + .list_votes(gauge_id, None, None)? + .votes, + ); + + // execute after epoch passes + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + mock.next_block()?; + + // confirm gauge recieved vote + let selected_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + + // voter1 option is least popular + assert_eq!( + selected_set, + vec![ + ("option2".to_string(), Uint128::new(1)), + ("option1".to_string(), Uint128::new(1)), + (voter1.to_string(), Uint128::new(1)), + ] + ); + + // Use hook caller to mock voter1 staking + dao.gauge_suite + .orchestrator + .call_as(&cw4) + .nft_stake_change_hook(NftStakeChangedHookMsg::Stake { + addr: voter1.clone(), + token_id: "1".to_string(), + })?; + + mock.next_block()?; + + // Currect selected set should be different than before voter1 got power + let current_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + + // voter1 option is least popular + assert_ne!(current_set, selected_set); + assert_eq!( + current_set, + vec![ + (voter1.to_string(), Uint128::new(2)), + ("option2".to_string(), Uint128::new(1)), + ("option1".to_string(), Uint128::new(1)), + ] + ); + + // execute after epoch passes + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + mock.next_block()?; + + // Mock voter1 unstaking 2 nfts + + dao.gauge_suite + .orchestrator + .call_as(&cw4) + .nft_stake_change_hook(NftStakeChangedHookMsg::Unstake { + addr: voter1.clone(), + token_ids: vec!["1".to_string(), "2".to_string()], + })?; + mock.next_block()?; + + // execute after epoch passes + mock.wait_seconds(EPOCH)?; + mock.call_as(&dao_addr).execute( + &GaugeExecuteMsg::Execute { gauge: gauge_id }, + &vec![], + &dao.gauge_suite.orchestrator.address()?, + )?; + mock.next_block()?; + + // Currect selected set should be different than before voter1 got power + let current_gauge_set = dao.gauge_suite.orchestrator.selected_set(gauge_id)?.votes; + assert_eq!( + current_gauge_set, + vec![ + ("option2".to_string(), Uint128::new(1)), + ("option1".to_string(), Uint128::new(1)) + ] + ); + + Ok(()) + } + // // todo: test on ohnft nft hooks + // // todo: test on bitsong fantoken hooks + // // todo: test on omniflix nft hooks +} diff --git a/scripts/src/tests/gauges/suite.rs b/scripts/src/tests/gauges/suite.rs new file mode 100644 index 000000000..4028148fc --- /dev/null +++ b/scripts/src/tests/gauges/suite.rs @@ -0,0 +1,245 @@ +use crate::{ + tests::{daos::voting::dao_cw4_voting_template, gauges::helpers::EPOCH}, + DaoDaoCw4Gauge, +}; +use cosmwasm_std::{coin, coins, to_json_binary, Decimal}; +use cw4::Member; +use cw_orch::{anyhow, prelude::*}; +use dao_interface::{ + msg::ExecuteMsg as CoreExecuteMsg, + state::{Admin, ModuleInstantiateInfo}, +}; +use gauge_adapter::msg::AssetUnchecked; +use gauge_orchestrator::msg::{ + ExecuteMsg as GaugeExecuteMsg, ExecuteMsgFns as OrchExecuteMsgFns, GaugeConfig, +}; + +impl DaoDaoCw4Gauge { + pub fn upload_with_cw4(&mut self, mock: MockBech32) -> Result { + self.upload()?; + // also upload cw4 group + let cw4 = mock + .upload_custom( + "cw4", + Box::new(ContractWrapper::new_with_empty( + cw4_group::contract::execute, + cw4_group::contract::instantiate, + cw4_group::contract::query, + )), + )? + .uploaded_code_id()?; + self.cw4_group = Some(cw4); + Ok(cw4) + } + pub fn custom_gauge_setup( + &mut self, + mock: MockBech32, + dao_members: Vec, + options: &[&str], + ) -> Result<(), CwOrchError> { + let init_members = self.custom_initial_members(dao_members)?; + // create dao + let dao_modules = dao_cw4_voting_template(mock.clone(), self, init_members)?; + // set contracts to cw-orch state + self.set_dao_module_addrs(dao_modules[1].clone(), dao_modules[0].clone())?; + // create gauge adapter + let gauge_config = self.init_testing_adapter(&options)?; + let adapter = Addr::unchecked(gauge_config.adapter.clone()); + // create orchestrator & add to DAO + let gauge = self.add_gauge_to_dao(mock.clone(), vec![gauge_config])?; + // set gauges to cw-orch suite + self.set_gauge_suite_addrs(gauge.clone(), adapter)?; + mock.add_balance(&self.dao_core.address()?, coins(10000, "ujuno"))?; + + Ok(()) + } + pub fn default_gauge_setup(&mut self, mock: MockBech32) -> Result<(), CwOrchError> { + let voter1 = mock.addr_make("voter1"); + let voter2 = mock.addr_make("voter2"); + + let init_members = self.default_inital_members(mock.clone())?; + // create dao + let dao_modules = dao_cw4_voting_template(mock.clone(), self, init_members)?; + // set contracts to cw-orch state + self.set_dao_module_addrs(dao_modules[1].clone(), dao_modules[0].clone())?; + let dao_addr = self.dao_core.addr_str()?; + // create gauge adapter + let default_options = vec![voter1.as_str(), voter2.as_str(), &dao_addr]; + let gauge_config = self.init_testing_adapter(&default_options)?; + let adapter = Addr::unchecked(gauge_config.adapter.clone()); + // create orchestrator & add to DAO + let gauge = self.add_gauge_to_dao(mock.clone(), vec![gauge_config])?; + // set gauges to cw-orch suite + self.set_gauge_suite_addrs(gauge.clone(), adapter)?; + mock.add_balance(&self.dao_core.address()?, coins(10000, "ujuno"))?; + + Ok(()) + } + pub fn set_dao_module_addrs(&mut self, vote: Addr, proposal: Addr) -> anyhow::Result<()> { + self.cw4_vote.set_default_address(&vote); + self.prop_single.set_default_address(&proposal); + Ok(()) + } + pub fn set_gauge_suite_addrs(&mut self, gauge: Addr, adapter: Addr) -> anyhow::Result<()> { + self.gauge_suite.orchestrator.set_default_address(&gauge); + self.gauge_suite.adapter.set_default_address(&adapter); + Ok(()) + } + + pub fn custom_initial_members(&self, members: Vec) -> anyhow::Result> { + let mut res: Vec = vec![]; + for member in members { + res.push(Member { + addr: member.denom, + weight: member.amount.u128().try_into().unwrap(), + }) + } + + Ok(res) + } + pub fn default_inital_members(&self, mock: MockBech32) -> anyhow::Result> { + let mut res: Vec = vec![]; + let members = vec![ + coin(100, &mock.sender.to_string()), + coin(100, mock.addr_make("voter1")), + coin(100, mock.addr_make("voter2")), + coin(600, mock.addr_make("voter3")), + coin(120, mock.addr_make("voter4")), + coin(130, mock.addr_make("voter5")), + coin(140, mock.addr_make("voter6")), + coin(150, mock.addr_make("voter7")), + ]; + + for member in members { + res.push(Member { + addr: member.denom, + weight: member.amount.u128().try_into().unwrap(), + }) + } + Ok(res) + } + + pub fn init_testing_adapter(&self, options: &[&str]) -> anyhow::Result { + // init adapter + let adapter = self.gauge_suite.test_adapter.instantiate( + &dao_gauge_adapter::contract::InstantiateMsg { + options: options.iter().map(|s| s.to_string()).collect(), + to_distribute: coin(1000, "ujuno"), + }, + Some(&self.dao_core.address()?), + None, + )?; + Ok(GaugeConfig { + title: "default-gauge".to_owned(), + adapter: adapter.instantiated_contract_address()?.to_string(), + epoch_size: EPOCH, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + reset_epoch: None, + total_epochs: None, + }) + } + pub fn init_minimal_adapter(&self) -> anyhow::Result { + // init adapter + let adapter = self.gauge_suite.adapter.instantiate( + &gauge_adapter::msg::InstantiateMsg { + admin: self.dao_core.address()?.to_string(), + required_deposit: None, + community_pool: self.dao_core.address()?.to_string(), + reward: AssetUnchecked::new_native("ujuno", 1000u128), + }, + Some(&self.dao_core.address()?), + None, + )?; + Ok(GaugeConfig { + title: "default-gauge".to_owned(), + adapter: adapter.instantiated_contract_address()?.to_string(), + epoch_size: EPOCH, + min_percent_selected: Some(Decimal::percent(5)), + max_options_selected: 10, + max_available_percentage: None, + reset_epoch: None, + total_epochs: None, + }) + } + + pub fn init_gauge(&self) -> anyhow::Result { + // init gauge + Ok(self + .gauge_suite + .orchestrator + .instantiate( + &gauge_orchestrator::msg::InstantiateMsg { + voting_powers: self.cw4_vote.addr_str()?, + hook_caller: self.cw4_vote.addr_str()?, + owner: self.dao_core.addr_str()?, + gauges: None, + }, + Some(&self.dao_core.address()?), + None, + )? + .instantiated_contract_address()?) + } + + pub fn add_gauge_to_dao( + &self, + mock: MockBech32, + gauge_config: Vec, + ) -> anyhow::Result { + let dao_addr = self.dao_core.address()?; + let cw4_addr = self.cw4_vote.address()?; + + let gauge = mock + .call_as(&dao_addr) + .execute( + &CoreExecuteMsg::UpdateProposalModules { + to_add: vec![ModuleInstantiateInfo { + code_id: self.gauge_suite.orchestrator.code_id()?, + msg: to_json_binary(&gauge_orchestrator::msg::InstantiateMsg { + voting_powers: self.cw4_vote.address()?.to_string(), + hook_caller: cw4_addr.to_string(), + owner: self.dao_core.address()?.to_string(), + gauges: gauge_config.into(), + })?, + admin: Some(Admin::Address { + addr: self.dao_core.address()?.to_string(), + }), + label: "CW4 Voting Contract".to_owned(), + funds: vec![], + }], + to_disable: vec![], + }, + &vec![], + &dao_addr, + )? + .event_attr_value("wasm", "prop_module")?; + + Ok(Addr::unchecked(gauge)) + } + + /// instantiate an adapter contract and return its configuration, including the contract addr. + pub fn init_adapter_return_config(&self, options: &[&str]) -> anyhow::Result { + let adapter = self.init_testing_adapter(options)?; + Ok(adapter) + } + /// adds an adapter to the existing gauge orchestrator + pub fn add_adapter_to_gauge(&self, adapter: GaugeConfig) -> anyhow::Result<()> { + let dao_addr = self.dao_core.address()?; + self.gauge_suite + .orchestrator + .call_as(&dao_addr) + .create_gauge(adapter)?; + Ok(()) + } + + pub fn run_epoch(&self, mock: MockBech32, id: u64) -> anyhow::Result<()> { + let dao = self.dao_core.address()?; + mock.call_as(&dao).execute( + &GaugeExecuteMsg::Execute { gauge: id }, + &vec![], + &self.gauge_suite.orchestrator.address()?, + )?; + Ok(()) + } +} diff --git a/scripts/src/tests/mod.rs b/scripts/src/tests/mod.rs new file mode 100644 index 000000000..40b79027a --- /dev/null +++ b/scripts/src/tests/mod.rs @@ -0,0 +1,30 @@ +use cw_orch::{ + anyhow::{self}, + prelude::*, +}; +use dao_voting_cw4::msg::InstantiateMsg as Cw4VotingInitMsg; + +use crate::DaoDao; + +mod daos; +// mod deploy; +mod distribution; +mod external; +mod gauges; +mod propose; +mod staking; +mod voting; + +pub(crate) const PREFIX: &str = "mock"; +pub(crate) const ADMIN: &str = "admin"; +// pub(crate) const DENOM: &str = "juno"; +// pub(crate) const DAO1: &str = "dao1"; + +#[test] +fn test_dao_suite() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let admin = mock.addr_make(ADMIN); + let _app = DaoDao::deploy_on(mock.clone(), admin.clone())?; + mock.next_block().unwrap(); + Ok(()) +} diff --git a/scripts/src/tests/propose/mod.rs b/scripts/src/tests/propose/mod.rs new file mode 100644 index 000000000..5a4c29255 --- /dev/null +++ b/scripts/src/tests/propose/mod.rs @@ -0,0 +1,15 @@ +use cw_orch::{anyhow, prelude::*}; +use crate::{ + propose::{DaoPreProposeSuite, DaoProposalSuite}, + tests::{ADMIN, PREFIX}, +}; + +#[test] +fn test_proposals_suite() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let admin = mock.addr_make(ADMIN); + let _pre_prop_suite = DaoPreProposeSuite::deploy_on(mock.clone(), admin.clone())?; + let _props = DaoProposalSuite::deploy_on(mock.clone(), admin.clone())?; + mock.next_block().unwrap(); + Ok(()) +} diff --git a/scripts/src/tests/staking/mod.rs b/scripts/src/tests/staking/mod.rs new file mode 100644 index 000000000..b3c6db344 --- /dev/null +++ b/scripts/src/tests/staking/mod.rs @@ -0,0 +1,15 @@ +use cw_orch::{anyhow, prelude::*}; + +use crate::{ + tests::{ADMIN, PREFIX}, + DaoStakingSuite, +}; + +#[test] +fn test_staking_suite() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let admin = mock.addr_make(ADMIN); + let _app = DaoStakingSuite::deploy_on(mock.clone(), admin.clone())?; + mock.next_block().unwrap(); + Ok(()) +} diff --git a/scripts/src/tests/voting/cw721_staked.rs b/scripts/src/tests/voting/cw721_staked.rs new file mode 100644 index 000000000..4c774ca34 --- /dev/null +++ b/scripts/src/tests/voting/cw721_staked.rs @@ -0,0 +1 @@ +mod test {} \ No newline at end of file diff --git a/scripts/src/tests/voting/mod.rs b/scripts/src/tests/voting/mod.rs new file mode 100644 index 000000000..fb2a24718 --- /dev/null +++ b/scripts/src/tests/voting/mod.rs @@ -0,0 +1,15 @@ +use cw_orch::{anyhow, prelude::*}; +use crate::{ + tests::{ADMIN, PREFIX}, + DaoVotingSuite, +}; +mod cw721_staked; + +#[test] +fn test_voting_suite() -> anyhow::Result<()> { + let mock = MockBech32::new(PREFIX); + let admin = mock.addr_make(ADMIN); + let _app = DaoVotingSuite::deploy_on(mock.clone(), admin.clone())?; + mock.next_block().unwrap(); + Ok(()) +} diff --git a/scripts/src/voting.rs b/scripts/src/voting.rs new file mode 100644 index 000000000..d0b011499 --- /dev/null +++ b/scripts/src/voting.rs @@ -0,0 +1,32 @@ +use cw_orch::prelude::*; +use dao_cw_orch::*; + +pub struct DaoVotingSuite { + pub voting_cw4: DaoVotingCw4, + pub voting_cw20_staked: DaoVotingCw20Staked, + pub voting_cw721_roles: DaoVotingCw721Roles, + pub voting_cw721_staked: DaoVotingCw721Staked, + // pub voting_onft_staked: DaoVotingONftStaked, + pub voting_token_staked: DaoVotingTokenStaked, +} + +impl DaoVotingSuite { + pub fn new(chain: Chain) -> DaoVotingSuite { + DaoVotingSuite:: { + voting_cw4: DaoVotingCw4::new("voting_cw4", chain.clone()), + voting_cw20_staked: DaoVotingCw20Staked::new("voting_cw20_staked", chain.clone()), + voting_cw721_roles: DaoVotingCw721Roles::new("voting_cw721_roles", chain.clone()), + voting_cw721_staked: DaoVotingCw721Staked::new("voting_cw721_staked", chain.clone()), + voting_token_staked: DaoVotingTokenStaked::new("voting_token_staked", chain.clone()), + } + } + + pub fn upload(&self) -> Result<(), CwOrchError> { + self.voting_cw4.upload()?; + self.voting_cw20_staked.upload()?; + self.voting_cw721_roles.upload()?; + self.voting_cw721_staked.upload()?; + self.voting_token_staked.upload()?; + Ok(()) + } +}