diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 5a0378cec..fca7b1d82 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -17,11 +17,11 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 - - name: Install latest stable toolchain + - name: Install latest nightly toolchain uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable + toolchain: nightly-2023-02-02 target: wasm32-unknown-unknown override: true diff --git a/Cargo.lock b/Cargo.lock index 98eff573b..7ca98ee44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,18 +15,18 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "assert_matches" @@ -36,9 +36,9 @@ checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" [[package]] name = "async-stream" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad445822218ce64be7a341abfb0b1ea43b5c23aa83902542a4542e78309d8e5e" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", @@ -47,24 +47,24 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4655ae1a7b0cdf149156f780c5bf3f1352bc53cbd9e0a361a7ef7b22947e965" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", ] [[package]] name = "async-trait" -version = "0.1.67" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ea188f25f0255d8f92797797c97ebf5631fa88178beb1a46fdf5622c9a00e4" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.5", + "syn 2.0.18", ] [[package]] @@ -86,9 +86,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.12" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f8ccfd9221ee7d1f3d4b33e1f8319b3a81ed8f61f2ea40b37b859794b4491" +checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39" dependencies = [ "async-trait", "axum-core", @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f958c80c248b34b9a877a643811be8dbca03ca5ba827f2b63baf3a81e5fc4e" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", "bytes", @@ -223,9 +223,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "byteorder" @@ -291,9 +291,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cosm-orc" @@ -333,7 +333,7 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673d31bd830c0772d78545de20d975129b6ab2f7db4e4e9313c3b8777d319194" dependencies = [ - "prost 0.11.8", + "prost 0.11.9", "prost-types", "tendermint-proto 0.26.0", "tonic", @@ -345,7 +345,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4776e787b24d9568dd61d3237eeb4eb321d622fb881b858c7b82806420e87d4" dependencies = [ - "prost 0.11.8", + "prost 0.11.9", "prost-types", "tendermint-proto 0.27.0", "tonic", @@ -374,11 +374,11 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22add0f9b2a5416df98c1d0248a8d8eedb882c38fbf0c5052b64eebe865df6d" +checksum = "41c0e41be7e6c7d7ab3c61cdc32fcfaa14f948491a401cbc1c74bb33b6f4b851" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "ed25519-zebra", "k256", "rand_core 0.6.4", @@ -387,18 +387,18 @@ dependencies = [ [[package]] name = "cosmwasm-derive" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e64f710a18ef90d0a632cf27842e98ffc2d005a38a6f76c12fd0bc03bc1a2d" +checksum = "3a7ee2798c92c00dd17bebb4210f81d5f647e5e92d847959b7977e0fd29a3500" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5ad2e23a971b9e4cd57b20cee3e2e79c33799bed4b128e473aca3702bfe5dd" +checksum = "407aca6f1671a08b60db8167f03bb7cb6b2378f0ddd9a030367b66ba33c2fd41" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -409,9 +409,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2926d159a9bb1a716a592b40280f1663f2491a9de3b6da77c0933cee2a2655b8" +checksum = "e6d1e00b8fd27ff923c10303023626358e23a6f9079f8ebec23a8b4b0bfcd4b3" dependencies = [ "proc-macro2", "quote", @@ -420,9 +420,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76fee88ff5bf7bef55bd37ac0619974701b99bf6bd4b16cf56ee8810718abd71" +checksum = "92d5fdfd112b070055f068fad079d490117c8e905a588b92a5a7c9276d029930" dependencies = [ "base64", "cosmwasm-crypto", @@ -440,9 +440,9 @@ dependencies = [ [[package]] name = "cosmwasm-storage" -version = "1.2.3" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "639bc36408bc1ac45e3323166ceeb8f0b91b55a941c4ad59d389829002fbbd94" +checksum = "af9e21c4f58986fd20184d7685e1c43c5732c9309337b09307d5952fd34dba6e" dependencies = [ "cosmwasm-std", "serde", @@ -450,9 +450,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -524,7 +524,7 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20-base 0.16.0", @@ -638,7 +638,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test", "cw-paginate 2.1.0", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -656,7 +656,7 @@ version = "2.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "thiserror", ] @@ -667,7 +667,7 @@ source = "git+https://github.com/CosmWasm/cw-multi-test?rev=9af32fd42efcf28e9a79 dependencies = [ "anyhow", "cosmwasm-std", - "cw-storage-plus 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "derivative", "itertools", @@ -688,7 +688,7 @@ dependencies = [ "cosmwasm-std", "cw-address-like", "cw-ownable-derive", - "cw-storage-plus 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "thiserror", ] @@ -722,7 +722,7 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "serde", ] @@ -735,7 +735,7 @@ dependencies = [ "cw-denom", "cw-multi-test", "cw-ownable", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw-vesting", "cw2 0.16.0", @@ -814,8 +814,7 @@ dependencies = [ [[package]] name = "cw-storage-plus" version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053a5083c258acd68386734f428a5a171b29f7d733151ae83090c6fcc9417ffa" +source = "git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42#6db957ce730a95141a3ab4dc5ab76fb38e8c0c42" dependencies = [ "cosmwasm-std", "schemars", @@ -824,8 +823,9 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "1.0.1" -source = "git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42#6db957ce730a95141a3ab4dc5ab76fb38e8c0c42" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f0e92a069d62067f3472c62e30adedb4cab1754725c0f2a682b3128d2bf3c79" dependencies = [ "cosmwasm-std", "schemars", @@ -839,7 +839,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -913,7 +913,7 @@ dependencies = [ "cw-ownable", "cw-paginate 2.1.0", "cw-stake-tracker", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw-wormhole", "cw2 0.16.0", @@ -931,7 +931,7 @@ version = "2.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "serde", ] @@ -980,7 +980,7 @@ checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "cw-storage-plus 1.1.0", "schemars", "serde", ] @@ -1102,7 +1102,7 @@ dependencies = [ "cw-multi-test", "cw-ownable", "cw-paginate 2.1.0", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.13.4", "cw-utils 0.16.0", "cw2 0.16.0", @@ -1123,7 +1123,7 @@ dependencies = [ "cw-controllers 0.16.0", "cw-multi-test", "cw-ownable", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.13.4", @@ -1142,7 +1142,7 @@ dependencies = [ "cosmwasm-std", "cw-multi-test", "cw-ownable", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -1301,6 +1301,19 @@ dependencies = [ "serde", ] +[[package]] +name = "cw721" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa49f5096cc1587489ac5d1d345936e8139738f40ad07a94c7b157b19f975c00" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils 1.0.1", + "schemars", + "serde", +] + [[package]] name = "cw721-base" version = "0.16.0" @@ -1318,17 +1331,58 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw721-base" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c2be7476fa786c3adc96b00e33a0c5917d2a84774d94c5dd76e987c315570f" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-ownable", + "cw-storage-plus 1.1.0", + "cw-utils 1.0.1", + "cw2 1.0.1", + "cw721 0.17.0", + "cw721-base 0.16.0", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw721-controllers" version = "2.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "thiserror", ] +[[package]] +name = "cw721-roles" +version = "2.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 0.16.0", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 1.0.1", + "cw-utils 0.16.0", + "cw2 0.16.0", + "cw4 0.16.0", + "cw721 0.17.0", + "cw721-base 0.17.0", + "dao-cw721-extensions", + "dao-testing", + "dao-voting-cw721-staked", + "serde", + "thiserror", +] + [[package]] name = "dao-core" version = "2.1.0" @@ -1338,13 +1392,13 @@ dependencies = [ "cw-core", "cw-multi-test", "cw-paginate 2.1.0", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", "cw20-base 0.16.0", - "cw721 0.16.0", - "cw721-base", + "cw721 0.17.0", + "cw721-base 0.17.0", "dao-interface", "dao-macros", "dao-proposal-sudo", @@ -1352,6 +1406,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-cw721-extensions" +version = "2.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers 0.16.0", + "cw4 0.16.0", +] + [[package]] name = "dao-interface" version = "2.1.0" @@ -1388,7 +1452,7 @@ dependencies = [ "cw-core-interface", "cw-multi-test", "cw-proposal-single", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.13.4", "cw-utils 0.16.0", "cw2 0.16.0", @@ -1420,7 +1484,7 @@ dependencies = [ "cw-denom", "cw-multi-test", "cw-paginate 2.1.0", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -1446,7 +1510,7 @@ dependencies = [ "cosmwasm-std", "cw-denom", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -1473,7 +1537,7 @@ dependencies = [ "cw-denom", "cw-hooks", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "dao-interface", @@ -1540,7 +1604,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw4 0.16.0", @@ -1562,7 +1626,7 @@ dependencies = [ "cosmwasm-std", "cw-hooks", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -1597,7 +1661,7 @@ dependencies = [ "cw-denom", "cw-hooks", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -1606,7 +1670,7 @@ dependencies = [ "cw3 0.16.0", "cw4 0.16.0", "cw4-group 0.16.0", - "cw721-base", + "cw721-base 0.17.0", "dao-core", "dao-interface", "dao-macros", @@ -1638,7 +1702,7 @@ dependencies = [ "cw-hooks", "cw-multi-test", "cw-proposal-single", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.13.4", "cw-utils 0.16.0", "cw2 0.16.0", @@ -1648,7 +1712,7 @@ dependencies = [ "cw3 0.16.0", "cw4 0.16.0", "cw4-group 0.16.0", - "cw721-base", + "cw721-base 0.17.0", "dao-core", "dao-interface", "dao-macros", @@ -1675,7 +1739,7 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw2 0.16.0", "dao-interface", "dao-macros", @@ -1700,7 +1764,8 @@ dependencies = [ "cw20-stake 2.1.0", "cw4 0.16.0", "cw4-group 0.16.0", - "cw721-base", + "cw721-base 0.17.0", + "cw721-roles", "dao-core", "dao-interface", "dao-pre-propose-multiple", @@ -1711,6 +1776,7 @@ dependencies = [ "dao-voting-cw20-balance", "dao-voting-cw20-staked", "dao-voting-cw4", + "dao-voting-cw721-roles", "dao-voting-cw721-staked", "dao-voting-native-staked", "rand", @@ -1735,7 +1801,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw20 0.16.0", "dao-core", @@ -1751,7 +1817,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -1769,7 +1835,7 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw20 0.16.0", @@ -1777,6 +1843,7 @@ dependencies = [ "cw20-stake 2.1.0", "dao-interface", "dao-macros", + "dao-voting", "thiserror", ] @@ -1788,7 +1855,7 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "cw4 0.16.0", @@ -1798,6 +1865,31 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-voting-cw721-roles" +version = "2.1.0" +dependencies = [ + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "cw-ownable", + "cw-paginate 2.1.0", + "cw-storage-plus 1.0.1", + "cw-utils 0.16.0", + "cw2 0.16.0", + "cw4 0.16.0", + "cw721 0.17.0", + "cw721-base 0.17.0", + "cw721-controllers", + "cw721-roles", + "dao-cw721-extensions", + "dao-interface", + "dao-macros", + "dao-testing", + "thiserror", +] + [[package]] name = "dao-voting-cw721-staked" version = "2.1.0" @@ -1808,15 +1900,16 @@ dependencies = [ "cw-controllers 0.16.0", "cw-multi-test", "cw-paginate 2.1.0", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", - "cw721 0.16.0", - "cw721-base", + "cw721 0.17.0", + "cw721-base 0.17.0", "cw721-controllers", "dao-interface", "dao-macros", "dao-testing", + "dao-voting", "thiserror", ] @@ -1831,7 +1924,7 @@ dependencies = [ "cw-controllers 0.16.0", "cw-multi-test", "cw-paginate 2.1.0", - "cw-storage-plus 1.0.1 (git+https://github.com/CosmWasm/cw-storage-plus?rev=6db957ce730a95141a3ab4dc5ab76fb38e8c0c42)", + "cw-storage-plus 1.0.1", "cw-utils 0.16.0", "cw2 0.16.0", "dao-interface", @@ -1871,9 +1964,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", @@ -1955,7 +2048,7 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "digest 0.10.6", + "digest 0.10.7", "ff", "generic-array", "group", @@ -2026,9 +2119,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -2041,9 +2134,9 @@ checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" [[package]] name = "futures" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -2056,9 +2149,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -2066,15 +2159,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -2083,38 +2176,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", ] [[package]] name = "futures-sink" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -2130,9 +2223,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -2140,9 +2233,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "js-sys", @@ -2164,9 +2257,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.16" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" dependencies = [ "bytes", "fnv", @@ -2245,7 +2338,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -2290,9 +2383,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", @@ -2363,9 +2456,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2391,9 +2484,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.2" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -2414,8 +2507,9 @@ dependencies = [ "cw20 0.16.0", "cw20-base 0.16.0", "cw20-stake 2.1.0", - "cw721 0.16.0", - "cw721-base", + "cw721 0.17.0", + "cw721-base 0.17.0", + "cw721-roles", "dao-core", "dao-interface", "dao-pre-propose-single", @@ -2448,9 +2542,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -2481,9 +2575,9 @@ dependencies = [ [[package]] name = "keccak" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" dependencies = [ "cpufeatures", ] @@ -2496,9 +2590,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.140" +version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" [[package]] name = "linked-hash-map" @@ -2508,12 +2602,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "matchit" @@ -2541,14 +2632,13 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2593,9 +2683,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "opaque-debug" @@ -2637,7 +2727,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -2669,15 +2759,15 @@ checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.5.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cbd939b234e95d72bc393d51788aec68aeeb5d51e748ca08ff3aad58cb722f7" +checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" dependencies = [ "thiserror", "ucd-trie", @@ -2685,9 +2775,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.5.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a81186863f3d0a27340815be8f2078dd8050b14cd71913db9fbda795e5f707d7" +checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" dependencies = [ "pest", "pest_generator", @@ -2695,22 +2785,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.5.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b" +checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", ] [[package]] name = "pest_meta" -version = "2.5.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e3b284b1f13a20dc5ebc90aff59a51b8d7137c221131b52a7260c08cbc1cc80" +checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" dependencies = [ "once_cell", "pest", @@ -2719,22 +2809,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", ] [[package]] @@ -2767,9 +2857,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.53" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" dependencies = [ "unicode-ident", ] @@ -2797,12 +2887,12 @@ dependencies = [ [[package]] name = "prost" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48e50df39172a3e7eb17e14642445da64996989bc212b583015435d39a58537" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", - "prost-derive 0.11.8", + "prost-derive 0.11.9", ] [[package]] @@ -2820,9 +2910,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea9b0f8cbe5e15a8a042d030bd96668db28ecb567ec37d691971ff5731d2b1b" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", "itertools", @@ -2833,18 +2923,18 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379119666929a1afd7a043aa6cf96fa67a6dce9af60c88095a4686dbce4c9c88" +checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13" dependencies = [ - "prost 0.11.8", + "prost 0.11.9", ] [[package]] name = "quote" -version = "1.0.26" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -2887,9 +2977,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.2" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -2898,9 +2988,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "rfc6979" @@ -2934,7 +3024,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -3074,9 +3164,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ "bitflags", "core-foundation", @@ -3087,9 +3177,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", @@ -3103,18 +3193,18 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.158" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] [[package]] name = "serde-json-wasm" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15bee9b04dd165c3f4e142628982ddde884c2022a89e8ddf99c4829bf2c3a58" +checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" dependencies = [ "serde", ] @@ -3130,13 +3220,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.158" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.5", + "syn 2.0.18", ] [[package]] @@ -3152,9 +3242,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.94" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -3169,14 +3259,14 @@ checksum = "bcec881020c684085e55a25f7fd888954d56609ef363479dc5a1305eb0d40cab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.5", + "syn 2.0.18", ] [[package]] name = "serde_yaml" -version = "0.9.19" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10" +checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c" dependencies = [ "indexmap", "itoa", @@ -3193,7 +3283,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -3217,16 +3307,16 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] name = "sha3" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] @@ -3236,7 +3326,7 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -3338,9 +3428,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "subtle-encoding" @@ -3364,9 +3454,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.5" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c2d1c76a26822187a1fbb5964e3fff108bc208f02e820ab9dac1234f6b388a" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -3379,18 +3469,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "synstructure" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-xid", -] - [[package]] name = "tendermint" version = "0.26.0" @@ -3406,7 +3484,7 @@ dependencies = [ "k256", "num-traits", "once_cell", - "prost 0.11.8", + "prost 0.11.9", "prost-types", "ripemd160", "serde", @@ -3446,7 +3524,7 @@ dependencies = [ "flex-error", "num-derive", "num-traits", - "prost 0.11.8", + "prost 0.11.9", "prost-types", "serde", "serde_bytes", @@ -3464,7 +3542,7 @@ dependencies = [ "flex-error", "num-derive", "num-traits", - "prost 0.11.8", + "prost 0.11.9", "prost-types", "serde", "serde_bytes", @@ -3553,14 +3631,14 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.5", + "syn 2.0.18", ] [[package]] name = "time" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "serde", "time-core", @@ -3569,15 +3647,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -3599,20 +3677,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.26.0" +version = "1.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -3627,13 +3704,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", ] [[package]] @@ -3649,9 +3726,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -3660,9 +3737,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -3701,8 +3778,8 @@ dependencies = [ "hyper-timeout", "percent-encoding", "pin-project", - "prost 0.11.8", - "prost-derive 0.11.8", + "prost 0.11.9", + "prost-derive 0.11.9", "tokio", "tokio-stream", "tokio-util", @@ -3759,20 +3836,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", ] [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] @@ -3825,9 +3902,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -3838,17 +3915,11 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "unsafe-libyaml" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c" +checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" [[package]] name = "untrusted" @@ -3858,9 +3929,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -3929,9 +4000,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3939,24 +4010,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3964,28 +4035,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -4047,37 +4118,37 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -4086,42 +4157,84 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + [[package]] name = "wynd-utils" version = "0.4.1" @@ -4145,21 +4258,20 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.3.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", - "synstructure", + "syn 2.0.18", ] diff --git a/Cargo.toml b/Cargo.toml index 178eb2c7e..2d30f8f64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [workspace] members = [ "contracts/dao-core", + "contracts/external/*", "contracts/proposal/*", "contracts/pre-propose/*", "contracts/staking/*", "contracts/voting/*", "packages/*", "test-contracts/*", - "ci/*", - "contracts/external/*" + "ci/*" ] exclude = ["ci/configs/"] @@ -18,10 +18,6 @@ license = "BSD-3-Clause" repository = "https://github.com/DA0-DA0/dao-contracts" version = "2.1.0" -[profile.release.package.stake-cw20-external-rewards] -codegen-units = 1 -incremental = false - [profile.release] codegen-units = 1 opt-level = 3 @@ -50,8 +46,8 @@ cw20-base = "0.16" cw3 = "0.16" cw4 = "0.16" cw4-group = "0.16" -cw721 = "0.16" -cw721-base = "0.16" +cw721 = "0.17" +cw721-base = "0.17" proc-macro2 = "1.0" quote = "1.0" rand = "0.8" @@ -74,7 +70,9 @@ cw-vesting = { path = "./contracts/external/cw-vesting" } cw20-stake = { path = "./contracts/staking/cw20-stake", version = "*" } cw-stake-tracker = { path = "./packages/cw-stake-tracker", version = "*" } cw721-controllers = { path = "./packages/cw721-controllers", version = "*" } +cw721-roles = { path = "./contracts/external/cw721-roles", version = "*" } dao-core = { path = "./contracts/dao-core" } +dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "*" } dao-interface = { path = "./packages/dao-interface", version = "*" } dao-macros = { path = "./packages/dao-macros", version = "*" } dao-pre-propose-approval-single = { path = "./contracts/pre-propose/dao-pre-propose-approval-single", version = "*" } @@ -93,6 +91,7 @@ dao-voting = { path = "./packages/dao-voting", version = "*" } dao-voting-cw20-balance = { path = "./test-contracts/dao-voting-cw20-balance", version = "*" } dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "*" } dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "*" } +dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "*" } dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "*" } dao-voting-native-staked = { path = "./contracts/voting/dao-voting-native-staked", version = "*" } diff --git a/ci/integration-tests/Cargo.toml b/ci/integration-tests/Cargo.toml index fca2adb56..51d6f1c8e 100644 --- a/ci/integration-tests/Cargo.toml +++ b/ci/integration-tests/Cargo.toml @@ -14,6 +14,7 @@ cosm-orc = { version = "4.0" } cw20 = { workspace = true } cw20-base = { workspace = true } cw721-base = { workspace = true } +cw721-roles = { workspace = true } cw721 = { workspace = true } cw-utils = { workspace = true } cosmwasm-std = { workspace = true, features = ["ibc3"] } diff --git a/ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs b/ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs index 620f2d53f..0f007995a 100644 --- a/ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs +++ b/ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs @@ -51,8 +51,11 @@ fn setup_test( "instantiate_dao_voting_cw721_staked", &module::msg::InstantiateMsg { owner, - nft_address: cw721.clone(), + nft_contract: module::msg::NftContract::Existing { + address: cw721.clone(), + }, unstaking_duration, + active_threshold: None, }, key, None, @@ -93,12 +96,12 @@ pub fn mint_nft(chain: &mut Chain, sender: &SigningKey, receiver: &str, token_id .execute( CW721_NAME, "mint_nft", - &cw721_base::ExecuteMsg::Mint::(cw721_base::MintMsg { + &cw721_base::ExecuteMsg::::Mint { token_id: token_id.to_string(), owner: receiver.to_string(), token_uri: None, extension: Empty::default(), - }), + }, sender, vec![], ) @@ -220,14 +223,12 @@ fn cw721_stake_max_claims_works(chain: &mut Chain) { reqs.push(ExecReq { contract_name: CW721_NAME.to_string(), - msg: Box::new(cw721_base::ExecuteMsg::Mint::( - cw721_base::MintMsg { - token_id: token_id.clone(), - owner: user_addr.to_string(), - token_uri: None, - extension: Empty::default(), - }, - )), + msg: Box::new(cw721_base::ExecuteMsg::::Mint { + token_id: token_id.clone(), + owner: user_addr.to_string(), + token_uri: None, + extension: Empty::default(), + }), funds: vec![], }); diff --git a/ci/integration-tests/src/tests/proposal_gas_test.rs b/ci/integration-tests/src/tests/proposal_gas_test.rs index 697125c81..e74aeeda7 100644 --- a/ci/integration-tests/src/tests/proposal_gas_test.rs +++ b/ci/integration-tests/src/tests/proposal_gas_test.rs @@ -1,5 +1,4 @@ use cosmwasm_std::{to_binary, CosmosMsg, Empty, WasmMsg}; -use cw721_base::MintMsg; use dao_proposal_single::query::ProposalResponse; use dao_voting::voting::Vote; use test_context::test_context; @@ -16,14 +15,12 @@ fn mint_mint_mint_mint(cw721: &str, owner: &str, mints: u64) -> Vec { .map(|mint| { WasmMsg::Execute { contract_addr: cw721.to_string(), - msg: to_binary(&cw721_base::msg::ExecuteMsg::Mint::( - MintMsg:: { + msg: to_binary(&cw721_base::msg::ExecuteMsg::::Mint{ token_id: mint.to_string(), owner: owner.to_string(), token_uri: Some("https://bafkreibufednctf2f2bpduiibgkvpqcw5rtdmhqh2htqx3qbdnji4h55hy.ipfs.nftstorage.link".to_string()), extension: Empty::default(), - }, - )) + }) .unwrap(), funds: vec![], } diff --git a/contracts/dao-core/src/tests.rs b/contracts/dao-core/src/tests.rs index 006dcd0c1..36748bf5e 100644 --- a/contracts/dao-core/src/tests.rs +++ b/contracts/dao-core/src/tests.rs @@ -2079,14 +2079,12 @@ fn test_cw721_receive() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), cw721_addr.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint(cw721_base::msg::MintMsg::< - Option, - > { + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { token_id: "ekez".to_string(), owner: CREATOR_ADDR.to_string(), token_uri: None, extension: None, - }), + }, &[], ) .unwrap(); @@ -2211,14 +2209,12 @@ fn test_cw721_receive_no_auto_add() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), cw721_addr.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint(cw721_base::msg::MintMsg::< - Option, - > { + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { token_id: "ekez".to_string(), owner: CREATOR_ADDR.to_string(), token_uri: None, extension: None, - }), + }, &[], ) .unwrap(); diff --git a/contracts/external/cw-fund-distributor/schema/cw-fund-distributor.json b/contracts/external/cw-fund-distributor/schema/cw-fund-distributor.json index e8bf91f56..19a0541ca 100644 --- a/contracts/external/cw-fund-distributor/schema/cw-fund-distributor.json +++ b/contracts/external/cw-fund-distributor/schema/cw-fund-distributor.json @@ -11,6 +11,7 @@ ], "properties": { "active_threshold": { + "description": "The number or percentage of tokens that must be staked for the DAO to be active", "anyOf": [ { "$ref": "#/definitions/ActiveThreshold" diff --git a/contracts/external/cw721-roles/.cargo/config b/contracts/external/cw721-roles/.cargo/config new file mode 100644 index 000000000..7d1a066c8 --- /dev/null +++ b/contracts/external/cw721-roles/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/external/cw721-roles/Cargo.toml b/contracts/external/cw721-roles/Cargo.toml new file mode 100644 index 000000000..1a6935974 --- /dev/null +++ b/contracts/external/cw721-roles/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "cw721-roles" +authors = ["Jake Hartnell"] +description = "Non-transferable CW721 NFT contract that incorporates voting weights and on-chain roles." +version = { workspace = true } +edition = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } +cw-controllers = { workspace = true } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +cw4 = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +dao-cw721-extensions = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +dao-testing = { workspace = true } +dao-voting-cw721-staked = { workspace = true } diff --git a/contracts/external/cw721-roles/README.md b/contracts/external/cw721-roles/README.md new file mode 100644 index 000000000..46c5f7079 --- /dev/null +++ b/contracts/external/cw721-roles/README.md @@ -0,0 +1,65 @@ +# cw721-roles + +This is a non-transferable NFT contract intended for use with DAOs. `cw721-roles` has an extension that allows for each NFT to have a `weight` associated with it, and also implements much of the functionality behind the [cw4-group contract](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw4-group) (credit to [Confio](https://confio.gmbh/) for their work on that). + +All methods of this contract are only callable via the configurable `minter` when the contract is created. It is primarily intended for use with DAOs. + +The `mint`, `burn`, `send`, and `transfer` methods have all been overriden from their default `cw721-base` versions, but work roughly the same with the caveat being they are only callable via the `minter`. All methods related to approvals are unsupported. + +## Extensions + +`cw721-roles` contains the following extensions: + +Token metadata has been extended with a weight and an optional human readable on-chain role which may be used in separate contracts for enforcing additional permissions. + +```rust +pub struct MetadataExt { + /// Optional on-chain role for this member, can be used by other contracts to enforce permissions + pub role: Option, + /// The voting weight of this role + pub weight: u64, +} +``` + +The contract has an additional execution extension that includes the ability to add and remove hooks for membership change events, as well as update a particular token's `token_uri`, `weight`, and `role`. All of these are only callable by the configured `minter`. + +```rust +pub enum ExecuteExt { + /// Add a new hook to be informed of all membership changes. + /// Must be called by Admin + AddHook { addr: String }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: String }, + /// Update the token_uri for a particular NFT. Must be called by minter / admin + UpdateTokenUri { + token_id: String, + token_uri: Option, + }, + /// Updates the voting weight of a token. Must be called by minter / admin + UpdateTokenWeight { token_id: String, weight: u64 }, + /// Udates the role of a token. Must be called by minter / admin + UpdateTokenRole { + token_id: String, + role: Option, + }, +} +``` + +The query extension implements queries that are compatible with the previously mentioned [cw4-group contract](https://github.com/CosmWasm/cw-plus/tree/main/contracts/cw4-group). + +```ignore +pub enum QueryExt { + /// Total weight at a given height + #[returns(cw4::TotalWeightResponse)] + TotalWeight { at_height: Option }, + /// Returns the weight of a certain member + #[returns(cw4::MemberResponse)] + Member { + addr: String, + at_height: Option, + }, + /// Shows all registered hooks. + #[returns(cw_controllers::HooksResponse)] + Hooks {}, +} +``` diff --git a/contracts/external/cw721-roles/examples/schema.rs b/contracts/external/cw721-roles/examples/schema.rs new file mode 100644 index 000000000..044f69e42 --- /dev/null +++ b/contracts/external/cw721-roles/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use cw721_roles::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + execute: ExecuteMsg, + query: QueryMsg, + } +} diff --git a/contracts/external/cw721-roles/schema/cw721-roles.json b/contracts/external/cw721-roles/schema/cw721-roles.json new file mode 100644 index 000000000..f19b95d25 --- /dev/null +++ b/contracts/external/cw721-roles/schema/cw721-roles.json @@ -0,0 +1,2126 @@ +{ + "contract_name": "cw721-roles", + "contract_version": "2.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "minter", + "name", + "symbol" + ], + "properties": { + "minter": { + "description": "The minter is the only one who can create new NFTs. This is designed for a base NFT that is controlled by an external program or contract. You will likely replace this with custom logic in custom NFTs", + "type": "string" + }, + "name": { + "description": "Name of the NFT contract", + "type": "string" + }, + "symbol": { + "description": "Symbol of the NFT contract", + "type": "string" + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This is like Cw721ExecuteMsg but we add a Mint command for an owner to make this stand-alone. You will likely want to remove mint and use other control logic in any contract that inherits this.", + "oneOf": [ + { + "description": "Transfer is a base message to move a token to another account without triggering actions", + "type": "object", + "required": [ + "transfer_nft" + ], + "properties": { + "transfer_nft": { + "type": "object", + "required": [ + "recipient", + "token_id" + ], + "properties": { + "recipient": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Send is a base message to transfer a token to a contract and trigger an action on the receiving contract.", + "type": "object", + "required": [ + "send_nft" + ], + "properties": { + "send_nft": { + "type": "object", + "required": [ + "contract", + "msg", + "token_id" + ], + "properties": { + "contract": { + "type": "string" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send the token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve" + ], + "properties": { + "approve": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted Approval", + "type": "object", + "required": [ + "revoke" + ], + "properties": { + "revoke": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Allows operator to transfer / send any token from the owner's account. If expiration is set, then this allowance has a time/height limit", + "type": "object", + "required": [ + "approve_all" + ], + "properties": { + "approve_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "expires": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "operator": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove previously granted ApproveAll permission", + "type": "object", + "required": [ + "revoke_all" + ], + "properties": { + "revoke_all": { + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Mint a new NFT, can only be called by the contract minter", + "type": "object", + "required": [ + "mint" + ], + "properties": { + "mint": { + "type": "object", + "required": [ + "extension", + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "allOf": [ + { + "$ref": "#/definitions/MetadataExt" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Burn an NFT the sender has access to", + "type": "object", + "required": [ + "burn" + ], + "properties": { + "burn": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Extension msg", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/ExecuteExt" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.", + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "ExecuteExt": { + "oneOf": [ + { + "description": "Add a new hook to be informed of all membership changes. Must be called by Admin", + "type": "object", + "required": [ + "add_hook" + ], + "properties": { + "add_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Remove a hook. Must be called by Admin", + "type": "object", + "required": [ + "remove_hook" + ], + "properties": { + "remove_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Update the token_uri for a particular NFT. Must be called by minter / admin", + "type": "object", + "required": [ + "update_token_uri" + ], + "properties": { + "update_token_uri": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + }, + "token_uri": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Updates the voting weight of a token. Must be called by minter / admin", + "type": "object", + "required": [ + "update_token_weight" + ], + "properties": { + "update_token_weight": { + "type": "object", + "required": [ + "token_id", + "weight" + ], + "properties": { + "token_id": { + "type": "string" + }, + "weight": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Udates the role of a token. Must be called by minter / admin", + "type": "object", + "required": [ + "update_token_role" + ], + "properties": { + "update_token_role": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "role": { + "type": [ + "string", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "MetadataExt": { + "type": "object", + "required": [ + "weight" + ], + "properties": { + "role": { + "description": "Optional on-chain role for this member, can be used by other contracts to enforce permissions", + "type": [ + "string", + "null" + ] + }, + "weight": { + "description": "The voting weight of this role", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Return the owner of the given token, error if token does not exist", + "type": "object", + "required": [ + "owner_of" + ], + "properties": { + "owner_of": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return operator that can access all of the owner's tokens.", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "type": "object", + "required": [ + "spender", + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "spender": { + "type": "string" + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return approvals that a token has", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return approval of a given operator for all tokens of an owner, error if not set", + "type": "object", + "required": [ + "operator" + ], + "properties": { + "operator": { + "type": "object", + "required": [ + "operator", + "owner" + ], + "properties": { + "include_expired": { + "type": [ + "boolean", + "null" + ] + }, + "operator": { + "type": "string" + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "List all operators that can access all of the owner's tokens", + "type": "object", + "required": [ + "all_operators" + ], + "properties": { + "all_operators": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired items, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Total number of tokens issued", + "type": "object", + "required": [ + "num_tokens" + ], + "properties": { + "num_tokens": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns top-level metadata about the contract", + "type": "object", + "required": [ + "contract_info" + ], + "properties": { + "contract_info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns metadata about one particular token, based on *ERC721 Metadata JSON Schema* but directly from the contract", + "type": "object", + "required": [ + "nft_info" + ], + "properties": { + "nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With MetaData Extension. Returns the result of both `NftInfo` and `OwnerOf` as one query as an optimization for clients", + "type": "object", + "required": [ + "all_nft_info" + ], + "properties": { + "all_nft_info": { + "type": "object", + "required": [ + "token_id" + ], + "properties": { + "include_expired": { + "description": "unset or false will filter out expired approvals, you must set to true to see them", + "type": [ + "boolean", + "null" + ] + }, + "token_id": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Returns all tokens owned by the given address, [] if unset.", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "type": "object", + "required": [ + "owner" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "type": "string" + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "With Enumerable extension. Requires pagination. Lists all token_ids controlled by the contract.", + "type": "object", + "required": [ + "all_tokens" + ], + "properties": { + "all_tokens": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Return the minter", + "type": "object", + "required": [ + "minter" + ], + "properties": { + "minter": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Extension query", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryExt" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "QueryExt": { + "oneOf": [ + { + "description": "Total weight at a given height", + "type": "object", + "required": [ + "total_weight" + ], + "properties": { + "total_weight": { + "type": "object", + "properties": { + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of Members", + "type": "object", + "required": [ + "list_members" + ], + "properties": { + "list_members": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the weight of a certain member", + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Shows all registered hooks.", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "migrate": null, + "sudo": null, + "responses": { + "all_nft_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllNftInfoResponse_for_QueryExt", + "type": "object", + "required": [ + "access", + "info" + ], + "properties": { + "access": { + "description": "Who can transfer the token", + "allOf": [ + { + "$ref": "#/definitions/OwnerOfResponse" + } + ] + }, + "info": { + "description": "Data on the token itself,", + "allOf": [ + { + "$ref": "#/definitions/NftInfoResponse_for_QueryExt" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "NftInfoResponse_for_QueryExt": { + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "allOf": [ + { + "$ref": "#/definitions/QueryExt" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "OwnerOfResponse": { + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "QueryExt": { + "oneOf": [ + { + "description": "Total weight at a given height", + "type": "object", + "required": [ + "total_weight" + ], + "properties": { + "total_weight": { + "type": "object", + "properties": { + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of Members", + "type": "object", + "required": [ + "list_members" + ], + "properties": { + "list_members": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the weight of a certain member", + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Shows all registered hooks.", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "all_operators": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorsResponse", + "type": "object", + "required": [ + "operators" + ], + "properties": { + "operators": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "all_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "approval": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "approvals": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ApprovalsResponse", + "type": "object", + "required": [ + "approvals" + ], + "properties": { + "approvals": { + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "contract_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractInfoResponse", + "type": "object", + "required": [ + "name", + "symbol" + ], + "properties": { + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + }, + "additionalProperties": false + }, + "extension": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Null", + "type": "null" + }, + "minter": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MinterResponse", + "description": "Shows who can mint these tokens", + "type": "object", + "properties": { + "minter": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "nft_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NftInfoResponse_for_QueryExt", + "type": "object", + "required": [ + "extension" + ], + "properties": { + "extension": { + "description": "You can add any custom metadata here when you extend cw721-base", + "allOf": [ + { + "$ref": "#/definitions/QueryExt" + } + ] + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "QueryExt": { + "oneOf": [ + { + "description": "Total weight at a given height", + "type": "object", + "required": [ + "total_weight" + ], + "properties": { + "total_weight": { + "type": "object", + "properties": { + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of Members", + "type": "object", + "required": [ + "list_members" + ], + "properties": { + "list_members": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the weight of a certain member", + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "type": "string" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Shows all registered hooks.", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "num_tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NumTokensResponse", + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "operator": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OperatorResponse", + "type": "object", + "required": [ + "approval" + ], + "properties": { + "approval": { + "$ref": "#/definitions/Approval" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "owner_of": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OwnerOfResponse", + "type": "object", + "required": [ + "approvals", + "owner" + ], + "properties": { + "approvals": { + "description": "If set this address is approved to transfer/send the token as well", + "type": "array", + "items": { + "$ref": "#/definitions/Approval" + } + }, + "owner": { + "description": "Owner of the token", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Approval": { + "type": "object", + "required": [ + "expires", + "spender" + ], + "properties": { + "expires": { + "description": "When the Approval expires (maybe Expiration::never)", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "spender": { + "description": "Account that can transfer/send the token", + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "tokens": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TokensResponse", + "type": "object", + "required": [ + "tokens" + ], + "properties": { + "tokens": { + "description": "Contains all token_ids in lexicographical ordering If there are more than `limit`, use `start_from` in future queries to achieve pagination.", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/external/cw721-roles/src/contract.rs b/contracts/external/cw721-roles/src/contract.rs new file mode 100644 index 000000000..2fc13bc37 --- /dev/null +++ b/contracts/external/cw721-roles/src/contract.rs @@ -0,0 +1,493 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + from_binary, to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, + StdResult, SubMsg, Uint64, +}; +use cw4::{ + Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, + TotalWeightResponse, +}; +use cw721::{Cw721ReceiveMsg, NftInfoResponse, OwnerOfResponse}; +use cw721_base::{Cw721Contract, InstantiateMsg as Cw721BaseInstantiateMsg}; +use cw_storage_plus::Bound; +use cw_utils::maybe_addr; +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; +use std::cmp::Ordering; + +use crate::msg::{ExecuteMsg, QueryMsg}; +use crate::state::{MEMBERS, TOTAL}; +use crate::{error::RolesContractError as ContractError, state::HOOKS}; + +// Version info for migration +const CONTRACT_NAME: &str = "crates.io:cw721-roles"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Settings for query pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +pub type Cw721Roles<'a> = Cw721Contract<'a, MetadataExt, Empty, ExecuteExt, QueryExt>; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + msg: Cw721BaseInstantiateMsg, +) -> Result { + Cw721Roles::default().instantiate(deps.branch(), env.clone(), info, msg)?; + + // Initialize total weight to zero + TOTAL.save(deps.storage, &0, env.block.height)?; + + cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::default() + .add_attribute("contract_name", CONTRACT_NAME) + .add_attribute("contract_version", CONTRACT_VERSION)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + // Only owner / minter can execute + cw_ownable::assert_owner(deps.storage, &info.sender)?; + + match msg { + ExecuteMsg::Mint { + token_id, + owner, + token_uri, + extension, + } => execute_mint(deps, env, info, token_id, owner, token_uri, extension), + ExecuteMsg::Burn { token_id } => execute_burn(deps, env, info, token_id), + ExecuteMsg::Extension { msg } => match msg { + ExecuteExt::AddHook { addr } => execute_add_hook(deps, info, addr), + ExecuteExt::RemoveHook { addr } => execute_remove_hook(deps, info, addr), + ExecuteExt::UpdateTokenRole { token_id, role } => { + execute_update_token_role(deps, env, info, token_id, role) + } + ExecuteExt::UpdateTokenUri { + token_id, + token_uri, + } => execute_update_token_uri(deps, env, info, token_id, token_uri), + ExecuteExt::UpdateTokenWeight { token_id, weight } => { + execute_update_token_weight(deps, env, info, token_id, weight) + } + }, + ExecuteMsg::TransferNft { + recipient, + token_id, + } => execute_transfer(deps, env, info, recipient, token_id), + ExecuteMsg::SendNft { + contract, + token_id, + msg, + } => execute_send(deps, env, info, token_id, contract, msg), + _ => Cw721Roles::default() + .execute(deps, env, info, msg) + .map_err(Into::into), + } +} + +pub fn execute_mint( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, + owner: String, + token_uri: Option, + extension: MetadataExt, +) -> Result { + let mut total = Uint64::from(TOTAL.load(deps.storage)?); + let mut diff = MemberDiff::new(owner.clone(), None, None); + + // Update member weights and total + MEMBERS.update( + deps.storage, + &deps.api.addr_validate(&owner)?, + env.block.height, + |old| -> StdResult<_> { + // Increment the total weight by the weight of the new token + total = total.checked_add(Uint64::from(extension.weight))?; + // Add the new NFT weight to the old weight for the owner + let new_weight = old.unwrap_or_default() + extension.weight; + // Set the diff for use in hooks + diff = MemberDiff::new(owner.clone(), old, Some(new_weight)); + Ok(new_weight) + }, + )?; + TOTAL.save(deps.storage, &total.u64(), env.block.height)?; + + let diffs = MemberChangedHookMsg { diffs: vec![diff] }; + + // Prepare hook messages + let msgs = HOOKS.prepare_hooks(deps.storage, |h| { + diffs.clone().into_cosmos_msg(h).map(SubMsg::new) + })?; + + // Call base mint + let res = Cw721Roles::default().execute( + deps, + env, + info, + ExecuteMsg::Mint { + token_id, + owner, + token_uri, + extension, + }, + )?; + + Ok(res.add_submessages(msgs)) +} + +pub fn execute_burn( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, +) -> Result { + // Lookup the owner of the NFT + let owner: OwnerOfResponse = from_binary(&Cw721Roles::default().query( + deps.as_ref(), + env.clone(), + QueryMsg::OwnerOf { + token_id: token_id.clone(), + include_expired: None, + }, + )?)?; + + // Get the weight of the token + let nft_info: NftInfoResponse = from_binary(&Cw721Roles::default().query( + deps.as_ref(), + env.clone(), + QueryMsg::NftInfo { + token_id: token_id.clone(), + }, + )?)?; + + let mut total = Uint64::from(TOTAL.load(deps.storage)?); + let mut diff = MemberDiff::new(owner.owner.clone(), None, None); + + // Update member weights and total + let owner_addr = deps.api.addr_validate(&owner.owner)?; + let old_weight = MEMBERS.load(deps.storage, &owner_addr)?; + + // Subtract the nft weight from the member's old weight + let new_weight = old_weight + .checked_sub(nft_info.extension.weight) + .ok_or(ContractError::CannotBurn {})?; + + // Subtract nft weight from the total + total = total.checked_sub(Uint64::from(nft_info.extension.weight))?; + + // Check if the new weight is now zero + if new_weight == 0 { + // New weight is now None + diff = MemberDiff::new(owner.owner, Some(old_weight), None); + // Remove owner from list of members + MEMBERS.remove(deps.storage, &owner_addr, env.block.height)?; + } else { + MEMBERS.update( + deps.storage, + &owner_addr, + env.block.height, + |old| -> StdResult<_> { + diff = MemberDiff::new(owner.owner.clone(), old, Some(new_weight)); + Ok(new_weight) + }, + )?; + } + + TOTAL.save(deps.storage, &total.u64(), env.block.height)?; + + let diffs = MemberChangedHookMsg { diffs: vec![diff] }; + + // Prepare hook messages + let msgs = HOOKS.prepare_hooks(deps.storage, |h| { + diffs.clone().into_cosmos_msg(h).map(SubMsg::new) + })?; + + // Remove the token + Cw721Roles::default() + .tokens + .remove(deps.storage, &token_id)?; + // Decrement the account + Cw721Roles::default().decrement_tokens(deps.storage)?; + + Ok(Response::new() + .add_attribute("action", "burn") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id) + .add_submessages(msgs)) +} + +pub fn execute_transfer( + deps: DepsMut, + _env: Env, + info: MessageInfo, + recipient: String, + token_id: String, +) -> Result { + let contract = Cw721Roles::default(); + + let mut token = contract.tokens.load(deps.storage, &token_id)?; + // set owner and remove existing approvals + token.owner = deps.api.addr_validate(&recipient)?; + token.approvals = vec![]; + contract.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::new() + .add_attribute("action", "transfer_nft") + .add_attribute("sender", info.sender) + .add_attribute("recipient", recipient) + .add_attribute("token_id", token_id)) +} + +pub fn execute_send( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token_id: String, + recipient_contract: String, + msg: Binary, +) -> Result { + let contract = Cw721Roles::default(); + + let mut token = contract.tokens.load(deps.storage, &token_id)?; + // set owner and remove existing approvals + token.owner = deps.api.addr_validate(&recipient_contract)?; + token.approvals = vec![]; + contract.tokens.save(deps.storage, &token_id, &token)?; + + let send = Cw721ReceiveMsg { + sender: info.sender.to_string(), + token_id: token_id.clone(), + msg, + }; + + Ok(Response::new() + .add_message(send.into_cosmos_msg(recipient_contract.clone())?) + .add_attribute("action", "send_nft") + .add_attribute("sender", info.sender) + .add_attribute("recipient", recipient_contract) + .add_attribute("token_id", token_id)) +} + +pub fn execute_add_hook( + deps: DepsMut, + _info: MessageInfo, + addr: String, +) -> Result { + let hook = deps.api.addr_validate(&addr)?; + HOOKS.add_hook(deps.storage, hook)?; + + Ok(Response::default() + .add_attribute("action", "add_hook") + .add_attribute("hook", addr)) +} + +pub fn execute_remove_hook( + deps: DepsMut, + _info: MessageInfo, + addr: String, +) -> Result { + let hook = deps.api.addr_validate(&addr)?; + HOOKS.remove_hook(deps.storage, hook)?; + + Ok(Response::default() + .add_attribute("action", "remove_hook") + .add_attribute("hook", addr)) +} + +pub fn execute_update_token_role( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token_id: String, + role: Option, +) -> Result { + let contract = Cw721Roles::default(); + + // Make sure NFT exists + let mut token = contract.tokens.load(deps.storage, &token_id)?; + + // Update role with new value + token.extension.role = role.clone(); + contract.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::default() + .add_attribute("action", "update_token_role") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id) + .add_attribute("role", role.unwrap_or_default())) +} + +pub fn execute_update_token_uri( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token_id: String, + token_uri: Option, +) -> Result { + let contract = Cw721Roles::default(); + + let mut token = contract.tokens.load(deps.storage, &token_id)?; + + // Set new token URI + token.token_uri = token_uri.clone(); + contract.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::new() + .add_attribute("action", "update_token_uri") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id) + .add_attribute("token_uri", token_uri.unwrap_or_default())) +} + +pub fn execute_update_token_weight( + deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, + weight: u64, +) -> Result { + let contract = Cw721Roles::default(); + + // Make sure NFT exists + let mut token = contract.tokens.load(deps.storage, &token_id)?; + + let mut total = Uint64::from(TOTAL.load(deps.storage)?); + let mut diff = MemberDiff::new(token.clone().owner, None, None); + + // Update member weights and total + MEMBERS.update( + deps.storage, + &token.owner, + env.block.height, + |old| -> Result<_, ContractError> { + let new_total_weight; + let old_total_weight = old.unwrap_or_default(); + + // Check if new token weight is great than, less than, or equal to + // the old token weight + match weight.cmp(&token.extension.weight) { + Ordering::Greater => { + // Subtract the old token weight from the new token weight + let weight_difference = weight + .checked_sub(token.extension.weight) + .ok_or(ContractError::NegativeValue {})?; + + // Increment the total weight by the weight difference of the new token + total = total.checked_add(Uint64::from(weight_difference))?; + // Add the new NFT weight to the old weight for the owner + new_total_weight = old_total_weight + weight_difference; + // Set the diff for use in hooks + diff = MemberDiff::new(token.clone().owner, old, Some(new_total_weight)); + } + Ordering::Less => { + // Subtract the new token weight from the old token weight + let weight_difference = token + .extension + .weight + .checked_sub(weight) + .ok_or(ContractError::NegativeValue {})?; + + // Subtract the weight difference from the old total weight + new_total_weight = old_total_weight + .checked_sub(weight_difference) + .ok_or(ContractError::NegativeValue {})?; + + // Subtract difference from the total + total = total.checked_sub(Uint64::from(weight_difference))?; + } + Ordering::Equal => return Err(ContractError::NoWeightChange {}), + } + + Ok(new_total_weight) + }, + )?; + TOTAL.save(deps.storage, &total.u64(), env.block.height)?; + + let diffs = MemberChangedHookMsg { diffs: vec![diff] }; + + // Prepare hook messages + let msgs = HOOKS.prepare_hooks(deps.storage, |h| { + diffs.clone().into_cosmos_msg(h).map(SubMsg::new) + })?; + + // Save token weight + token.extension.weight = weight; + contract.tokens.save(deps.storage, &token_id, &token)?; + + Ok(Response::default() + .add_submessages(msgs) + .add_attribute("action", "update_token_weight") + .add_attribute("sender", info.sender) + .add_attribute("token_id", token_id) + .add_attribute("weight", weight.to_string())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Extension { msg } => match msg { + QueryExt::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?), + QueryExt::ListMembers { start_after, limit } => { + to_binary(&query_list_members(deps, start_after, limit)?) + } + QueryExt::Member { addr, at_height } => { + to_binary(&query_member(deps, addr, at_height)?) + } + QueryExt::TotalWeight { at_height } => to_binary(&query_total_weight(deps, at_height)?), + }, + _ => Cw721Roles::default().query(deps, env, msg), + } +} + +pub fn query_total_weight(deps: Deps, height: Option) -> StdResult { + let weight = match height { + Some(h) => TOTAL.may_load_at_height(deps.storage, h), + None => TOTAL.may_load(deps.storage), + }? + .unwrap_or_default(); + Ok(TotalWeightResponse { weight }) +} + +pub fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { + let addr = deps.api.addr_validate(&addr)?; + let weight = match height { + Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h), + None => MEMBERS.may_load(deps.storage, &addr), + }?; + Ok(MemberResponse { weight }) +} + +pub fn query_list_members( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let addr = maybe_addr(deps.api, start_after)?; + let start = addr.as_ref().map(Bound::exclusive); + + let members = MEMBERS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + item.map(|(addr, weight)| Member { + addr: addr.into(), + weight, + }) + }) + .collect::>()?; + + Ok(MemberListResponse { members }) +} diff --git a/contracts/external/cw721-roles/src/error.rs b/contracts/external/cw721-roles/src/error.rs new file mode 100644 index 000000000..4d63e6efa --- /dev/null +++ b/contracts/external/cw721-roles/src/error.rs @@ -0,0 +1,29 @@ +use cosmwasm_std::{OverflowError, StdError}; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum RolesContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error(transparent)] + Base(#[from] cw721_base::ContractError), + + #[error(transparent)] + HookError(#[from] cw_controllers::HookError), + + #[error("{0}")] + OverflowErr(#[from] OverflowError), + + #[error(transparent)] + Ownable(#[from] cw_ownable::OwnershipError), + + #[error("Cannot burn NFT, member weight would be negative")] + CannotBurn {}, + + #[error("Would result in negative value")] + NegativeValue {}, + + #[error("The submitted weight is equal to the previous value, no change will occur")] + NoWeightChange {}, +} diff --git a/contracts/external/cw721-roles/src/lib.rs b/contracts/external/cw721-roles/src/lib.rs new file mode 100644 index 000000000..fa8b1eadb --- /dev/null +++ b/contracts/external/cw721-roles/src/lib.rs @@ -0,0 +1,16 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::RolesContractError as ContractError; + +// So consumers don't need dependencies to interact with this contract. +pub use cw721_base::MinterResponse; +pub use cw_ownable::{Action, Ownership}; +pub use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; diff --git a/contracts/external/cw721-roles/src/msg.rs b/contracts/external/cw721-roles/src/msg.rs new file mode 100644 index 000000000..fbfb15fd2 --- /dev/null +++ b/contracts/external/cw721-roles/src/msg.rs @@ -0,0 +1,5 @@ +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; + +pub type InstantiateMsg = cw721_base::InstantiateMsg; +pub type ExecuteMsg = cw721_base::ExecuteMsg; +pub type QueryMsg = cw721_base::QueryMsg; diff --git a/contracts/external/cw721-roles/src/state.rs b/contracts/external/cw721-roles/src/state.rs new file mode 100644 index 000000000..fa1a88570 --- /dev/null +++ b/contracts/external/cw721-roles/src/state.rs @@ -0,0 +1,22 @@ +use cosmwasm_std::Addr; +use cw_controllers::Hooks; +use cw_storage_plus::{SnapshotItem, SnapshotMap, Strategy}; + +// Hooks to contracts that will receive staking and unstaking messages. +pub const HOOKS: Hooks = Hooks::new("hooks"); + +/// A historic snapshot of total weight over time +pub const TOTAL: SnapshotItem = SnapshotItem::new( + "total", + "total__checkpoints", + "total__changelog", + Strategy::EveryBlock, +); + +/// A historic list of members and total voting weights +pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( + "members", + "members__checkpoints", + "members__changelog", + Strategy::EveryBlock, +); diff --git a/contracts/external/cw721-roles/src/tests.rs b/contracts/external/cw721-roles/src/tests.rs new file mode 100644 index 000000000..270a1bc0e --- /dev/null +++ b/contracts/external/cw721-roles/src/tests.rs @@ -0,0 +1,588 @@ +use cosmwasm_std::{to_binary, Addr, Binary}; +use cw4::{HooksResponse, Member, MemberListResponse, MemberResponse, TotalWeightResponse}; +use cw721::{NftInfoResponse, OwnerOfResponse}; +use cw_multi_test::{App, Executor}; +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; +use dao_testing::contracts::{cw721_roles_contract, voting_cw721_staked_contract}; +use dao_voting_cw721_staked::msg::{InstantiateMsg as Cw721StakedInstantiateMsg, NftContract}; + +use crate::error::RolesContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +const ALICE: &str = "alice"; +const BOB: &str = "bob"; +const DAO: &str = "dao"; + +pub fn setup() -> (App, Addr) { + let mut app = App::default(); + + let cw721_id = app.store_code(cw721_roles_contract()); + let cw721_addr = app + .instantiate_contract( + cw721_id, + Addr::unchecked(DAO), + &InstantiateMsg { + name: "bad kids".to_string(), + symbol: "bad kids".to_string(), + minter: DAO.to_string(), + }, + &[], + "cw721_roles".to_string(), + None, + ) + .unwrap(); + + (app, cw721_addr) +} + +pub fn query_nft_owner( + app: &App, + nft: &Addr, + token_id: &str, +) -> Result { + let owner = app.wrap().query_wasm_smart( + nft, + &QueryMsg::OwnerOf { + token_id: token_id.to_string(), + include_expired: None, + }, + )?; + Ok(owner) +} + +pub fn query_member( + app: &App, + nft: &Addr, + member: &str, + at_height: Option, +) -> Result { + let member = app.wrap().query_wasm_smart( + nft, + &QueryMsg::Extension { + msg: QueryExt::Member { + addr: member.to_string(), + at_height, + }, + }, + )?; + Ok(member) +} + +pub fn query_total_weight( + app: &App, + nft: &Addr, + at_height: Option, +) -> Result { + let member = app.wrap().query_wasm_smart( + nft, + &QueryMsg::Extension { + msg: QueryExt::TotalWeight { at_height }, + }, + )?; + Ok(member) +} + +pub fn query_token_info( + app: &App, + nft: &Addr, + token_id: &str, +) -> Result, RolesContractError> { + let info = app.wrap().query_wasm_smart( + nft, + &QueryMsg::NftInfo { + token_id: token_id.to_string(), + }, + )?; + Ok(info) +} + +#[test] +fn test_minting_and_burning() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was created successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.extension.weight, 1); + + // Create another token for alice to give her even more total weight + let msg = ExecuteMsg::Mint { + token_id: "2".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Create a token for bob + let msg = ExecuteMsg::Mint { + token_id: "3".to_string(), + owner: BOB.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Query list of members + let members_list: MemberListResponse = app + .wrap() + .query_wasm_smart( + cw721_addr.clone(), + &QueryMsg::Extension { + msg: QueryExt::ListMembers { + start_after: None, + limit: None, + }, + }, + ) + .unwrap(); + assert_eq!( + members_list, + MemberListResponse { + members: vec![ + Member { + addr: ALICE.to_string(), + weight: 2 + }, + Member { + addr: BOB.to_string(), + weight: 1 + } + ] + } + ); + + // Member query returns total weight for alice + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(2)); + + // Total weight is now 3 + let total: TotalWeightResponse = query_total_weight(&app, &cw721_addr, None).unwrap(); + assert_eq!(total.weight, 3); + + // Burn a role for alice + let msg = ExecuteMsg::Burn { + token_id: "2".to_string(), + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token is now gone + let res = query_token_info(&app, &cw721_addr, "2"); + assert!(res.is_err()); + + // Alice's weight has been update acordingly + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(1)); +} + +#[test] +fn test_minting_and_transfer_permissions() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: Some("member".to_string()), + weight: 1, + }, + }; + + // Non-minter can't mint + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // DAO can mint successfully as the minter + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Non-minter can't transfer + let msg = ExecuteMsg::TransferNft { + recipient: BOB.to_string(), + token_id: "1".to_string(), + }; + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // DAO can transfer + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let owner: OwnerOfResponse = query_nft_owner(&app, &cw721_addr, "1").unwrap(); + assert_eq!(owner.owner, BOB); +} + +#[test] +fn test_send_permissions() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: Some("member".to_string()), + weight: 1, + }, + }; + // DAO can mint successfully as the minter + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Instantiate an NFT staking voting contract for testing SendNft + let dao_voting_cw721_staked_id = app.store_code(voting_cw721_staked_contract()); + let cw721_staked_addr = app + .instantiate_contract( + dao_voting_cw721_staked_id, + Addr::unchecked(DAO), + &Cw721StakedInstantiateMsg { + owner: None, + nft_contract: NftContract::Existing { + address: cw721_addr.to_string(), + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721-staking", + None, + ) + .unwrap(); + + // Non-minter can't send + let msg = ExecuteMsg::SendNft { + contract: cw721_staked_addr.to_string(), + token_id: "1".to_string(), + msg: to_binary(&Binary::default()).unwrap(), + }; + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // DAO can send + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Staking contract now owns the NFT + let owner: OwnerOfResponse = query_nft_owner(&app, &cw721_addr, "1").unwrap(); + assert_eq!(owner.owner, cw721_staked_addr.as_str()); +} + +#[test] +fn test_update_token_role() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenRole { + token_id: "1".to_string(), + role: Some("queen".to_string()), + }, + }; + + // Only admin / minter can update role + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Update token role + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was updated successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.extension.role, Some("queen".to_string())); +} + +#[test] +fn test_update_token_uri() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenUri { + token_id: "1".to_string(), + token_uri: Some("ipfs://abc...".to_string()), + }, + }; + + // Only admin / minter can update token_uri + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Update token_uri + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was updated successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.token_uri, Some("ipfs://abc...".to_string())); +} + +#[test] +fn test_update_token_weight() { + let (mut app, cw721_addr) = setup(); + + // Mint token + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenWeight { + token_id: "1".to_string(), + weight: 2, + }, + }; + + // Only admin / minter can update token weight + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Update token weight + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was updated successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.extension.weight, 2); + + // New value should be reflected in member's voting weight + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(2)); + + // Update weight to a smaller value + app.execute_contract( + Addr::unchecked(DAO), + cw721_addr.clone(), + &ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenWeight { + token_id: "1".to_string(), + weight: 1, + }, + }, + &[], + ) + .unwrap(); + + // New value should be reflected in member's voting weight + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(1)); + + // Create another token for alice to give her even more total weight + let msg = ExecuteMsg::Mint { + token_id: "2".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 10, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Alice's weight should be updated to include both tokens + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(11)); + + // Update Alice's second token to 0 weight + // Update weight to a smaller value + app.execute_contract( + Addr::unchecked(DAO), + cw721_addr.clone(), + &ExecuteMsg::Extension { + msg: ExecuteExt::UpdateTokenWeight { + token_id: "2".to_string(), + weight: 0, + }, + }, + &[], + ) + .unwrap(); + + // Alice's voting value should be 1 + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(1)); +} + +#[test] +fn test_zero_weight_token() { + let (mut app, cw721_addr) = setup(); + + // Mint token with zero weight + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 0, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Token was created successfully + let info: NftInfoResponse = query_token_info(&app, &cw721_addr, "1").unwrap(); + assert_eq!(info.extension.weight, 0); + + // Member query returns total weight for alice + let member: MemberResponse = query_member(&app, &cw721_addr, ALICE, None).unwrap(); + assert_eq!(member.weight, Some(0)); +} + +#[test] +fn test_hooks() { + let (mut app, cw721_addr) = setup(); + + // Mint initial NFT + let msg = ExecuteMsg::Mint { + token_id: "1".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::AddHook { + addr: DAO.to_string(), + }, + }; + + // Hook can't be added by non-minter + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Hook can be added by the owner / minter + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Query hooks + let hooks: HooksResponse = app + .wrap() + .query_wasm_smart( + cw721_addr.clone(), + &QueryMsg::Extension { + msg: QueryExt::Hooks {}, + }, + ) + .unwrap(); + assert_eq!( + hooks, + HooksResponse { + hooks: vec![DAO.to_string()] + } + ); + + // Test hook fires when a new member is added + let msg = ExecuteMsg::Mint { + token_id: "2".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }; + // Should error as the DAO is not a contract, meaning hooks fired + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Should also error for burn, as this also fires hooks + let msg = ExecuteMsg::Burn { + token_id: "1".to_string(), + }; + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + let msg = ExecuteMsg::Extension { + msg: ExecuteExt::RemoveHook { + addr: DAO.to_string(), + }, + }; + + // Hook can't be removed by non-minter + app.execute_contract(Addr::unchecked(ALICE), cw721_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Hook can be removed by the owner / minter + app.execute_contract(Addr::unchecked(DAO), cw721_addr.clone(), &msg, &[]) + .unwrap(); + + // Minting should now work again as there are no hooks to dead + app.execute_contract( + Addr::unchecked(DAO), + cw721_addr, + &ExecuteMsg::Mint { + token_id: "2".to_string(), + owner: ALICE.to_string(), + token_uri: Some("ipfs://xyz...".to_string()), + extension: MetadataExt { + role: None, + weight: 1, + }, + }, + &[], + ) + .unwrap(); +} diff --git a/contracts/external/dao-migrator/schema/dao-migrator.json b/contracts/external/dao-migrator/schema/dao-migrator.json index 83713819a..44fec454e 100644 --- a/contracts/external/dao-migrator/schema/dao-migrator.json +++ b/contracts/external/dao-migrator/schema/dao-migrator.json @@ -190,7 +190,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -1212,7 +1212,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -2269,7 +2269,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -3025,7 +3025,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -4206,7 +4206,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -5332,7 +5332,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", 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 dbfb1718a..42a8aa02b 100644 --- a/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json +++ b/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json @@ -73,7 +73,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -712,7 +712,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -1217,7 +1217,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -1875,7 +1875,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", 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 d3b26b591..7b63d7843 100644 --- a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json +++ b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json @@ -190,7 +190,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -1241,7 +1241,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -2183,7 +2183,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -3045,7 +3045,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -4226,7 +4226,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -5364,7 +5364,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs index 24be103df..b6ddf53c4 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs @@ -15,10 +15,8 @@ use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo}, multiple_choice::VotingStrategy, pre_propose::PreProposeInfo, - threshold::PercentageThreshold, + threshold::{ActiveThreshold, ActiveThreshold::AbsoluteCount, PercentageThreshold}, }; -use dao_voting_cw20_staked::msg::ActiveThreshold; -use dao_voting_cw20_staked::msg::ActiveThreshold::AbsoluteCount; use dao_voting_cw4::msg::GroupContract; use crate::testing::tests::ALTERNATIVE_ADDR; @@ -154,7 +152,10 @@ pub fn _instantiate_with_staked_cw721_governance( msg: to_binary(&dao_voting_cw721_staked::msg::InstantiateMsg { owner: Some(Admin::CoreModule {}), unstaking_duration: None, - nft_address: nft_address.to_string(), + nft_contract: dao_voting_cw721_staked::msg::NftContract::Existing { + address: nft_address.to_string(), + }, + active_threshold: None, }) .unwrap(), admin: None, @@ -192,14 +193,12 @@ pub fn _instantiate_with_staked_cw721_governance( app.execute_contract( Addr::unchecked("ekez"), nft_address.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint( - cw721_base::msg::MintMsg::> { - token_id: format!("{address}_{i}"), - owner: address.clone(), - token_uri: None, - extension: None, - }, - ), + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { + token_id: format!("{address}_{i}"), + owner: address.clone(), + token_uri: None, + extension: None, + }, &[], ) .unwrap(); diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs index 69be27727..8ba4d8d73 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs @@ -15,9 +15,8 @@ use dao_voting::{ }, pre_propose::PreProposeInfo, status::Status, - threshold::{PercentageThreshold, Threshold}, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, }; -use dao_voting_cw20_staked::msg::ActiveThreshold; use std::panic; use crate::{ 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 c016b8ef0..3c758538f 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -190,7 +190,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -1212,7 +1212,7 @@ "additionalProperties": false }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -2269,7 +2269,7 @@ ] }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -3025,7 +3025,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -4206,7 +4206,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", @@ -5332,7 +5332,7 @@ } }, "PercentageThreshold": { - "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", + "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `vote_weights >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.", "oneOf": [ { "description": "The majority of voters must vote yes for the proposal to pass.", diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index 98b92867a..30562be2c 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -1,6 +1,5 @@ use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Empty, Uint128}; use cw20::Cw20Coin; -use dao_voting_cw20_staked::msg::ActiveThreshold; use cw_multi_test::{next_block, App, BankSudo, Executor, SudoMsg}; use cw_utils::Duration; @@ -10,7 +9,7 @@ use dao_pre_propose_single as cppbps; use dao_voting::{ deposit::{DepositRefundPolicy, UncheckedDepositInfo}, pre_propose::PreProposeInfo, - threshold::{PercentageThreshold, Threshold::ThresholdQuorum}, + threshold::{ActiveThreshold, PercentageThreshold, Threshold::ThresholdQuorum}, }; use dao_voting_cw4::msg::GroupContract; @@ -149,7 +148,10 @@ pub(crate) fn instantiate_with_staked_cw721_governance( msg: to_binary(&dao_voting_cw721_staked::msg::InstantiateMsg { owner: Some(Admin::CoreModule {}), unstaking_duration: None, - nft_address: nft_address.to_string(), + nft_contract: dao_voting_cw721_staked::msg::NftContract::Existing { + address: nft_address.to_string(), + }, + active_threshold: None, }) .unwrap(), admin: None, @@ -186,14 +188,12 @@ pub(crate) fn instantiate_with_staked_cw721_governance( app.execute_contract( Addr::unchecked("ekez"), nft_address.clone(), - &cw721_base::msg::ExecuteMsg::, Empty>::Mint( - cw721_base::msg::MintMsg::> { - token_id: format!("{address}_{i}"), - owner: address.clone(), - token_uri: None, - extension: None, - }, - ), + &cw721_base::msg::ExecuteMsg::, Empty>::Mint { + token_id: format!("{address}_{i}"), + owner: address.clone(), + token_uri: None, + extension: None, + }, &[], ) .unwrap(); diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index 5bc4a8801..b08236064 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -21,10 +21,9 @@ use dao_voting::{ mask_proposal_hook_index, mask_vote_hook_index, }, status::Status, - threshold::{PercentageThreshold, Threshold}, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, voting::{Vote, Votes}, }; -use dao_voting_cw20_staked::msg::ActiveThreshold; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, diff --git a/contracts/voting/dao-voting-cw20-staked/Cargo.toml b/contracts/voting/dao-voting-cw20-staked/Cargo.toml index 143da5454..974d1e6f7 100644 --- a/contracts/voting/dao-voting-cw20-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw20-staked/Cargo.toml @@ -21,14 +21,15 @@ cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } cosmwasm-storage = { workspace = true } cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } cw2 = { workspace = true } cw20 = { workspace = true } -cw-utils = { workspace = true } +cw20-base = { workspace = true, features = ["library"] } +cw20-stake = { workspace = true, features = ["library"] } thiserror = { workspace = true } dao-macros = { workspace = true } dao-interface = { workspace = true } -cw20-stake = { workspace = true } -cw20-base = { workspace = true, features = ["library"] } +dao-voting = { workspace = true } [dev-dependencies] cw-multi-test = { workspace = true } diff --git a/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json b/contracts/voting/dao-voting-cw20-staked/schema/dao-voting-cw20-staked.json index 5dfe64bc6..cbd74d332 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 @@ -11,6 +11,7 @@ ], "properties": { "active_threshold": { + "description": "The number or percentage of tokens that must be staked for the DAO to be active", "anyOf": [ { "$ref": "#/definitions/ActiveThreshold" diff --git a/contracts/voting/dao-voting-cw20-staked/src/contract.rs b/contracts/voting/dao-voting-cw20-staked/src/contract.rs index ef3cb602b..53ab5b4d6 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/contract.rs @@ -8,12 +8,13 @@ use cw2::set_contract_version; use cw20::{Cw20Coin, TokenInfoResponse}; use cw_utils::parse_reply_instantiate_data; use dao_interface::voting::IsActiveResponse; +use dao_voting::threshold::ActiveThreshold; use std::convert::TryInto; use crate::error::ContractError; use crate::msg::{ - ActiveThreshold, ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, - StakingInfo, TokenInfo, + ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StakingInfo, + TokenInfo, }; use crate::state::{ ACTIVE_THRESHOLD, DAO, STAKING_CONTRACT, STAKING_CONTRACT_CODE_ID, diff --git a/contracts/voting/dao-voting-cw20-staked/src/msg.rs b/contracts/voting/dao-voting-cw20-staked/src/msg.rs index b25a9a200..24078427f 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/msg.rs @@ -1,10 +1,11 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Decimal, Uint128}; +use cosmwasm_std::Uint128; use cw20::Cw20Coin; use cw20_base::msg::InstantiateMarketingInfo; use cw_utils::Duration; use dao_macros::{active_query, token_query, voting_module_query}; +use dao_voting::threshold::ActiveThreshold; /// Information about the staking contract to be used with this voting /// module. @@ -51,24 +52,11 @@ pub enum TokenInfo { }, } -/// The threshold of tokens that must be staked in order for this -/// voting module to be active. If this is not reached, this module -/// will response to `is_active` queries with false and proposal -/// modules which respect active thresholds will not allow the -/// creation of proposals. -#[cw_serde] -pub enum ActiveThreshold { - /// The absolute number of tokens that must be staked for the - /// module to be active. - AbsoluteCount { count: Uint128 }, - /// The percentage of tokens that must be staked for the module to - /// be active. Computed as `staked / total_supply`. - Percentage { percent: Decimal }, -} - #[cw_serde] pub struct InstantiateMsg { pub token_info: TokenInfo, + /// The number or percentage of tokens that must be staked + /// for the DAO to be active pub active_threshold: Option, } diff --git a/contracts/voting/dao-voting-cw20-staked/src/state.rs b/contracts/voting/dao-voting-cw20-staked/src/state.rs index 05ea27233..d4e61d397 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/state.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/state.rs @@ -1,7 +1,7 @@ -use crate::msg::ActiveThreshold; use cosmwasm_std::Addr; use cw_storage_plus::Item; use cw_utils::Duration; +use dao_voting::threshold::ActiveThreshold; pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); pub const TOKEN: Item = Item::new("token"); diff --git a/contracts/voting/dao-voting-cw20-staked/src/tests.rs b/contracts/voting/dao-voting-cw20-staked/src/tests.rs index 06c89f2dc..ca793736b 100644 --- a/contracts/voting/dao-voting-cw20-staked/src/tests.rs +++ b/contracts/voting/dao-voting-cw20-staked/src/tests.rs @@ -6,13 +6,11 @@ use cw2::ContractVersion; use cw20::{BalanceResponse, Cw20Coin, MinterResponse, TokenInfoResponse}; use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor}; use dao_interface::voting::{InfoResponse, IsActiveResponse, VotingPowerAtHeightResponse}; +use dao_voting::threshold::ActiveThreshold; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, - msg::{ - ActiveThreshold, ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, - StakingInfo, - }, + msg::{ActiveThresholdResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StakingInfo}, }; const DAO_ADDR: &str = "dao"; diff --git a/contracts/voting/dao-voting-cw721-roles/.cargo/config b/contracts/voting/dao-voting-cw721-roles/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/voting/dao-voting-cw721-roles/Cargo.toml b/contracts/voting/dao-voting-cw721-roles/Cargo.toml new file mode 100644 index 000000000..8fa5354a1 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "dao-voting-cw721-roles" +authors = ["Jake Hartnell"] +description = "A DAO DAO voting module based on non-transferrable cw721 tokens." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +dao-cw721-extensions = { workspace = true } +dao-macros = { workspace = true } +dao-interface = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +cw721-controllers = { workspace = true } +cw-ownable = { workspace = true } +cw-paginate = { workspace = true } +cw721 = { workspace = true } +cw-utils = { workspace = true } +cw2 = { workspace = true } +cw4 = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +cw721-roles = { workspace = true } +anyhow = { workspace = true } +dao-testing = { workspace = true } diff --git a/contracts/voting/dao-voting-cw721-roles/README.md b/contracts/voting/dao-voting-cw721-roles/README.md new file mode 100644 index 000000000..f15f4e20a --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/README.md @@ -0,0 +1,3 @@ +# dao-voting-cw721-roles + +This contract works in conjunction with the [cw721-roles contract](../../external/cw721-roles), and allows for a DAO with non-transferrable roles that can have different weights for voting power. This contract implements the interface needed to be a DAO DAO [voting module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module). diff --git a/contracts/voting/dao-voting-cw721-roles/examples/schema.rs b/contracts/voting/dao-voting-cw721-roles/examples/schema.rs new file mode 100644 index 000000000..0e391c586 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use dao_voting_cw721_roles::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json b/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json new file mode 100644 index 000000000..c6b62b000 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/schema/dao-voting-cw721-roles.json @@ -0,0 +1,378 @@ +{ + "contract_name": "dao-voting-cw721-roles", + "contract_version": "2.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "nft_contract" + ], + "properties": { + "nft_contract": { + "description": "Info about the associated NFT contract", + "allOf": [ + { + "$ref": "#/definitions/NftContract" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "MetadataExt": { + "type": "object", + "required": [ + "weight" + ], + "properties": { + "role": { + "description": "Optional on-chain role for this member, can be used by other contracts to enforce permissions", + "type": [ + "string", + "null" + ] + }, + "weight": { + "description": "The voting weight of this role", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "NftContract": { + "oneOf": [ + { + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of an already instantiated cw721-weighted-roles token contract.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "new" + ], + "properties": { + "new": { + "type": "object", + "required": [ + "code_id", + "initial_nfts", + "label", + "name", + "symbol" + ], + "properties": { + "code_id": { + "description": "Code ID for cw721 token contract.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "initial_nfts": { + "description": "Initial NFTs to mint when instantiating the new cw721 contract. If empty, an error is thrown.", + "type": "array", + "items": { + "$ref": "#/definitions/NftMintMsg" + } + }, + "label": { + "description": "Label to use for instantiated cw721 contract.", + "type": "string" + }, + "name": { + "description": "NFT collection name", + "type": "string" + }, + "symbol": { + "description": "NFT collection symbol", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "NftMintMsg": { + "type": "object", + "required": [ + "extension", + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "allOf": [ + { + "$ref": "#/definitions/MetadataExt" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "type": "string", + "enum": [] + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the voting power for an address at a given height.", + "type": "object", + "required": [ + "voting_power_at_height" + ], + "properties": { + "voting_power_at_height": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total voting power at a given block heigh.", + "type": "object", + "required": [ + "total_power_at_height" + ], + "properties": { + "total_power_at_height": { + "type": "object", + "properties": { + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the address of the DAO this module belongs to.", + "type": "object", + "required": [ + "dao" + ], + "properties": { + "dao": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns contract version info.", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": null, + "sudo": null, + "responses": { + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "type": "object", + "required": [ + "nft_address" + ], + "properties": { + "nft_address": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, + "dao": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Addr", + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InfoResponse", + "type": "object", + "required": [ + "info" + ], + "properties": { + "info": { + "$ref": "#/definitions/ContractVersion" + } + }, + "additionalProperties": false, + "definitions": { + "ContractVersion": { + "type": "object", + "required": [ + "contract", + "version" + ], + "properties": { + "contract": { + "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing", + "type": "string" + }, + "version": { + "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)", + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "total_power_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "voting_power_at_height": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VotingPowerAtHeightResponse", + "type": "object", + "required": [ + "height", + "power" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "power": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + } + } +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/contract.rs b/contracts/voting/dao-voting-cw721-roles/src/contract.rs new file mode 100644 index 000000000..62d6ba185 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/contract.rs @@ -0,0 +1,232 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response, StdResult, SubMsg, + WasmMsg, +}; +use cw2::set_contract_version; +use cw4::{MemberResponse, TotalWeightResponse}; +use cw721_base::{ + ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg, QueryMsg as Cw721QueryMsg, +}; +use cw_ownable::Action; +use cw_utils::parse_reply_instantiate_data; +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt, QueryExt}; + +use crate::msg::{ExecuteMsg, InstantiateMsg, NftContract, QueryMsg}; +use crate::state::{Config, CONFIG, DAO, INITITIAL_NFTS}; +use crate::ContractError; + +pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cw721-roles"; +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const INSTANTIATE_NFT_CONTRACT_REPLY_ID: u64 = 0; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result, ContractError> { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + DAO.save(deps.storage, &info.sender)?; + + match msg.nft_contract { + NftContract::Existing { address } => { + let config = Config { + nft_address: deps.api.addr_validate(&address)?, + }; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attribute("nft_contract", address)) + } + NftContract::New { + code_id, + label, + name, + symbol, + initial_nfts, + } => { + // Check there is at least one NFT to initialize + if initial_nfts.is_empty() { + return Err(ContractError::NoInitialNfts {}); + } + + // Save initial NFTs for use in reply + INITITIAL_NFTS.save(deps.storage, &initial_nfts)?; + + // Create instantiate submessage for NFT roles contract + let msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + code_id, + funds: vec![], + admin: Some(info.sender.to_string()), + label, + msg: to_binary(&Cw721InstantiateMsg { + name, + symbol, + // Admin must be set to contract to mint initial NFTs + minter: env.contract.address.to_string(), + })?, + }, + INSTANTIATE_NFT_CONTRACT_REPLY_ID, + ); + + Ok(Response::default().add_submessage(msg)) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsg, +) -> Result, ContractError> { + Err(ContractError::NoExecute {}) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => query_config(deps), + QueryMsg::Dao {} => query_dao(deps), + QueryMsg::VotingPowerAtHeight { address, height } => { + query_voting_power_at_height(deps, env, address, height) + } + QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height), + QueryMsg::Info {} => query_info(deps), + } +} + +pub fn query_voting_power_at_height( + deps: Deps, + env: Env, + address: String, + at_height: Option, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let member: MemberResponse = deps.querier.query_wasm_smart( + config.nft_address, + &Cw721QueryMsg::::Extension { + msg: QueryExt::Member { + addr: address, + at_height, + }, + }, + )?; + + to_binary(&dao_interface::voting::VotingPowerAtHeightResponse { + power: member.weight.unwrap_or(0).into(), + height: at_height.unwrap_or(env.block.height), + }) +} + +pub fn query_total_power_at_height( + deps: Deps, + env: Env, + at_height: Option, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let total: TotalWeightResponse = deps.querier.query_wasm_smart( + config.nft_address, + &Cw721QueryMsg::::Extension { + msg: QueryExt::TotalWeight { at_height }, + }, + )?; + + to_binary(&dao_interface::voting::TotalPowerAtHeightResponse { + power: total.weight.into(), + height: at_height.unwrap_or(env.block.height), + }) +} + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + to_binary(&config) +} + +pub fn query_dao(deps: Deps) -> StdResult { + let dao = DAO.load(deps.storage)?; + to_binary(&dao) +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = cw2::get_contract_version(deps.storage)?; + to_binary(&dao_interface::voting::InfoResponse { info }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + INSTANTIATE_NFT_CONTRACT_REPLY_ID => { + let res = parse_reply_instantiate_data(msg); + match res { + Ok(res) => { + let dao = DAO.load(deps.storage)?; + let nft_contract = res.contract_address; + + // Save config + let config = Config { + nft_address: deps.api.addr_validate(&nft_contract)?, + }; + CONFIG.save(deps.storage, &config)?; + + let initial_nfts = INITITIAL_NFTS.load(deps.storage)?; + + // Add mint submessages + let mint_submessages: Vec = initial_nfts + .iter() + .flat_map(|nft| -> Result { + Ok(SubMsg::new(WasmMsg::Execute { + contract_addr: nft_contract.clone(), + funds: vec![], + msg: to_binary( + &Cw721ExecuteMsg::::Mint { + token_id: nft.token_id.clone(), + owner: nft.owner.clone(), + token_uri: nft.token_uri.clone(), + extension: MetadataExt { + role: nft.clone().extension.role, + weight: nft.extension.weight, + }, + }, + )?, + })) + }) + .collect::>(); + + // Clear space + INITITIAL_NFTS.remove(deps.storage); + + // Update minter message + let update_minter_msg = WasmMsg::Execute { + contract_addr: nft_contract.clone(), + msg: to_binary( + &Cw721ExecuteMsg::::UpdateOwnership( + Action::TransferOwnership { + new_owner: dao.to_string(), + expiry: None, + }, + ), + )?, + funds: vec![], + }; + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attribute("nft_contract", nft_contract) + .add_message(update_minter_msg) + .add_submessages(mint_submessages)) + } + Err(_) => Err(ContractError::NftInstantiateError {}), + } + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/error.rs b/contracts/voting/dao-voting-cw721-roles/src/error.rs new file mode 100644 index 000000000..2fa498222 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/error.rs @@ -0,0 +1,23 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error(transparent)] + Std(#[from] StdError), + + #[error("Error instantiating cw721-roles contract")] + NftInstantiateError {}, + + #[error("This contract only supports queries")] + NoExecute {}, + + #[error("New cw721-roles contract must be instantiated with at least one NFT")] + NoInitialNfts {}, + + #[error("Only the owner of this contract my execute this message")] + NotOwner {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/lib.rs b/contracts/voting/dao-voting-cw721-roles/src/lib.rs new file mode 100644 index 000000000..d4a73c5be --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/lib.rs @@ -0,0 +1,11 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod msg; +pub mod state; + +#[cfg(test)] +mod testing; + +pub use crate::error::ContractError; diff --git a/contracts/voting/dao-voting-cw721-roles/src/msg.rs b/contracts/voting/dao-voting-cw721-roles/src/msg.rs new file mode 100644 index 000000000..b9bdf4d8d --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/msg.rs @@ -0,0 +1,55 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use dao_cw721_extensions::roles::MetadataExt; +use dao_macros::voting_module_query; + +#[cw_serde] +pub struct NftMintMsg { + /// Unique ID of the NFT + pub token_id: String, + /// The owner of the newly minter NFT + pub owner: String, + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + /// Any custom extension used by this contract + pub extension: MetadataExt, +} + +#[cw_serde] +pub enum NftContract { + Existing { + /// Address of an already instantiated cw721-weighted-roles token contract. + address: String, + }, + New { + /// Code ID for cw721 token contract. + code_id: u64, + /// Label to use for instantiated cw721 contract. + label: String, + /// NFT collection name + name: String, + /// NFT collection symbol + symbol: String, + /// Initial NFTs to mint when instantiating the new cw721 contract. + /// If empty, an error is thrown. + initial_nfts: Vec, + }, +} + +#[cw_serde] +pub struct InstantiateMsg { + /// Info about the associated NFT contract + pub nft_contract: NftContract, +} + +#[cw_serde] +pub enum ExecuteMsg {} + +#[voting_module_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(crate::state::Config)] + Config {}, +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/state.rs b/contracts/voting/dao-voting-cw721-roles/src/state.rs new file mode 100644 index 000000000..de55f8d3d --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/state.rs @@ -0,0 +1,16 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +use crate::msg::NftMintMsg; + +#[cw_serde] +pub struct Config { + pub nft_address: Addr, +} + +pub const CONFIG: Item = Item::new("config"); +pub const DAO: Item = Item::new("dao"); + +// Holds initial NFTs messages during instantiation. +pub const INITITIAL_NFTS: Item> = Item::new("initial_nfts"); diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/execute.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/execute.rs new file mode 100644 index 000000000..081a2beaf --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/execute.rs @@ -0,0 +1,28 @@ +use cosmwasm_std::Addr; +use cw_multi_test::{App, AppResponse, Executor}; +use dao_cw721_extensions::roles::{ExecuteExt, MetadataExt}; + +use anyhow::Result as AnyResult; + +pub fn mint_nft( + app: &mut App, + cw721: &Addr, + sender: &str, + receiver: &str, + token_id: &str, +) -> AnyResult { + app.execute_contract( + Addr::unchecked(sender), + cw721.clone(), + &cw721_base::ExecuteMsg::::Mint { + token_id: token_id.to_string(), + owner: receiver.to_string(), + token_uri: None, + extension: MetadataExt { + role: Some("admin".to_string()), + weight: 1, + }, + }, + &[], + ) +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/instantiate.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/instantiate.rs new file mode 100644 index 000000000..59cabddc7 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/instantiate.rs @@ -0,0 +1,24 @@ +use cosmwasm_std::Addr; +use cw_multi_test::{App, Executor}; +use dao_testing::contracts::cw721_roles_contract; + +pub fn instantiate_cw721_roles(app: &mut App, sender: &str, minter: &str) -> (Addr, u64) { + let cw721_id = app.store_code(cw721_roles_contract()); + + let cw721_addr = app + .instantiate_contract( + cw721_id, + Addr::unchecked(sender), + &cw721_base::InstantiateMsg { + name: "bad kids".to_string(), + symbol: "bad kids".to_string(), + minter: minter.to_string(), + }, + &[], + "cw721_roles".to_string(), + None, + ) + .unwrap(); + + (cw721_addr, cw721_id) +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/mod.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/mod.rs new file mode 100644 index 000000000..98cebccd9 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/mod.rs @@ -0,0 +1,47 @@ +mod execute; +mod instantiate; +mod queries; +mod tests; + +use cosmwasm_std::Addr; +use cw_multi_test::{App, Executor}; +use dao_testing::contracts::dao_voting_cw721_roles_contract; + +use crate::msg::{InstantiateMsg, NftContract, NftMintMsg}; + +use self::instantiate::instantiate_cw721_roles; + +/// Address used as the owner, instantiator, and minter. +pub(crate) const CREATOR_ADDR: &str = "creator"; + +pub(crate) struct CommonTest { + app: App, + module_addr: Addr, +} + +pub(crate) fn setup_test(initial_nfts: Vec) -> CommonTest { + let mut app = App::default(); + let module_id = app.store_code(dao_voting_cw721_roles_contract()); + + let (_, cw721_id) = instantiate_cw721_roles(&mut app, CREATOR_ADDR, CREATOR_ADDR); + let module_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "cw721-roles".to_string(), + name: "Job Titles".to_string(), + symbol: "TITLES".to_string(), + initial_nfts, + }, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + CommonTest { app, module_addr } +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/queries.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/queries.rs new file mode 100644 index 000000000..dfc3f3468 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/queries.rs @@ -0,0 +1,52 @@ +use cosmwasm_std::{Addr, StdResult}; +use cw_multi_test::App; +use dao_cw721_extensions::roles::QueryExt; +use dao_interface::voting::{ + InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +}; + +use crate::{msg::QueryMsg, state::Config}; + +pub fn query_config(app: &App, module: &Addr) -> StdResult { + let config = app.wrap().query_wasm_smart(module, &QueryMsg::Config {})?; + Ok(config) +} + +pub fn query_voting_power( + app: &App, + module: &Addr, + addr: &str, + height: Option, +) -> StdResult { + let power = app.wrap().query_wasm_smart( + module, + &QueryMsg::VotingPowerAtHeight { + address: addr.to_string(), + height, + }, + )?; + Ok(power) +} + +pub fn query_total_power( + app: &App, + module: &Addr, + height: Option, +) -> StdResult { + let power = app + .wrap() + .query_wasm_smart(module, &QueryMsg::TotalPowerAtHeight { height })?; + Ok(power) +} + +pub fn query_info(app: &App, module: &Addr) -> StdResult { + let info = app.wrap().query_wasm_smart(module, &QueryMsg::Info {})?; + Ok(info) +} + +pub fn query_minter(app: &App, nft: &Addr) -> StdResult { + let minter = app + .wrap() + .query_wasm_smart(nft, &cw721_base::QueryMsg::::Minter {})?; + Ok(minter) +} diff --git a/contracts/voting/dao-voting-cw721-roles/src/testing/tests.rs b/contracts/voting/dao-voting-cw721-roles/src/testing/tests.rs new file mode 100644 index 000000000..78753a430 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-roles/src/testing/tests.rs @@ -0,0 +1,126 @@ +use cosmwasm_std::{Addr, Uint128}; +use cw_multi_test::{App, Executor}; +use dao_cw721_extensions::roles::MetadataExt; +use dao_testing::contracts::dao_voting_cw721_roles_contract; + +use crate::{ + msg::{InstantiateMsg, NftContract, NftMintMsg}, + state::Config, + testing::{ + execute::mint_nft, + queries::{query_config, query_info, query_minter, query_total_power, query_voting_power}, + }, +}; + +use super::{instantiate::instantiate_cw721_roles, setup_test, CommonTest, CREATOR_ADDR}; + +#[test] +fn test_info_query_works() -> anyhow::Result<()> { + let CommonTest { + app, module_addr, .. + } = setup_test(vec![NftMintMsg { + token_id: "1".to_string(), + owner: CREATOR_ADDR.to_string(), + token_uri: None, + extension: MetadataExt { + role: None, + weight: 1, + }, + }]); + let info = query_info(&app, &module_addr)?; + assert_eq!(info.info.version, env!("CARGO_PKG_VERSION").to_string()); + Ok(()) +} + +#[test] +#[should_panic(expected = "New cw721-roles contract must be instantiated with at least one NFT")] +fn test_instantiate_no_roles_fails() { + setup_test(vec![]); +} + +#[test] +fn test_use_existing_nft_contract() { + let mut app = App::default(); + let module_id = app.store_code(dao_voting_cw721_roles_contract()); + + let (cw721_addr, _) = instantiate_cw721_roles(&mut app, CREATOR_ADDR, CREATOR_ADDR); + let module_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Existing { + address: cw721_addr.clone().to_string(), + }, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + // Get total power + let total = query_total_power(&app, &module_addr, None).unwrap(); + assert_eq!(total.power, Uint128::zero()); + + // Creator mints themselves a new NFT + mint_nft(&mut app, &cw721_addr, CREATOR_ADDR, CREATOR_ADDR, "1").unwrap(); + + // Get voting power for creator + let vp = query_voting_power(&app, &module_addr, CREATOR_ADDR, None).unwrap(); + assert_eq!(vp.power, Uint128::new(1)); +} + +#[test] +fn test_voting_queries() { + let CommonTest { + mut app, + module_addr, + .. + } = setup_test(vec![NftMintMsg { + token_id: "1".to_string(), + owner: CREATOR_ADDR.to_string(), + token_uri: None, + extension: MetadataExt { + role: Some("admin".to_string()), + weight: 1, + }, + }]); + + // Get config + let config: Config = query_config(&app, &module_addr).unwrap(); + let cw721_addr = config.nft_address; + + // Get NFT minter + let minter = query_minter(&app, &cw721_addr.clone()).unwrap(); + // Minter should be the contract that instantiated the cw721 contract. + // In the test setup, this is the module_addr but would normally be + // the dao-core contract. + assert_eq!(minter.minter, Some(module_addr.to_string())); + + // Get total power + let total = query_total_power(&app, &module_addr, None).unwrap(); + assert_eq!(total.power, Uint128::new(1)); + + // Get voting power for creator + let vp = query_voting_power(&app, &module_addr, CREATOR_ADDR, None).unwrap(); + assert_eq!(vp.power, Uint128::new(1)); + + // Mint a new NFT + mint_nft( + &mut app, + &cw721_addr, + module_addr.as_ref(), + CREATOR_ADDR, + "2", + ) + .unwrap(); + + // Get total power + let total = query_total_power(&app, &module_addr, None).unwrap(); + assert_eq!(total.power, Uint128::new(2)); + + // Get voting power for creator + let vp = query_voting_power(&app, &module_addr, CREATOR_ADDR, None).unwrap(); + assert_eq!(vp.power, Uint128::new(2)); +} diff --git a/contracts/voting/dao-voting-cw721-staked/Cargo.toml b/contracts/voting/dao-voting-cw721-staked/Cargo.toml index 884db9fa0..a952ebe52 100644 --- a/contracts/voting/dao-voting-cw721-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw721-staked/Cargo.toml @@ -21,15 +21,16 @@ cw-storage-plus = { workspace = true } cw-controllers = { workspace = true } dao-macros = { workspace = true } dao-interface = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } cw721-controllers = { workspace = true } cw-paginate = { workspace = true } -cw721 = { workspace = true } cw-utils = { workspace = true } cw2 = { workspace = true } +dao-voting = { workspace = true } thiserror = { workspace = true } [dev-dependencies] -cw721-base = { workspace = true } cw-multi-test = { workspace = true } anyhow = { workspace = true } dao-testing = { workspace = true } 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 78e04e12d..ef51fca01 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 @@ -7,12 +7,27 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "nft_address" + "nft_contract" ], "properties": { - "nft_address": { + "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" + } + ] + }, + "nft_contract": { "description": "Address of the cw721 NFT contract that may be staked.", - "type": "string" + "allOf": [ + { + "$ref": "#/definitions/NftContract" + } + ] }, "owner": { "description": "May change unstaking duration and add hooks.", @@ -39,6 +54,55 @@ }, "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 + } + ] + }, "Admin": { "description": "Information about the CosmWasm level admin of a contract. Used in conjunction with `ModuleInstantiateInfo` to instantiate modules.", "oneOf": [ @@ -80,6 +144,10 @@ } ] }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "Duration": { "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", "oneOf": [ @@ -113,6 +181,121 @@ "additionalProperties": false } ] + }, + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" + }, + "NftContract": { + "oneOf": [ + { + "type": "object", + "required": [ + "existing" + ], + "properties": { + "existing": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of an already instantiated cw721 token contract.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "new" + ], + "properties": { + "new": { + "type": "object", + "required": [ + "code_id", + "initial_nfts", + "label", + "name", + "symbol" + ], + "properties": { + "code_id": { + "description": "Code ID for cw721 token contract.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "initial_nfts": { + "description": "Initial NFTs to mint when creating the NFT contract. If empty, an error is thrown.", + "type": "array", + "items": { + "$ref": "#/definitions/NftMintMsg" + } + }, + "label": { + "description": "Label to use for instantiated cw721 contract.", + "type": "string" + }, + "name": { + "description": "NFT collection name", + "type": "string" + }, + "symbol": { + "description": "NFT collection symbol", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "NftMintMsg": { + "type": "object", + "required": [ + "extension", + "owner", + "token_id" + ], + "properties": { + "extension": { + "description": "Any custom extension used by this contract", + "allOf": [ + { + "$ref": "#/definitions/Empty" + } + ] + }, + "owner": { + "description": "The owner of the newly minter NFT", + "type": "string" + }, + "token_id": { + "description": "Unique ID of the NFT", + "type": "string" + }, + "token_uri": { + "description": "Universal resource identifier for this NFT Should point to a JSON file that conforms to the ERC721 Metadata JSON Schema", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "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" } } }, @@ -243,9 +426,84 @@ } }, "additionalProperties": false + }, + { + "description": "Sets the active threshold to a new value. Only the instantiator this contract (a DAO most likely) may call this method.", + "type": "object", + "required": [ + "update_active_threshold" + ], + "properties": { + "update_active_threshold": { + "type": "object", + "properties": { + "new_threshold": { + "anyOf": [ + { + "$ref": "#/definitions/ActiveThreshold" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { + "ActiveThreshold": { + "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.", + "oneOf": [ + { + "description": "The absolute number of tokens that must be staked for the module to be active.", + "type": "object", + "required": [ + "absolute_count" + ], + "properties": { + "absolute_count": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.", + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "object", + "required": [ + "percent" + ], + "properties": { + "percent": { + "$ref": "#/definitions/Decimal" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, "Binary": { "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" @@ -271,6 +529,10 @@ }, "additionalProperties": false }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, "Duration": { "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", "oneOf": [ @@ -304,6 +566,10 @@ "additionalProperties": false } ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" } } }, @@ -393,6 +659,32 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "active_threshold" + ], + "properties": { + "active_threshold": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "is_active" + ], + "properties": { + "is_active": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Returns the voting power for an address at a given height.", "type": "object", @@ -480,6 +772,83 @@ "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", @@ -611,6 +980,11 @@ } } }, + "is_active": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Boolean", + "type": "boolean" + }, "nft_claims": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "NftClaimsResponse", diff --git a/contracts/voting/dao-voting-cw721-staked/src/contract.rs b/contracts/voting/dao-voting-cw721-staked/src/contract.rs index 9a601020f..fea817858 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/contract.rs @@ -1,24 +1,34 @@ use crate::hooks::{stake_hook_msgs, unstake_hook_msgs}; -#[cfg(not(feature = "library"))] +use crate::msg::{ActiveThresholdResponse, NftContract}; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::state::{ - register_staked_nft, register_unstaked_nfts, Config, CONFIG, DAO, HOOKS, MAX_CLAIMS, - NFT_BALANCES, NFT_CLAIMS, STAKED_NFTS_PER_OWNER, TOTAL_STAKED_NFTS, + register_staked_nft, register_unstaked_nfts, Config, ACTIVE_THRESHOLD, CONFIG, DAO, HOOKS, + INITITIAL_NFTS, MAX_CLAIMS, NFT_BALANCES, NFT_CLAIMS, STAKED_NFTS_PER_OWNER, TOTAL_STAKED_NFTS, }; use crate::ContractError; +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; use cosmwasm_std::{ - entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, - StdResult, Uint128, WasmMsg, + to_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env, MessageInfo, Reply, + Response, StdResult, SubMsg, Uint128, Uint256, WasmMsg, }; use cw2::set_contract_version; -use cw721::Cw721ReceiveMsg; +use cw721::{Cw721ReceiveMsg, NumTokensResponse}; use cw_storage_plus::Bound; -use cw_utils::Duration; +use cw_utils::{parse_reply_instantiate_data, Duration}; +use dao_interface::voting::IsActiveResponse; use dao_interface::Admin; +use dao_voting::threshold::ActiveThreshold; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cw721-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); +const INSTANTIATE_NFT_CONTRACT_REPLY_ID: u64 = 0; + +// 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, @@ -35,28 +45,95 @@ pub fn instantiate( .as_ref() .map(|owner| match owner { Admin::Address { addr } => deps.api.addr_validate(addr), - Admin::CoreModule {} => Ok(info.sender), + Admin::CoreModule {} => Ok(info.sender.clone()), }) .transpose()?; - let config = Config { - owner: owner.clone(), - nft_address: deps.api.addr_validate(&msg.nft_address)?, - unstaking_duration: msg.unstaking_duration, - }; - CONFIG.save(deps.storage, &config)?; + if let Some(active_threshold) = msg.active_threshold.as_ref() { + match active_threshold { + ActiveThreshold::Percentage { percent } => { + if percent > &Decimal::percent(100) || percent.is_zero() { + return Err(ContractError::InvalidActivePercentage {}); + } + } + ActiveThreshold::AbsoluteCount { count } => { + if count.is_zero() { + return Err(ContractError::ZeroActiveCount {}); + } + } + } + ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?; + } TOTAL_STAKED_NFTS.save(deps.storage, &Uint128::zero(), env.block.height)?; - Ok(Response::default() - .add_attribute("method", "instantiate") - .add_attribute("nft_contract", msg.nft_address) - .add_attribute( - "owner", - owner - .map(|a| a.into_string()) - .unwrap_or_else(|| "None".to_string()), - )) + match msg.nft_contract { + NftContract::Existing { address } => { + let config = Config { + owner: owner.clone(), + nft_address: deps.api.addr_validate(&address)?, + unstaking_duration: msg.unstaking_duration, + }; + CONFIG.save(deps.storage, &config)?; + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attribute("nft_contract", address) + .add_attribute( + "owner", + owner + .map(|a| a.into_string()) + .unwrap_or_else(|| "None".to_string()), + )) + } + NftContract::New { + code_id, + label, + name, + symbol, + initial_nfts, + } => { + // Check there is at least one NFT to initialize + if initial_nfts.is_empty() { + return Err(ContractError::NoInitialNfts {}); + } + + // Save config with empty nft_address + let config = Config { + owner: owner.clone(), + nft_address: Addr::unchecked(""), + unstaking_duration: msg.unstaking_duration, + }; + CONFIG.save(deps.storage, &config)?; + + // Save initial NFTs for use in reply + INITITIAL_NFTS.save(deps.storage, &initial_nfts)?; + + // Create instantiate submessage for NFT roles contract + let msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + code_id, + funds: vec![], + admin: Some(info.sender.to_string()), + label, + msg: to_binary(&cw721_base::msg::InstantiateMsg { + name, + symbol, + // Admin must be set to contract to mint initial NFTs + minter: env.contract.address.to_string(), + })?, + }, + INSTANTIATE_NFT_CONTRACT_REPLY_ID, + ); + + Ok(Response::default().add_submessage(msg).add_attribute( + "owner", + owner + .map(|a| a.into_string()) + .unwrap_or_else(|| "None".to_string()), + )) + } + } } #[cfg_attr(not(feature = "library"), entry_point)] @@ -75,6 +152,9 @@ pub fn execute( } 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) + } } } @@ -304,23 +384,132 @@ pub fn execute_remove_hook( .add_attribute("hook", addr)) } +pub fn execute_update_active_threshold( + deps: DepsMut, + _env: Env, + info: MessageInfo, + new_active_threshold: Option, +) -> Result { + let dao = DAO.load(deps.storage)?; + if info.sender != dao { + return Err(ContractError::Unauthorized {}); + } + + if let Some(active_threshold) = new_active_threshold { + match active_threshold { + ActiveThreshold::Percentage { percent } => { + if percent > Decimal::percent(100) || percent.is_zero() { + return Err(ContractError::InvalidActivePercentage {}); + } + } + ActiveThreshold::AbsoluteCount { count } => { + if count.is_zero() { + return Err(ContractError::ZeroActiveCount {}); + } + } + } + ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; + } else { + ACTIVE_THRESHOLD.remove(deps.storage); + } + + Ok(Response::new().add_attribute("action", "update_active_threshold")) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { + QueryMsg::ActiveThreshold {} => query_active_threshold(deps), QueryMsg::Config {} => query_config(deps), QueryMsg::Dao {} => query_dao(deps), + QueryMsg::Info {} => query_info(deps), + QueryMsg::IsActive {} => query_is_active(deps, env), QueryMsg::NftClaims { address } => query_nft_claims(deps, address), QueryMsg::Hooks {} => query_hooks(deps), - QueryMsg::VotingPowerAtHeight { address, height } => { - query_voting_power_at_height(deps, env, address, height) - } - QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height), - QueryMsg::Info {} => query_info(deps), QueryMsg::StakedNfts { address, start_after, limit, } => query_staked_nfts(deps, address, start_after, limit), + QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height), + QueryMsg::VotingPowerAtHeight { address, height } => { + query_voting_power_at_height(deps, env, address, height) + } + } +} + +pub fn query_active_threshold(deps: Deps) -> StdResult { + to_binary(&ActiveThresholdResponse { + active_threshold: ACTIVE_THRESHOLD.may_load(deps.storage)?, + }) +} + +pub fn query_is_active(deps: Deps, env: Env) -> StdResult { + let threshold = ACTIVE_THRESHOLD.may_load(deps.storage)?; + if let Some(threshold) = threshold { + let config = CONFIG.load(deps.storage)?; + let staked_nfts = TOTAL_STAKED_NFTS + .may_load_at_height(deps.storage, env.block.height)? + .unwrap_or_default(); + let total_nfts: NumTokensResponse = deps.querier.query_wasm_smart( + config.nft_address, + &cw721_base::msg::QueryMsg::::NumTokens {}, + )?; + + match threshold { + ActiveThreshold::AbsoluteCount { count } => to_binary(&IsActiveResponse { + active: staked_nfts >= count, + }), + ActiveThreshold::Percentage { percent } => { + // Check if there are any staked NFTs + if staked_nfts.is_zero() { + return to_binary(&IsActiveResponse { active: false }); + } + + // percent is bounded between [0, 100]. decimal + // represents percents in u128 terms as p * + // 10^15. this bounds percent between [0, 10^17]. + // + // total_potential_power is bounded between [0, 2^64] + // as it tracks the count of NFT tokens which has + // a max supply of 2^64. + // + // with our precision factor being 10^9: + // + // total_nfts <= 2^64 * 10^9 <= 2^256 + // + // so we're good to put that in a u256. + // + // multiply_ratio promotes to a u512 under the hood, + // so it won't overflow, multiplying by a percent less + // than 100 is gonna make something the same size or + // smaller, applied + 10^9 <= 2^128 * 10^9 + 10^9 <= + // 2^256, so the top of the round won't overflow, and + // rounding is rounding down, so the whole thing can + // be safely unwrapped at the end of the day thank you + // for coming to my ted talk. + let total_nfts_count = Uint128::from(total_nfts.count).full_mul(PRECISION_FACTOR); + + // under the hood decimals are `atomics / 10^decimal_places`. + // cosmwasm doesn't give us a Decimal * Uint256 + // implementation so we take the decimal apart and + // multiply by the fraction. + let applied = total_nfts_count.multiply_ratio( + percent.atomics(), + Uint256::from(10u64).pow(percent.decimal_places()), + ); + let rounded = (applied + Uint256::from(PRECISION_FACTOR) - Uint256::from(1u128)) + / Uint256::from(PRECISION_FACTOR); + let count: Uint128 = rounded.try_into().unwrap(); + + // staked_nfts >= total_nfts * percent + to_binary(&IsActiveResponse { + active: staked_nfts >= count, + }) + } + } + } else { + to_binary(&IsActiveResponse { active: true }) } } @@ -391,3 +580,69 @@ pub fn query_staked_nfts( }; to_binary(&range?) } + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { + match msg.id { + INSTANTIATE_NFT_CONTRACT_REPLY_ID => { + let res = parse_reply_instantiate_data(msg); + match res { + Ok(res) => { + let dao = DAO.load(deps.storage)?; + let nft_contract = res.contract_address; + + // Save NFT contract to config + let mut config = CONFIG.load(deps.storage)?; + config.nft_address = deps.api.addr_validate(&nft_contract)?; + CONFIG.save(deps.storage, &config)?; + + let initial_nfts = INITITIAL_NFTS.load(deps.storage)?; + + // Add mint submessages + let mint_submessages: Vec = initial_nfts + .iter() + .flat_map(|nft| -> Result { + Ok(SubMsg::new(WasmMsg::Execute { + contract_addr: nft_contract.clone(), + funds: vec![], + msg: to_binary( + &cw721_base::msg::ExecuteMsg::::Mint { + token_id: nft.token_id.clone(), + owner: nft.owner.clone(), + token_uri: nft.token_uri.clone(), + extension: Empty {}, + }, + )?, + })) + }) + .collect::>(); + + // Clear space + INITITIAL_NFTS.remove(deps.storage); + + // Update minter message + let update_minter_msg = WasmMsg::Execute { + contract_addr: nft_contract.clone(), + msg: to_binary( + &cw721_base::msg::ExecuteMsg::::UpdateOwnership( + cw721_base::Action::TransferOwnership { + new_owner: dao.to_string(), + expiry: None, + }, + ), + )?, + funds: vec![], + }; + + Ok(Response::default() + .add_attribute("method", "instantiate") + .add_attribute("nft_contract", nft_contract) + .add_message(update_minter_msg) + .add_submessages(mint_submessages)) + } + Err(_) => Err(ContractError::NftInstantiateError {}), + } + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/voting/dao-voting-cw721-staked/src/error.rs b/contracts/voting/dao-voting-cw721-staked/src/error.rs index d61728c5a..cf2050df3 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/error.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/error.rs @@ -6,26 +6,44 @@ pub enum ContractError { #[error(transparent)] Std(#[from] StdError), - #[error("Nothing to claim")] - NothingToClaim {}, + #[error("Can not stake that which has already been staked")] + AlreadyStaked {}, + + #[error(transparent)] + HookError(#[from] cw_controllers::HookError), + + #[error("Active threshold percentage must be greater than 0 and less than 1")] + InvalidActivePercentage {}, #[error("Invalid token. Got ({received}), expected ({expected})")] InvalidToken { received: Addr, expected: Addr }, + #[error("Error instantiating cw721-roles contract")] + NftInstantiateError {}, + + #[error("New cw721-roles contract must be instantiated with at least one NFT")] + NoInitialNfts {}, + + #[error("Nothing to claim")] + NothingToClaim {}, + #[error("Only the owner of this contract my execute this message")] NotOwner {}, #[error("Can not unstake that which you have not staked (unstaking {token_id})")] NotStaked { token_id: String }, - #[error("Can not stake that which has already been staked")] - AlreadyStaked {}, - #[error("Too many outstanding claims. Claim some tokens before unstaking more.")] TooManyClaims {}, - #[error(transparent)] - HookError(#[from] cw_controllers::HookError), + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Active threshold count must be greater than zero")] + ZeroActiveCount {}, #[error("Can't unstake zero NFTs.")] ZeroUnstake {}, diff --git a/contracts/voting/dao-voting-cw721-staked/src/msg.rs b/contracts/voting/dao-voting-cw721-staked/src/msg.rs index b0bd1073b..27cc298a9 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/msg.rs @@ -1,18 +1,59 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Empty; use cw721::Cw721ReceiveMsg; use cw_utils::Duration; use dao_interface::Admin; -use dao_macros::voting_module_query; +use dao_macros::{active_query, voting_module_query}; +use dao_voting::threshold::ActiveThreshold; + +#[cw_serde] +pub struct NftMintMsg { + /// Unique ID of the NFT + pub token_id: String, + /// The owner of the newly minter NFT + pub owner: String, + /// Universal resource identifier for this NFT + /// Should point to a JSON file that conforms to the ERC721 + /// Metadata JSON Schema + pub token_uri: Option, + /// Any custom extension used by this contract + pub extension: Empty, +} + +#[cw_serde] +#[allow(clippy::large_enum_variant)] +pub enum NftContract { + Existing { + /// Address of an already instantiated cw721 token contract. + address: String, + }, + New { + /// Code ID for cw721 token contract. + code_id: u64, + /// Label to use for instantiated cw721 contract. + label: String, + /// NFT collection name + name: String, + /// NFT collection symbol + symbol: String, + /// Initial NFTs to mint when creating the NFT contract. + /// If empty, an error is thrown. + initial_nfts: Vec, + }, +} #[cw_serde] pub struct InstantiateMsg { /// May change unstaking duration and add hooks. pub owner: Option, /// Address of the cw721 NFT contract that may be staked. - pub nft_address: String, + pub nft_contract: NftContract, /// Amount of time between unstaking and tokens being /// avaliable. To unstake with no delay, leave as `None`. pub unstaking_duration: Option, + /// The number or percentage of tokens that must be staked + /// for the DAO to be active + pub active_threshold: Option, } #[cw_serde] @@ -38,8 +79,15 @@ pub enum ExecuteMsg { RemoveHook { addr: String, }, + /// Sets the active threshold to a new value. Only the + /// instantiator this contract (a DAO most likely) may call this + /// method. + UpdateActiveThreshold { + new_threshold: Option, + }, } +#[active_query] #[voting_module_query] #[cw_serde] #[derive(QueryResponses)] @@ -57,4 +105,14 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + #[returns(ActiveThresholdResponse)] + ActiveThreshold {}, } + +#[cw_serde] +pub struct ActiveThresholdResponse { + pub active_threshold: Option, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/contracts/voting/dao-voting-cw721-staked/src/state.rs b/contracts/voting/dao-voting-cw721-staked/src/state.rs index f2a932bcb..486285af2 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/state.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/state.rs @@ -4,8 +4,9 @@ use cw721_controllers::NftClaims; use cw_controllers::Hooks; use cw_storage_plus::{Item, Map, SnapshotItem, SnapshotMap, Strategy}; use cw_utils::Duration; +use dao_voting::threshold::ActiveThreshold; -use crate::ContractError; +use crate::{msg::NftMintMsg, ContractError}; #[cw_serde] pub struct Config { @@ -14,9 +15,13 @@ pub struct Config { pub unstaking_duration: Option, } +pub const ACTIVE_THRESHOLD: Item = Item::new("active_threshold"); pub const CONFIG: Item = Item::new("config"); pub const DAO: Item = Item::new("dao"); +// Holds initial NFTs messages during instantiation. +pub const INITITIAL_NFTS: Item> = Item::new("initial_nfts"); + /// The set of NFTs currently staked by each address. The existence of /// an `(address, token_id)` pair implies that `address` has staked /// `token_id`. diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/execute.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/execute.rs index d4dbf388c..de31d35e8 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/execute.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/execute.rs @@ -1,6 +1,5 @@ use cosmwasm_std::{Addr, Binary, Empty}; use cw721::Cw721ExecuteMsg; -use cw721_base::MintMsg; use cw_multi_test::{App, AppResponse, Executor}; use anyhow::Result as AnyResult; @@ -45,12 +44,12 @@ pub fn mint_nft( app.execute_contract( addr!(sender), cw721.clone(), - &cw721_base::ExecuteMsg::Mint::(MintMsg { + &cw721_base::ExecuteMsg::Mint:: { token_id: token_id.to_string(), owner: receiver.to_string(), token_uri: None, extension: Empty::default(), - }), + }, &[], ) } 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 bbb9907b5..be9c6ba83 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs @@ -11,7 +11,7 @@ use cw_utils::Duration; use dao_interface::Admin; use dao_testing::contracts::voting_cw721_staked_contract; -use crate::msg::InstantiateMsg; +use crate::msg::{InstantiateMsg, NftContract}; use self::instantiate::instantiate_cw721_base; @@ -35,8 +35,11 @@ pub(crate) fn setup_test(owner: Option, unstaking_duration: Option anyhow::Result<()> { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + let module_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }], + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + let config = query_config(&app, &module_addr)?; + let cw721_addr = config.nft_address; + + // Check that the NFT contract was created + let owner = query_nft_owner(&app, &cw721_addr, "1")?; + assert_eq!(owner.owner, CREATOR_ADDR); + + Ok(()) +} + // I can stake tokens, voting power and total power is updated one // block later. #[test] @@ -362,7 +411,9 @@ fn test_info_query_works() -> anyhow::Result<()> { #[test] fn test_add_remove_hooks() -> anyhow::Result<()> { let CommonTest { - mut app, module, .. + mut app, + module, + nft, } = setup_test( Some(Admin::Address { addr: CREATOR_ADDR.to_string(), @@ -373,11 +424,19 @@ fn test_add_remove_hooks() -> anyhow::Result<()> { add_hook(&mut app, &module, CREATOR_ADDR, "meow")?; remove_hook(&mut app, &module, CREATOR_ADDR, "meow")?; + // Minting NFT works if no hooks + mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1").unwrap(); + + // Add a hook to a fake contract called "meow" add_hook(&mut app, &module, CREATOR_ADDR, "meow")?; let hooks = query_hooks(&app, &module)?; assert_eq!(hooks.hooks, vec!["meow".to_string()]); + // Minting / staking now doesn't work because meow isn't a contract + // This failure means the hook is working + mint_and_stake_nft(&mut app, &nft, &module, CREATOR_ADDR, "1").unwrap_err(); + let res = add_hook(&mut app, &module, CREATOR_ADDR, "meow"); is_error!(res => "Given address already registered as a hook"); @@ -389,3 +448,453 @@ fn test_add_remove_hooks() -> anyhow::Result<()> { Ok(()) } + +#[test] +#[should_panic(expected = "Active threshold count must be greater than zero")] +fn test_instantiate_zero_active_threshold_count() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::zero(), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +fn test_active_threshold_absolute_count() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let voting_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![ + NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }, + NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "2".to_string(), + extension: Empty {}, + }, + NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "3".to_string(), + extension: Empty {}, + }, + ], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(3), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + // Get NFT contract address + let nft_addr = query_config(&app, &voting_addr).unwrap().nft_address; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake NFTs + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "1").unwrap(); + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "2").unwrap(); + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "3").unwrap(); + + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let voting_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(20), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + // Get NFT contract address + let nft_addr = query_config(&app, &voting_addr).unwrap().nft_address; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake NFTs + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "1").unwrap(); + app.update_block(next_block); + + // Active as enough staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_active_threshold_percent_rounds_up() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let voting_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![ + NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }, + NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "2".to_string(), + extension: Empty {}, + }, + NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "3".to_string(), + extension: Empty {}, + }, + NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "4".to_string(), + extension: Empty {}, + }, + NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "5".to_string(), + extension: Empty {}, + }, + ], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(50), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + // Get NFT contract address + let nft_addr = query_config(&app, &voting_addr).unwrap().nft_address; + + // Not active as none staked + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + assert!(!is_active.active); + + // Stake 2 token as creator, should not be active. + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "1").unwrap(); + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "2").unwrap(); + + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::IsActive {}) + .unwrap(); + println!("{:?}", is_active); + assert!(!is_active.active); + + // Stake 1 more token as creator, should now be active. + stake_nft(&mut app, &nft_addr, &voting_addr, CREATOR_ADDR, "3").unwrap(); + app.update_block(next_block); + + let is_active: IsActiveResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::IsActive {}) + .unwrap(); + assert!(is_active.active); +} + +#[test] +fn test_update_active_threshold() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + let voting_addr = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }], + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(voting_addr.clone(), &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!(resp.active_threshold, None); + + let msg = ExecuteMsg::UpdateActiveThreshold { + new_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100), + }), + }; + + // Expect failure as sender is not the DAO + app.execute_contract(Addr::unchecked("bob"), voting_addr.clone(), &msg, &[]) + .unwrap_err(); + + // Expect success as sender is the DAO (in this case the creator) + app.execute_contract( + Addr::unchecked(CREATOR_ADDR), + voting_addr.clone(), + &msg, + &[], + ) + .unwrap(); + + let resp: ActiveThresholdResponse = app + .wrap() + .query_wasm_smart(voting_addr, &QueryMsg::ActiveThreshold {}) + .unwrap(); + assert_eq!( + resp.active_threshold, + Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(100) + }) + ); +} + +#[test] +#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +fn test_active_threshold_percentage_gt_100() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(120), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +fn test_active_threshold_percentage_lte_0() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![NftMintMsg { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(0), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +fn test_no_initial_nfts_fails() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); + + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + owner: Some(Admin::Address { + addr: CREATOR_ADDR.to_string(), + }), + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + initial_nfts: vec![], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(0), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); +} diff --git a/justfile b/justfile index bc0779bb5..2fa8cbcfb 100644 --- a/justfile +++ b/justfile @@ -56,11 +56,11 @@ workspace-optimize: --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --platform linux/amd64 \ - cosmwasm/workspace-optimizer:0.12.11 + cosmwasm/workspace-optimizer:0.12.13 workspace-optimize-arm: docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --platform linux/arm64 \ - cosmwasm/workspace-optimizer-arm64:0.12.11 + cosmwasm/workspace-optimizer-arm64:0.12.13 diff --git a/packages/cw-hooks/src/lib.rs b/packages/cw-hooks/src/lib.rs index 14bff3082..334b88e44 100644 --- a/packages/cw-hooks/src/lib.rs +++ b/packages/cw-hooks/src/lib.rs @@ -33,7 +33,7 @@ impl<'a> Hooks<'a> { pub fn add_hook(&self, storage: &mut dyn Storage, addr: Addr) -> Result<(), HookError> { let mut hooks = self.0.may_load(storage)?.unwrap_or_default(); - if !hooks.iter().any(|h| h == &addr) { + if !hooks.iter().any(|h| h == addr) { hooks.push(addr); } else { return Err(HookError::HookAlreadyRegistered {}); @@ -43,7 +43,7 @@ impl<'a> Hooks<'a> { pub fn remove_hook(&self, storage: &mut dyn Storage, addr: Addr) -> Result<(), HookError> { let mut hooks = self.0.load(storage)?; - if let Some(p) = hooks.iter().position(|x| x == &addr) { + if let Some(p) = hooks.iter().position(|h| h == addr) { hooks.remove(p); } else { return Err(HookError::HookNotRegistered {}); diff --git a/packages/dao-cw721-extensions/Cargo.toml b/packages/dao-cw721-extensions/Cargo.toml new file mode 100644 index 000000000..609e89d0d --- /dev/null +++ b/packages/dao-cw721-extensions/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "dao-cw721-extensions" +authors = ["Jake Hartnell"] +description = "A package for DAO cw721 extensions." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-controllers = { workspace = true } +cw4 = { workspace = true } diff --git a/packages/dao-cw721-extensions/README.md b/packages/dao-cw721-extensions/README.md new file mode 100644 index 000000000..4fae07714 --- /dev/null +++ b/packages/dao-cw721-extensions/README.md @@ -0,0 +1,3 @@ +# DAO CW721 Extensions: extensions for DAO related NFT contracts + +This implements extensions for cw721 NFT contracts integrating with DAO DAO (for example `cw721-roles`). diff --git a/packages/dao-cw721-extensions/src/lib.rs b/packages/dao-cw721-extensions/src/lib.rs new file mode 100644 index 000000000..22bacf03c --- /dev/null +++ b/packages/dao-cw721-extensions/src/lib.rs @@ -0,0 +1,3 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod roles; diff --git a/packages/dao-cw721-extensions/src/roles.rs b/packages/dao-cw721-extensions/src/roles.rs new file mode 100644 index 000000000..0f2c9166a --- /dev/null +++ b/packages/dao-cw721-extensions/src/roles.rs @@ -0,0 +1,56 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::CustomMsg; + +#[cw_serde] +pub struct MetadataExt { + /// Optional on-chain role for this member, can be used by other contracts to enforce permissions + pub role: Option, + /// The voting weight of this role + pub weight: u64, +} + +#[cw_serde] +pub enum ExecuteExt { + /// Add a new hook to be informed of all membership changes. + /// Must be called by Admin + AddHook { addr: String }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: String }, + /// Update the token_uri for a particular NFT. Must be called by minter / admin + UpdateTokenUri { + token_id: String, + token_uri: Option, + }, + /// Updates the voting weight of a token. Must be called by minter / admin + UpdateTokenWeight { token_id: String, weight: u64 }, + /// Udates the role of a token. Must be called by minter / admin + UpdateTokenRole { + token_id: String, + role: Option, + }, +} +impl CustomMsg for ExecuteExt {} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryExt { + /// Total weight at a given height + #[returns(cw4::TotalWeightResponse)] + TotalWeight { at_height: Option }, + /// Returns a list of Members + #[returns(cw4::MemberListResponse)] + ListMembers { + start_after: Option, + limit: Option, + }, + /// Returns the weight of a certain member + #[returns(cw4::MemberResponse)] + Member { + addr: String, + at_height: Option, + }, + /// Shows all registered hooks. + #[returns(cw_controllers::HooksResponse)] + Hooks {}, +} +impl CustomMsg for QueryExt {} diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index 49793d160..b65be2569 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -31,6 +31,7 @@ cw-proposal-single-v1 = { workspace = true } cw-vesting = { workspace = true } cw20-stake = { workspace = true } cw721-base = { workspace = true } +cw721-roles = { workspace = true } dao-core = { workspace = true, features = ["library"] } dao-interface = { workspace = true } dao-pre-propose-multiple = { workspace = true } @@ -42,6 +43,7 @@ dao-voting-cw20-balance = { workspace = true } dao-voting-cw20-staked = { workspace = true } dao-voting-cw4 = { workspace = true } dao-voting-cw721-staked = { workspace = true } +dao-voting-cw721-roles = { workspace = true } dao-voting-native-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 c6df42b17..0173e444a 100644 --- a/packages/dao-testing/src/contracts.rs +++ b/packages/dao-testing/src/contracts.rs @@ -31,6 +31,15 @@ pub fn cw721_base_contract() -> Box> { Box::new(contract) } +pub fn cw721_roles_contract() -> Box> { + let contract = ContractWrapper::new( + cw721_roles::contract::execute, + cw721_roles::contract::instantiate, + cw721_roles::contract::query, + ); + Box::new(contract) +} + pub fn cw20_stake_contract() -> Box> { let contract = ContractWrapper::new( cw20_stake::contract::execute, @@ -113,7 +122,8 @@ pub fn voting_cw721_staked_contract() -> Box> { dao_voting_cw721_staked::contract::execute, dao_voting_cw721_staked::contract::instantiate, dao_voting_cw721_staked::contract::query, - ); + ) + .with_reply(dao_voting_cw721_staked::contract::reply); Box::new(contract) } @@ -138,6 +148,16 @@ pub fn dao_voting_cw4_contract() -> Box> { Box::new(contract) } +pub fn dao_voting_cw721_roles_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_cw721_roles::contract::execute, + dao_voting_cw721_roles::contract::instantiate, + dao_voting_cw721_roles::contract::query, + ) + .with_reply(dao_voting_cw721_roles::contract::reply); + Box::new(contract) +} + pub fn v1_proposal_single_contract() -> Box> { let contract = ContractWrapper::new( cw_proposal_single_v1::contract::execute, diff --git a/packages/dao-testing/src/helpers.rs b/packages/dao-testing/src/helpers.rs index 798e9b5c0..346010e8e 100644 --- a/packages/dao-testing/src/helpers.rs +++ b/packages/dao-testing/src/helpers.rs @@ -1,11 +1,17 @@ -use cosmwasm_std::{to_binary, Addr, Binary, Empty, Uint128}; +use cosmwasm_std::{to_binary, Addr, Binary, Uint128}; use cw20::Cw20Coin; -use cw_multi_test::{App, Contract, ContractWrapper, Executor}; +use cw_multi_test::{App, Executor}; use cw_utils::Duration; use dao_interface::{Admin, ModuleInstantiateInfo}; -use dao_voting_cw20_staked::msg::ActiveThreshold; +use dao_voting::threshold::ActiveThreshold; use dao_voting_cw4::msg::GroupContract; +use crate::contracts::{ + cw20_balances_voting_contract, cw20_base_contract, cw20_stake_contract, + cw20_staked_balances_voting_contract, cw4_group_contract, dao_core_contract, + dao_voting_cw4_contract, +}; + const CREATOR_ADDR: &str = "creator"; pub fn instantiate_with_cw20_balances_governance( @@ -14,9 +20,9 @@ pub fn instantiate_with_cw20_balances_governance( governance_instantiate: Binary, initial_balances: Option>, ) -> Addr { - let cw20_id = app.store_code(cw20_contract()); - let core_id = app.store_code(cw_gov_contract()); - let votemod_id = app.store_code(cw20_balances_voting()); + let cw20_id = app.store_code(cw20_base_contract()); + let core_id = app.store_code(dao_core_contract()); + let votemod_id = app.store_code(cw20_balances_voting_contract()); let initial_balances = initial_balances.unwrap_or_else(|| { vec![Cw20Coin { @@ -115,10 +121,10 @@ pub fn instantiate_with_staked_balances_governance( .collect() }; - let cw20_id = app.store_code(cw20_contract()); - let cw20_stake_id = app.store_code(cw20_stake()); - let staked_balances_voting_id = app.store_code(staked_balances_voting()); - let core_contract_id = app.store_code(cw_gov_contract()); + let cw20_id = app.store_code(cw20_base_contract()); + let cw20_stake_id = app.store_code(cw20_stake_contract()); + let staked_balances_voting_id = app.store_code(cw20_staked_balances_voting_contract()); + let core_contract_id = app.store_code(dao_core_contract()); let instantiate_core = dao_core::msg::InstantiateMsg { dao_uri: None, @@ -218,10 +224,10 @@ pub fn instantiate_with_staking_active_threshold( initial_balances: Option>, active_threshold: Option, ) -> Addr { - let cw20_id = app.store_code(cw20_contract()); + let cw20_id = app.store_code(cw20_base_contract()); let cw20_staking_id = app.store_code(cw20_stake_contract()); - let governance_id = app.store_code(cw_gov_contract()); - let votemod_id = app.store_code(cw20_staked_balances_voting()); + let governance_id = app.store_code(dao_core_contract()); + let votemod_id = app.store_code(cw20_staked_balances_voting_contract()); let initial_balances = initial_balances.unwrap_or_else(|| { vec![ @@ -291,9 +297,9 @@ pub fn instantiate_with_cw4_groups_governance( proposal_module_instantiate: Binary, initial_weights: Option>, ) -> Addr { - let cw4_id = app.store_code(cw4_contract()); - let core_id = app.store_code(cw_gov_contract()); - let votemod_id = app.store_code(cw4_voting_contract()); + let cw4_id = app.store_code(cw4_group_contract()); + let core_id = app.store_code(dao_core_contract()); + let votemod_id = app.store_code(dao_voting_cw4_contract()); let initial_weights = initial_weights.unwrap_or_default(); @@ -362,89 +368,3 @@ pub fn instantiate_with_cw4_groups_governance( addr } - -pub fn cw20_contract() -> Box> { - let contract = ContractWrapper::new( - cw20_base::contract::execute, - cw20_base::contract::instantiate, - cw20_base::contract::query, - ); - Box::new(contract) -} - -pub fn cw20_stake_contract() -> Box> { - let contract = ContractWrapper::new( - cw20_stake::contract::execute, - cw20_stake::contract::instantiate, - cw20_stake::contract::query, - ); - Box::new(contract) -} - -pub fn cw20_balances_voting() -> Box> { - let contract = ContractWrapper::new( - dao_voting_cw20_balance::contract::execute, - dao_voting_cw20_balance::contract::instantiate, - dao_voting_cw20_balance::contract::query, - ) - .with_reply(dao_voting_cw20_balance::contract::reply); - Box::new(contract) -} - -fn cw20_staked_balances_voting() -> Box> { - let contract = ContractWrapper::new( - dao_voting_cw20_staked::contract::execute, - dao_voting_cw20_staked::contract::instantiate, - dao_voting_cw20_staked::contract::query, - ) - .with_reply(dao_voting_cw20_staked::contract::reply); - Box::new(contract) -} - -fn cw_gov_contract() -> Box> { - let contract = ContractWrapper::new( - dao_core::contract::execute, - dao_core::contract::instantiate, - dao_core::contract::query, - ) - .with_reply(dao_core::contract::reply); - Box::new(contract) -} - -fn staked_balances_voting() -> Box> { - let contract = ContractWrapper::new( - dao_voting_cw20_staked::contract::execute, - dao_voting_cw20_staked::contract::instantiate, - dao_voting_cw20_staked::contract::query, - ) - .with_reply(dao_voting_cw20_staked::contract::reply); - Box::new(contract) -} - -fn cw20_stake() -> Box> { - let contract = ContractWrapper::new( - cw20_stake::contract::execute, - cw20_stake::contract::instantiate, - cw20_stake::contract::query, - ); - Box::new(contract) -} - -fn cw4_contract() -> Box> { - let contract = ContractWrapper::new( - cw4_group::contract::execute, - cw4_group::contract::instantiate, - cw4_group::contract::query, - ); - Box::new(contract) -} - -fn cw4_voting_contract() -> Box> { - let contract = ContractWrapper::new( - dao_voting_cw4::contract::execute, - dao_voting_cw4::contract::instantiate, - dao_voting_cw4::contract::query, - ) - .with_reply(dao_voting_cw4::contract::reply); - Box::new(contract) -} diff --git a/packages/dao-voting/src/threshold.rs b/packages/dao-voting/src/threshold.rs index bdbb0bc5b..40fd779af 100644 --- a/packages/dao-voting/src/threshold.rs +++ b/packages/dao-voting/src/threshold.rs @@ -3,6 +3,21 @@ use cosmwasm_std::{Decimal, Uint128}; use thiserror::Error; +/// The threshold of tokens that must be staked in order for this +/// voting module to be active. If this is not reached, this module +/// will response to `is_active` queries with false and proposal +/// modules which respect active thresholds will not allow the +/// creation of proposals. +#[cw_serde] +pub enum ActiveThreshold { + /// The absolute number of tokens that must be staked for the + /// module to be active. + AbsoluteCount { count: Uint128 }, + /// The percentage of tokens that must be staked for the module to + /// be active. Computed as `staked / total_supply`. + Percentage { percent: Decimal }, +} + #[derive(Error, Debug, PartialEq, Eq)] pub enum ThresholdError { #[error("Required threshold cannot be zero")]