diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index fe568df6f..fca7b1d82 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -25,16 +25,6 @@ jobs: target: wasm32-unknown-unknown override: true - - name: Clone libwasmv (needed for test-tube) - uses: actions/checkout@v2 - with: - repository: CosmWasm/wasmvm - path: ./wasmvm - ref: v1.3.0 - - - name: Install libwasmv - run: cd ./wasmvm && make build-rust && cd ../ - - name: Rust Dependencies Cache uses: actions/cache@v3 with: @@ -70,9 +60,6 @@ jobs: - name: Run Integration Tests run: just integration-test - - name: Run Test Tube Integration Tests - run: just test-tube - - name: Combine Test Gas Reports run: cd ci/integration-tests/ && jq -rs 'reduce .[] as $item ({}; . * $item)' gas_reports/*.json > gas_report.json diff --git a/.github/workflows/test_tube.yml b/.github/workflows/test_tube.yml new file mode 100644 index 000000000..ca15a4726 --- /dev/null +++ b/.github/workflows/test_tube.yml @@ -0,0 +1,68 @@ +name: Test Tube Tests + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Integration tests + runs-on: ubuntu-latest + env: + GAS_OUT_DIR: gas_reports + GAS_LIMIT: 100000000 + steps: + - name: Checkout sources + uses: actions/checkout@v3 + + - name: Install latest nightly toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2023-02-02 + target: wasm32-unknown-unknown + override: true + + - name: Clone libwasmv (needed for test-tube) + uses: actions/checkout@v2 + with: + repository: CosmWasm/wasmvm + path: ./wasmvm + ref: v1.3.0 + + - name: Install libwasmv + run: cd ./wasmvm && make build-rust && cd ../ + + - name: Rust Dependencies Cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + artifacts/ + key: ${{ runner.os }}-cargo-with-artifacts-${{ hashFiles('**/Cargo.lock') }} + + - name: Set latest just version + run: echo "JUST_VERSION=$(cargo search just -q | sed -n -e '/^just[[:space:]]/p' | cut -d '"' -f 2)" >> $GITHUB_ENV + + - name: Get cached just + uses: actions/cache@v3 + with: + path: ~/.cargo/bin/just + key: ${{ runner.os }}-just-${{ env.JUST_VERSION }} + + - name: Install just + run: cargo install just || true + + - name: Run download deps + run: just download-deps + + - name: Run workspace optimize + run: just workspace-optimize + + - name: Run Test Tube Integration Tests + run: just test-tube diff --git a/Cargo.lock b/Cargo.lock index b81b4604b..b76510e82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -79,7 +79,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -179,9 +179,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "base64ct" @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" @@ -345,9 +345,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "num-traits", ] @@ -589,7 +589,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a44d3f9c25b2f864737c6605a98f2e4675d53fd8bbc7cf4d7c02475661a793d" dependencies = [ - "base64 0.21.3", + "base64 0.21.4", "bnum", "cosmwasm-crypto", "cosmwasm-derive", @@ -858,19 +858,20 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.16.5" -source = "git+https://github.com/CosmWasm/cw-multi-test.git?rev=d38db7752b9f054c395d6108453f8b321e4cab02#d38db7752b9f054c395d6108453f8b321e4cab02" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d818f5323c80ed4890db7f89d65eda3f0261fe21878e628c27ea2d8de4b7ba4" dependencies = [ "anyhow", "cosmwasm-std", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "derivative", - "itertools", - "k256 0.11.6", - "prost 0.9.0", + "itertools 0.11.0", + "prost 0.12.1", "schemars", "serde", + "sha2 0.10.7", "thiserror", ] @@ -1670,6 +1671,7 @@ dependencies = [ "cw2 1.1.0", "cw20 1.1.0", "cw721 0.18.0", + "osmosis-std", ] [[package]] @@ -1963,6 +1965,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-test-custom-factory" +version = "2.2.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 1.1.0", + "cw-tokenfactory-issuer", + "cw-utils 1.0.1", + "cw2 1.1.0", + "cw721 0.18.0", + "cw721-base 0.18.0", + "dao-dao-macros", + "dao-interface", + "dao-voting 2.2.0", + "thiserror", +] + [[package]] name = "dao-testing" version = "2.2.0" @@ -1990,6 +2013,7 @@ dependencies = [ "dao-pre-propose-single", "dao-proposal-condorcet", "dao-proposal-single", + "dao-test-custom-factory", "dao-voting 0.1.0", "dao-voting 2.2.0", "dao-voting-cw20-balance", @@ -2134,12 +2158,14 @@ dependencies = [ "dao-dao-macros", "dao-hooks", "dao-interface", + "dao-proposal-hook-counter", + "dao-proposal-single", + "dao-test-custom-factory", "dao-testing", "dao-voting 2.2.0", - "sg-multi-test", - "sg-std", - "sg721", - "sg721-base", + "osmosis-std", + "osmosis-test-tube", + "serde", "thiserror", ] @@ -2165,6 +2191,7 @@ dependencies = [ "dao-interface", "dao-proposal-hook-counter", "dao-proposal-single", + "dao-test-custom-factory", "dao-testing", "dao-voting 2.2.0", "osmosis-std", @@ -2523,7 +2550,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -2654,7 +2681,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.21.3", + "base64 0.21.4", "bytes", "headers-core", "http", @@ -2895,6 +2922,7 @@ dependencies = [ "dao-interface", "dao-pre-propose-single", "dao-proposal-single", + "dao-test-custom-factory", "dao-voting 2.2.0", "dao-voting-cw20-staked", "dao-voting-cw721-staked", @@ -2926,6 +2954,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -3002,9 +3039,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libloading" @@ -3024,9 +3061,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "log" @@ -3192,7 +3229,7 @@ version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f47f0b2f22adb341bb59e5a3a1b464dde033181954bd055b9ae86d6511ba465b" dependencies = [ - "itertools", + "itertools 0.10.5", "proc-macro2", "prost-types", "quote", @@ -3308,7 +3345,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -3339,7 +3376,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -3382,9 +3419,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -3403,32 +3440,32 @@ dependencies = [ [[package]] name = "prost" -version = "0.9.0" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ "bytes", - "prost-derive 0.9.0", + "prost-derive 0.11.9", ] [[package]] name = "prost" -version = "0.11.9" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +checksum = "f4fdd22f3b9c31b53c060df4a0613a1c7f062d4115a2b984dd15b1858f7e340d" dependencies = [ "bytes", - "prost-derive 0.11.9", + "prost-derive 0.12.1", ] [[package]] name = "prost-derive" -version = "0.9.0" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 1.0.109", @@ -3436,15 +3473,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.11.9" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.34", ] [[package]] @@ -3621,9 +3658,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.11" +version = "0.38.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" dependencies = [ "bitflags 2.4.0", "errno", @@ -3822,7 +3859,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -3838,9 +3875,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -3855,7 +3892,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -3871,71 +3908,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sg-multi-test" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20744734b8049c64747bfb083bbc06a3c7204d1d34881ed3d89698e182aa9f97" -dependencies = [ - "anyhow", - "cosmwasm-std", - "cw-multi-test", - "schemars", - "serde", - "sg-std", -] - -[[package]] -name = "sg-std" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4db53aebc2b4f981dc20a51213544adde8beaace6880345627f4babe2e1bc3ab" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-utils 1.0.1", - "cw721 0.18.0", - "schemars", - "serde", - "thiserror", -] - -[[package]] -name = "sg721" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e7d8f93b519c4c95973a68c7abee2de838497974d666dddb4dabd04d9c7cbf6" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-ownable", - "cw-utils 1.0.1", - "cw721-base 0.18.0", - "serde", - "thiserror", -] - -[[package]] -name = "sg721-base" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af08801d6f50cb13be05a3d2e815fbdb9dbba82086bbab877599ed4d422e9441" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-ownable", - "cw-storage-plus 1.1.0", - "cw-utils 1.0.1", - "cw2 1.1.0", - "cw721 0.18.0", - "cw721-base 0.18.0", - "serde", - "sg-std", - "sg721", - "thiserror", - "url", -] - [[package]] name = "sha1" version = "0.10.5" @@ -4028,9 +4000,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys", @@ -4154,9 +4126,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.31" +version = "2.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" +checksum = "88ec6cdb6a4c16306eccf52ccd8d492e4ab64705a15a5016acb205251001bf72" dependencies = [ "proc-macro2", "quote", @@ -4449,7 +4421,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -4508,7 +4480,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.3", + "socket2 0.5.4", "tokio-macros", "windows-sys", ] @@ -4531,7 +4503,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -4619,7 +4591,7 @@ checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" dependencies = [ "async-trait", "axum", - "base64 0.21.3", + "base64 0.21.4", "bytes", "futures-core", "futures-util", @@ -4691,7 +4663,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] [[package]] @@ -4721,9 +4693,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ucd-trie" @@ -4739,9 +4711,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -4845,7 +4817,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", "wasm-bindgen-shared", ] @@ -4867,7 +4839,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5054,5 +5026,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.31", + "syn 2.0.34", ] diff --git a/Cargo.toml b/Cargo.toml index 0d55ba587..d9456cd60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,9 @@ members = [ "contracts/proposal/*", "contracts/pre-propose/*", "contracts/staking/*", + "contracts/test/*", "contracts/voting/*", "packages/*", - "test-contracts/*", "ci/*", ] exclude = ["ci/configs/", "wasmvm/libwasmvm"] @@ -41,7 +41,7 @@ cosmwasm-schema = { version = "1.2" } cosmwasm-std = { version = "1.2", features = ["ibc3"] } cosmwasm-storage = { version = "1.2" } cw-controllers = "1.1" -cw-multi-test = "0.16" +cw-multi-test = "0.17" cw-storage-plus = { version = "1.1" } cw-utils = "1.0" cw2 = "1.1" @@ -103,11 +103,12 @@ dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-singl dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.2.0" } dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.2.0" } dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.2.0" } -dao-proposal-sudo = { path = "./test-contracts/dao-proposal-sudo", version = "2.2.0" } -dao-proposal-hook-counter = { path = "./test-contracts/dao-proposal-hook-counter", version = "2.2.0" } +dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.2.0" } +dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.2.0" } +dao-test-custom-factory = { path = "./contracts/test/dao-test-custom-factory", version = "*" } dao-testing = { path = "./packages/dao-testing", version = "2.2.0" } dao-voting = { path = "./packages/dao-voting", version = "2.2.0" } -dao-voting-cw20-balance = { path = "./test-contracts/dao-voting-cw20-balance", version = "2.2.0" } +dao-voting-cw20-balance = { path = "./contracts/test/dao-voting-cw20-balance", version = "2.2.0" } dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", version = "2.2.0" } dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.2.0" } dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "*" } @@ -125,7 +126,3 @@ cw20-staked-balance-voting-v1 = { package = "cw20-staked-balance-voting", versio cw4-voting-v1 = { package = "cw4-voting", version = "0.1.0" } voting-v1 = { package = "dao-voting", version = "0.1.0" } stake-cw20-v03 = { package = "stake-cw20", version = "0.2.6" } - -# TODO remove when new release is tagged upstream -[patch.crates-io] -cw-multi-test = { git = "https://github.com/CosmWasm/cw-multi-test.git", rev = "d38db7752b9f054c395d6108453f8b321e4cab02" } diff --git a/ci/bootstrap-env/src/main.rs b/ci/bootstrap-env/src/main.rs index b8bdeb0a5..85fd119a5 100644 --- a/ci/bootstrap-env/src/main.rs +++ b/ci/bootstrap-env/src/main.rs @@ -73,6 +73,7 @@ fn main() -> Result<()> { }, active_threshold: None, })?, + funds: vec![], admin: Some(Admin::CoreModule {}), label: "DAO DAO Voting Module".to_string(), }, @@ -101,12 +102,14 @@ fn main() -> Result<()> { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Pre-Propose Module".to_string(), }, }, close_proposal_on_execution_failure: false, })?, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Proposal Module".to_string(), }], initial_items: None, diff --git a/ci/integration-tests/Cargo.toml b/ci/integration-tests/Cargo.toml index 3d9e217b8..6291ec502 100644 --- a/ci/integration-tests/Cargo.toml +++ b/ci/integration-tests/Cargo.toml @@ -25,6 +25,7 @@ dao-dao-core = { workspace = true } dao-interface = { workspace = true } dao-pre-propose-single = { workspace = true } dao-proposal-single = { workspace = true } +dao-test-custom-factory = { workspace = true } dao-voting = { workspace = true } dao-voting-cw20-staked = { workspace = true } dao-voting-cw721-staked = { workspace = true } diff --git a/ci/integration-tests/src/helpers/helper.rs b/ci/integration-tests/src/helpers/helper.rs index 4d30c1670..bcfd0e26d 100644 --- a/ci/integration-tests/src/helpers/helper.rs +++ b/ci/integration-tests/src/helpers/helper.rs @@ -59,6 +59,7 @@ pub fn create_dao( })?, admin: Some(Admin::CoreModule {}), label: "DAO DAO Voting Module".to_string(), + funds: vec![], }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: chain.orc.contract_map.code_id("dao_proposal_single")?, @@ -86,11 +87,13 @@ pub fn create_dao( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Pre-Propose Module".to_string(), }, }, })?, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Proposal Module".to_string(), }], initial_items: None, diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index c2e9cf422..56e65cdab 100644 --- a/contracts/dao-dao-core/schema/dao-dao-core.json +++ b/contracts/dao-dao-core/schema/dao-dao-core.json @@ -125,6 +125,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "InitialItem": { "description": "Information about an item to be stored in the items list.", "type": "object", @@ -149,6 +164,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -170,6 +186,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -184,6 +207,10 @@ } }, "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" } } }, @@ -1159,6 +1186,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -1180,6 +1208,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2014,6 +2049,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "MigrateParams": { "type": "object", "required": [ @@ -2096,6 +2146,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -2117,6 +2168,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2209,6 +2267,10 @@ }, "additionalProperties": false }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, "V1CodeIds": { "type": "object", "required": [ diff --git a/contracts/dao-dao-core/src/contract.rs b/contracts/dao-dao-core/src/contract.rs index 97f3a7d3e..21e38784e 100644 --- a/contracts/dao-dao-core/src/contract.rs +++ b/contracts/dao-dao-core/src/contract.rs @@ -910,6 +910,7 @@ pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result>(); @@ -198,12 +201,14 @@ fn test_instantiate_with_submessage_failure() { code_id: cw20_id, msg: to_binary("bad").unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "I have a bad instantiate message".to_string(), }); governance_modules.push(ModuleInstantiateInfo { code_id: cw20_id, msg: to_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "Everybody knowing that goodness is good makes wickedness." @@ -222,6 +227,7 @@ makes wickedness." code_id: cw20_id, msg: to_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: governance_modules, @@ -252,12 +258,14 @@ fn test_update_config() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }], initial_items: None, @@ -349,12 +357,14 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { code_id: propmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: propmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -418,6 +428,7 @@ fn test_swap_governance(swaps: Vec<(u32, u32)>) { code_id: propmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: format!("governance module {n}"), }) .collect(); @@ -525,12 +536,14 @@ fn test_removed_modules_can_not_execute() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -566,6 +579,7 @@ fn test_removed_modules_can_not_execute() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "new governance module".to_string(), }]; @@ -597,6 +611,7 @@ fn test_removed_modules_can_not_execute() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "new governance module".to_string(), }]; let to_disable = vec![new_proposal_module.address.to_string()]; @@ -682,12 +697,14 @@ fn test_module_already_disabled() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -737,6 +754,7 @@ fn test_module_already_disabled() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], to_disable, @@ -781,12 +799,14 @@ fn test_swap_voting_module() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -833,6 +853,7 @@ fn test_swap_voting_module() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, }) @@ -882,12 +903,14 @@ fn test_permissions() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -914,6 +937,7 @@ fn test_permissions() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, }, @@ -981,12 +1005,14 @@ fn do_standard_instantiate(auto_add: bool, admin: Option) -> (Addr, App) code_id: voting_id, msg: to_binary(&voting_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -1613,12 +1639,14 @@ fn test_list_items() { code_id: voting_id, msg: to_binary(&voting_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -1745,12 +1773,14 @@ fn test_instantiate_with_items() { code_id: voting_id, msg: to_binary(&voting_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: Some(initial_items.clone()), @@ -2576,12 +2606,14 @@ fn test_migrate_from_compatible() { code_id: voting_id, msg: to_binary(&voting_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "governance module".to_string(), }], initial_items: None, @@ -2859,6 +2891,7 @@ fn test_module_prefixes() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ @@ -2866,18 +2899,21 @@ fn test_module_prefixes() { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "proposal module 1".to_string(), }, ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "proposal module 2".to_string(), }, ModuleInstantiateInfo { code_id: govmod_id, msg: to_binary(&govmod_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "proposal module 2".to_string(), }, ], diff --git a/contracts/external/cw-admin-factory/src/tests.rs b/contracts/external/cw-admin-factory/src/tests.rs index 226d1f1ef..27221cb31 100644 --- a/contracts/external/cw-admin-factory/src/tests.rs +++ b/contracts/external/cw-admin-factory/src/tests.rs @@ -84,6 +84,7 @@ pub fn test_set_admin() { code_id: cw20_code_id, msg: to_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ @@ -91,12 +92,14 @@ pub fn test_set_admin() { code_id: cw20_code_id, msg: to_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "prop module".to_string(), }, ModuleInstantiateInfo { code_id: cw20_code_id, msg: to_binary(&cw20_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "prop module 2".to_string(), }, ], diff --git a/contracts/external/cw-tokenfactory-issuer/src/contract.rs b/contracts/external/cw-tokenfactory-issuer/src/contract.rs index 8d550284e..2883087c6 100644 --- a/contracts/external/cw-tokenfactory-issuer/src/contract.rs +++ b/contracts/external/cw-tokenfactory-issuer/src/contract.rs @@ -91,7 +91,7 @@ pub fn execute( } => execute::burn(deps, env, info, amount, address), ExecuteMsg::Deny { address, status } => execute::deny(deps, env, info, address, status), ExecuteMsg::Allow { address, status } => execute::allow(deps, info, address, status), - ExecuteMsg::Freeze { status } => execute::freeze(deps, info, status), + ExecuteMsg::Freeze { status } => execute::freeze(deps, env, info, status), ExecuteMsg::ForceTransfer { amount, from_address, diff --git a/contracts/external/cw-tokenfactory-issuer/src/execute.rs b/contracts/external/cw-tokenfactory-issuer/src/execute.rs index c8ff45f4a..09a84f248 100644 --- a/contracts/external/cw-tokenfactory-issuer/src/execute.rs +++ b/contracts/external/cw-tokenfactory-issuer/src/execute.rs @@ -6,7 +6,7 @@ use osmosis_std::types::osmosis::tokenfactory::v1beta1::{ use token_bindings::TokenFactoryMsg; use crate::error::ContractError; -use crate::helpers::check_before_send_hook_features_enabled; +use crate::helpers::{check_before_send_hook_features_enabled, check_is_not_frozen}; use crate::state::{ BeforeSendHookInfo, ALLOWLIST, BEFORE_SEND_HOOK_INFO, BURNER_ALLOWANCES, DENOM, DENYLIST, IS_FROZEN, MINTER_ALLOWANCES, @@ -50,6 +50,9 @@ pub fn mint( // Get token denom from contract let denom = DENOM.load(deps.storage)?; + // Check token is not frozen, or if from or to address is on allowlist + check_is_not_frozen(deps.as_ref(), info.sender.as_str(), &to_address, &denom)?; + // Create tokenfactory MsgMint which mints coins to the contract address let mint_tokens_msg = TokenFactoryMsg::mint_contract_tokens( denom.clone(), @@ -325,9 +328,13 @@ pub fn set_minter( /// contract), or add an airdrop contract to the allowlist so users can claim /// their tokens (but not yet trade them). /// +/// This issuer contract itself is added to the allowlist when freezing, to allow +/// for minting of tokens (if minters with allowances are also on the allowlist). +/// /// Must be the contract owner to call this method. pub fn freeze( deps: DepsMut, + env: Env, info: MessageInfo, status: bool, ) -> Result, ContractError> { @@ -340,6 +347,14 @@ pub fn freeze( // NOTE: Does not check if new status is same as old status IS_FROZEN.save(deps.storage, &status)?; + // Add the issue contract itself to the Allowlist, or remove + // if unfreezing to save storage. + if status { + ALLOWLIST.save(deps.storage, &env.contract.address, &status)?; + } else { + ALLOWLIST.remove(deps.storage, &env.contract.address); + } + Ok(Response::new() .add_attribute("action", "freeze") .add_attribute("status", status.to_string())) diff --git a/contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs b/contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs index 4b68bce12..594a07f4c 100644 --- a/contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs +++ b/contracts/external/cw-tokenfactory-issuer/tests/cases/mint.rs @@ -1,6 +1,8 @@ use cosmwasm_std::Uint128; use cw_tokenfactory_issuer::{msg::AllowanceInfo, ContractError}; -use osmosis_test_tube::{osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest, Account}; +use osmosis_test_tube::{ + osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest, Account, RunnerError, +}; use crate::test_env::{ test_query_over_default_limit, test_query_within_default_limit, TestEnv, TokenfactoryIssuer, @@ -100,6 +102,69 @@ fn used_up_allowance_should_be_removed_from_storage() { ); } +#[test] +fn granular_minting_permissions_when_frozen() { + let env = TestEnv::default(); + let owner = &env.test_accs[0]; + let denom = env.cw_tokenfactory_issuer.query_denom().unwrap().denom; + + let minter = &env.test_accs[1]; + let minter_two = &env.test_accs[2]; + let mint_to = &env.test_accs[3]; + + // Owner sets before send hook to enable advanced features + env.cw_tokenfactory_issuer + .set_before_send_hook(env.cw_tokenfactory_issuer.contract_addr.clone(), owner) + .unwrap(); + + // Owner freezes contract + env.cw_tokenfactory_issuer.freeze(true, owner).unwrap(); + + // Owner grants minter a mint allowance + env.cw_tokenfactory_issuer + .set_minter(&minter.address(), 1000000, owner) + .unwrap(); + + // Owner grants minter_two a mint allowance + env.cw_tokenfactory_issuer + .set_minter(&minter_two.address(), 1000000, owner) + .unwrap(); + + // Minter can't mint when frozen + let err = env + .cw_tokenfactory_issuer + .mint(&mint_to.address(), 100, minter) + .unwrap_err(); + assert_eq!( + err, + RunnerError::ExecuteError { + msg: format!("failed to execute message; message index: 0: The contract is frozen for denom \"{}\". Addresses need to be added to the allowlist to enable transfers to or from an account.: execute wasm contract failed", denom) + } + ); + + // Own puts minter on allowlist + env.cw_tokenfactory_issuer + .allow(&minter.address(), true, owner) + .unwrap(); + + // Minter can mint + env.cw_tokenfactory_issuer + .mint(&mint_to.address(), 100, minter) + .unwrap(); + + // Minter-two can't mint because not allowed + let err = env + .cw_tokenfactory_issuer + .mint(&mint_to.address(), 100, minter_two) + .unwrap_err(); + assert_eq!( + err, + RunnerError::ExecuteError { + msg: format!("failed to execute message; message index: 0: The contract is frozen for denom \"{}\". Addresses need to be added to the allowlist to enable transfers to or from an account.: execute wasm contract failed", denom) + } + ); +} + #[test] fn mint_less_than_or_eq_allowance_should_pass_and_deduct_allowance() { let cases = vec![ diff --git a/contracts/external/dao-migrator/schema/dao-migrator.json b/contracts/external/dao-migrator/schema/dao-migrator.json index db060770c..ad401a0a8 100644 --- a/contracts/external/dao-migrator/schema/dao-migrator.json +++ b/contracts/external/dao-migrator/schema/dao-migrator.json @@ -110,6 +110,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" @@ -153,6 +168,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -174,6 +190,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -1175,6 +1198,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -1196,6 +1220,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2077,11 +2108,27 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -2103,6 +2150,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2157,6 +2211,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" } } }, diff --git a/contracts/external/dao-migrator/src/testing/setup.rs b/contracts/external/dao-migrator/src/testing/setup.rs index b9d93950e..8b24e7a40 100644 --- a/contracts/external/dao-migrator/src/testing/setup.rs +++ b/contracts/external/dao-migrator/src/testing/setup.rs @@ -314,6 +314,7 @@ pub fn execute_migration( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "migrator".to_string(), }], to_disable: vec![], diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs index 2127cebf5..cf6eaf5ca 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs @@ -71,6 +71,7 @@ fn get_default_proposal_module_instantiate( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, @@ -1200,6 +1201,7 @@ fn test_instantiate_with_zero_native_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, @@ -1263,6 +1265,7 @@ fn test_instantiate_with_zero_cw20_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs index 5aea7b2bc..353fd0c41 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs @@ -95,6 +95,7 @@ fn get_proposal_module_approval_single_instantiate( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module, needs supervision".to_string(), }, }, @@ -126,6 +127,7 @@ fn get_proposal_module_approver_instantiate( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "approver module".to_string(), }, }, diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs index 3d7352515..2de6ee79c 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs @@ -72,6 +72,7 @@ fn get_default_proposal_module_instantiate( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, @@ -1059,6 +1060,7 @@ fn test_instantiate_with_zero_native_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, @@ -1120,6 +1122,7 @@ fn test_instantiate_with_zero_cw20_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, diff --git a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs index 078ea2d4b..e4ab908f9 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs @@ -70,6 +70,7 @@ fn get_default_proposal_module_instantiate( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, @@ -995,6 +996,7 @@ fn test_instantiate_with_zero_native_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, @@ -1056,6 +1058,7 @@ fn test_instantiate_with_zero_cw20_deposit() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "baby's first pre-propose module".to_string(), }, }, diff --git a/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs b/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs index a9e7ac30a..9f9f945be 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/testing/suite.rs @@ -96,12 +96,14 @@ impl SuiteBuilder { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: condorcet_id, msg: to_binary(&self.instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "condorcet module".to_string(), }], initial_items: None, 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 8d67f6e3c..11c7ed910 100644 --- a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json +++ b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json @@ -110,6 +110,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" @@ -153,6 +168,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -174,6 +190,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -261,6 +284,10 @@ } ] }, + "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" + }, "VotingStrategy": { "description": "Determines how many choices may be selected.", "oneOf": [ @@ -1149,6 +1176,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -1170,6 +1198,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -1991,11 +2026,27 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -2017,6 +2068,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2071,6 +2129,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" } } }, diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs index 2d767ae5d..f6188a6f0 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/instantiate.rs @@ -39,6 +39,7 @@ fn get_pre_propose_info( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "pre_propose_contract".to_string(), }, } @@ -156,13 +157,15 @@ pub fn _instantiate_with_staked_cw721_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -273,13 +276,15 @@ pub fn _instantiate_with_native_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -387,13 +392,15 @@ pub fn instantiate_with_cw20_balances_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -471,13 +478,15 @@ pub fn instantiate_with_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -608,13 +617,15 @@ pub fn instantiate_with_multiple_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, dao_uri: None, @@ -721,12 +732,14 @@ pub fn instantiate_with_staking_active_threshold( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, msg: to_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -798,12 +811,14 @@ pub fn _instantiate_with_cw4_groups_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, msg: to_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs index eaf69fcc8..9f1749423 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs @@ -96,6 +96,7 @@ pub fn get_pre_propose_info( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "pre_propose_contract".to_string(), }, } 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 44da656fc..b98955db8 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -110,6 +110,21 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" @@ -153,6 +168,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -174,6 +190,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -1175,6 +1198,7 @@ "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -1196,6 +1220,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2077,11 +2108,27 @@ "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", "type": "string" }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, "ModuleInstantiateInfo": { "description": "Information needed to instantiate a module.", "type": "object", "required": [ "code_id", + "funds", "label", "msg" ], @@ -2103,6 +2150,13 @@ "format": "uint64", "minimum": 0.0 }, + "funds": { + "description": "Funds to be sent to the instantiated contract.", + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, "label": { "description": "Label for the instantiated contract.", "type": "string" @@ -2157,6 +2211,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" } } }, diff --git a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs index aafcb5abe..b9e9d74de 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/instantiate.rs @@ -41,6 +41,7 @@ pub(crate) fn get_pre_propose_info( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "pre_propose_contract".to_string(), }, } @@ -154,13 +155,15 @@ pub(crate) fn instantiate_with_staked_cw721_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, }; @@ -271,13 +274,15 @@ pub(crate) fn instantiate_with_native_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, }; @@ -390,13 +395,15 @@ pub(crate) fn instantiate_with_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&proposal_module_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, }; @@ -503,12 +510,14 @@ pub(crate) fn instantiate_with_staking_active_threshold( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, msg: to_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -580,12 +589,14 @@ pub(crate) fn instantiate_with_cw4_groups_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: proposal_module_code_id, msg: to_binary(&proposal_module_instantiate).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index a4e6f4b46..8634243dd 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -1626,13 +1626,15 @@ fn test_migrate_from_v1() { }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: v1_proposal_single_code, - label: "DAO DAO governance module.".to_string(), - admin: Some(Admin::CoreModule {}), msg: to_binary(&instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO governance module.".to_string(), }], initial_items: None, }; @@ -1739,6 +1741,7 @@ fn test_migrate_from_v1() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO pre-propose".to_string(), }, }, @@ -2409,6 +2412,7 @@ fn test_update_pre_propose_module() { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "new pre-propose module".to_string(), }, }, diff --git a/test-contracts/dao-proposal-hook-counter/.cargo/config b/contracts/test/dao-proposal-hook-counter/.cargo/config similarity index 100% rename from test-contracts/dao-proposal-hook-counter/.cargo/config rename to contracts/test/dao-proposal-hook-counter/.cargo/config diff --git a/test-contracts/dao-proposal-hook-counter/.gitignore b/contracts/test/dao-proposal-hook-counter/.gitignore similarity index 100% rename from test-contracts/dao-proposal-hook-counter/.gitignore rename to contracts/test/dao-proposal-hook-counter/.gitignore diff --git a/test-contracts/dao-proposal-hook-counter/Cargo.toml b/contracts/test/dao-proposal-hook-counter/Cargo.toml similarity index 100% rename from test-contracts/dao-proposal-hook-counter/Cargo.toml rename to contracts/test/dao-proposal-hook-counter/Cargo.toml diff --git a/test-contracts/dao-proposal-hook-counter/README.md b/contracts/test/dao-proposal-hook-counter/README.md similarity index 100% rename from test-contracts/dao-proposal-hook-counter/README.md rename to contracts/test/dao-proposal-hook-counter/README.md diff --git a/test-contracts/dao-proposal-hook-counter/examples/schema.rs b/contracts/test/dao-proposal-hook-counter/examples/schema.rs similarity index 100% rename from test-contracts/dao-proposal-hook-counter/examples/schema.rs rename to contracts/test/dao-proposal-hook-counter/examples/schema.rs diff --git a/test-contracts/dao-proposal-hook-counter/schema/execute_msg.json b/contracts/test/dao-proposal-hook-counter/schema/execute_msg.json similarity index 100% rename from test-contracts/dao-proposal-hook-counter/schema/execute_msg.json rename to contracts/test/dao-proposal-hook-counter/schema/execute_msg.json diff --git a/test-contracts/dao-proposal-hook-counter/schema/instantiate_msg.json b/contracts/test/dao-proposal-hook-counter/schema/instantiate_msg.json similarity index 100% rename from test-contracts/dao-proposal-hook-counter/schema/instantiate_msg.json rename to contracts/test/dao-proposal-hook-counter/schema/instantiate_msg.json diff --git a/test-contracts/dao-proposal-hook-counter/schema/query_msg.json b/contracts/test/dao-proposal-hook-counter/schema/query_msg.json similarity index 100% rename from test-contracts/dao-proposal-hook-counter/schema/query_msg.json rename to contracts/test/dao-proposal-hook-counter/schema/query_msg.json diff --git a/test-contracts/dao-proposal-hook-counter/src/contract.rs b/contracts/test/dao-proposal-hook-counter/src/contract.rs similarity index 93% rename from test-contracts/dao-proposal-hook-counter/src/contract.rs rename to contracts/test/dao-proposal-hook-counter/src/contract.rs index be3cddc8d..0f2a36fd3 100644 --- a/test-contracts/dao-proposal-hook-counter/src/contract.rs +++ b/contracts/test/dao-proposal-hook-counter/src/contract.rs @@ -66,12 +66,12 @@ pub fn execute_proposal_hook( match proposal_hook { ProposalHookMsg::NewProposal { .. } => { let mut count = PROPOSAL_COUNTER.load(deps.storage)?; - count += 1; + count = count.checked_add(1).unwrap_or_default(); PROPOSAL_COUNTER.save(deps.storage, &count)?; } ProposalHookMsg::ProposalStatusChanged { .. } => { let mut count = STATUS_CHANGED_COUNTER.load(deps.storage)?; - count += 1; + count = count.checked_add(1).unwrap_or_default(); STATUS_CHANGED_COUNTER.save(deps.storage, &count)?; } } @@ -88,12 +88,12 @@ pub fn execute_stake_hook( match stake_hook { StakeChangedHookMsg::Stake { .. } => { let mut count = STAKE_COUNTER.load(deps.storage)?; - count += Uint128::new(1); + count = count.checked_add(Uint128::new(1))?; STAKE_COUNTER.save(deps.storage, &count)?; } StakeChangedHookMsg::Unstake { .. } => { let mut count = STAKE_COUNTER.load(deps.storage)?; - count += Uint128::new(1); + count = count.checked_add(Uint128::new(1))?; STAKE_COUNTER.save(deps.storage, &count)?; } } @@ -110,7 +110,7 @@ pub fn execute_vote_hook( match vote_hook { VoteHookMsg::NewVote { .. } => { let mut count = VOTE_COUNTER.load(deps.storage)?; - count += 1; + count = count.checked_add(1).unwrap_or_default(); VOTE_COUNTER.save(deps.storage, &count)?; } } diff --git a/test-contracts/dao-proposal-sudo/src/error.rs b/contracts/test/dao-proposal-hook-counter/src/error.rs similarity index 60% rename from test-contracts/dao-proposal-sudo/src/error.rs rename to contracts/test/dao-proposal-hook-counter/src/error.rs index dc19f1033..0e7ceb05e 100644 --- a/test-contracts/dao-proposal-sudo/src/error.rs +++ b/contracts/test/dao-proposal-hook-counter/src/error.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{OverflowError, StdError}; use thiserror::Error; #[derive(Error, Debug)] @@ -6,6 +6,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error(transparent)] + OverflowError(#[from] OverflowError), + #[error("Unauthorized")] Unauthorized {}, } diff --git a/test-contracts/dao-proposal-hook-counter/src/lib.rs b/contracts/test/dao-proposal-hook-counter/src/lib.rs similarity index 100% rename from test-contracts/dao-proposal-hook-counter/src/lib.rs rename to contracts/test/dao-proposal-hook-counter/src/lib.rs diff --git a/test-contracts/dao-proposal-hook-counter/src/msg.rs b/contracts/test/dao-proposal-hook-counter/src/msg.rs similarity index 100% rename from test-contracts/dao-proposal-hook-counter/src/msg.rs rename to contracts/test/dao-proposal-hook-counter/src/msg.rs diff --git a/test-contracts/dao-proposal-hook-counter/src/state.rs b/contracts/test/dao-proposal-hook-counter/src/state.rs similarity index 100% rename from test-contracts/dao-proposal-hook-counter/src/state.rs rename to contracts/test/dao-proposal-hook-counter/src/state.rs diff --git a/test-contracts/dao-proposal-hook-counter/src/tests.rs b/contracts/test/dao-proposal-hook-counter/src/tests.rs similarity index 99% rename from test-contracts/dao-proposal-hook-counter/src/tests.rs rename to contracts/test/dao-proposal-hook-counter/src/tests.rs index cb3483a81..728baba65 100644 --- a/test-contracts/dao-proposal-hook-counter/src/tests.rs +++ b/contracts/test/dao-proposal-hook-counter/src/tests.rs @@ -121,12 +121,14 @@ fn instantiate_with_default_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id, msg: to_binary(&msg).unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, diff --git a/test-contracts/dao-proposal-sudo/.cargo/config b/contracts/test/dao-proposal-sudo/.cargo/config similarity index 100% rename from test-contracts/dao-proposal-sudo/.cargo/config rename to contracts/test/dao-proposal-sudo/.cargo/config diff --git a/test-contracts/dao-proposal-sudo/.gitignore b/contracts/test/dao-proposal-sudo/.gitignore similarity index 100% rename from test-contracts/dao-proposal-sudo/.gitignore rename to contracts/test/dao-proposal-sudo/.gitignore diff --git a/test-contracts/dao-proposal-sudo/Cargo.toml b/contracts/test/dao-proposal-sudo/Cargo.toml similarity index 100% rename from test-contracts/dao-proposal-sudo/Cargo.toml rename to contracts/test/dao-proposal-sudo/Cargo.toml diff --git a/test-contracts/dao-proposal-sudo/README b/contracts/test/dao-proposal-sudo/README similarity index 100% rename from test-contracts/dao-proposal-sudo/README rename to contracts/test/dao-proposal-sudo/README diff --git a/test-contracts/dao-proposal-sudo/examples/schema.rs b/contracts/test/dao-proposal-sudo/examples/schema.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/examples/schema.rs rename to contracts/test/dao-proposal-sudo/examples/schema.rs diff --git a/test-contracts/dao-proposal-sudo/schema/admin_response.json b/contracts/test/dao-proposal-sudo/schema/admin_response.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/admin_response.json rename to contracts/test/dao-proposal-sudo/schema/admin_response.json diff --git a/test-contracts/dao-proposal-sudo/schema/dao_response.json b/contracts/test/dao-proposal-sudo/schema/dao_response.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/dao_response.json rename to contracts/test/dao-proposal-sudo/schema/dao_response.json diff --git a/test-contracts/dao-proposal-sudo/schema/execute_msg.json b/contracts/test/dao-proposal-sudo/schema/execute_msg.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/execute_msg.json rename to contracts/test/dao-proposal-sudo/schema/execute_msg.json diff --git a/test-contracts/dao-proposal-sudo/schema/info_response.json b/contracts/test/dao-proposal-sudo/schema/info_response.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/info_response.json rename to contracts/test/dao-proposal-sudo/schema/info_response.json diff --git a/test-contracts/dao-proposal-sudo/schema/instantiate_msg.json b/contracts/test/dao-proposal-sudo/schema/instantiate_msg.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/instantiate_msg.json rename to contracts/test/dao-proposal-sudo/schema/instantiate_msg.json diff --git a/test-contracts/dao-proposal-sudo/schema/query_msg.json b/contracts/test/dao-proposal-sudo/schema/query_msg.json similarity index 100% rename from test-contracts/dao-proposal-sudo/schema/query_msg.json rename to contracts/test/dao-proposal-sudo/schema/query_msg.json diff --git a/test-contracts/dao-proposal-sudo/src/contract.rs b/contracts/test/dao-proposal-sudo/src/contract.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/src/contract.rs rename to contracts/test/dao-proposal-sudo/src/contract.rs diff --git a/test-contracts/dao-proposal-hook-counter/src/error.rs b/contracts/test/dao-proposal-sudo/src/error.rs similarity index 100% rename from test-contracts/dao-proposal-hook-counter/src/error.rs rename to contracts/test/dao-proposal-sudo/src/error.rs diff --git a/test-contracts/dao-proposal-sudo/src/lib.rs b/contracts/test/dao-proposal-sudo/src/lib.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/src/lib.rs rename to contracts/test/dao-proposal-sudo/src/lib.rs diff --git a/test-contracts/dao-proposal-sudo/src/msg.rs b/contracts/test/dao-proposal-sudo/src/msg.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/src/msg.rs rename to contracts/test/dao-proposal-sudo/src/msg.rs diff --git a/test-contracts/dao-proposal-sudo/src/state.rs b/contracts/test/dao-proposal-sudo/src/state.rs similarity index 100% rename from test-contracts/dao-proposal-sudo/src/state.rs rename to contracts/test/dao-proposal-sudo/src/state.rs diff --git a/test-contracts/dao-voting-cw20-balance/.cargo/config b/contracts/test/dao-test-custom-factory/.cargo/config similarity index 100% rename from test-contracts/dao-voting-cw20-balance/.cargo/config rename to contracts/test/dao-test-custom-factory/.cargo/config diff --git a/contracts/test/dao-test-custom-factory/Cargo.toml b/contracts/test/dao-test-custom-factory/Cargo.toml new file mode 100644 index 000000000..47ffd045e --- /dev/null +++ b/contracts/test/dao-test-custom-factory/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "dao-test-custom-factory" +authors = ["Jake Hartnell"] +description = "A test contract for testing factory patterns in dao-voting-token-staked and dao-voting-cw721-staked." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-storage = { workspace = true } +cw2 = { workspace = true } +cw721 = { workspace = true } +cw721-base = { workspace = true, features = ["library"] } +cw-ownable = { workspace = true } +cw-storage-plus = { workspace = true } +cw-utils = { workspace = true } +thiserror = { workspace = true } +dao-dao-macros = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } +cw-tokenfactory-issuer = { workspace = true, features = ["library"] } + +[dev-dependencies] +cw-multi-test = { workspace = true } diff --git a/contracts/test/dao-test-custom-factory/README b/contracts/test/dao-test-custom-factory/README new file mode 100644 index 000000000..61c1265e5 --- /dev/null +++ b/contracts/test/dao-test-custom-factory/README @@ -0,0 +1,2 @@ +# Test Custom Factory contract +Used for testing custom factories with `dao-voting-token-staked` and `dao-voting-cw721-staked`. This also serves an example for how to build custom factory contracts for token or NFT based DAOs. diff --git a/contracts/test/dao-test-custom-factory/examples/schema.rs b/contracts/test/dao-test-custom-factory/examples/schema.rs new file mode 100644 index 000000000..f0a2ba4f4 --- /dev/null +++ b/contracts/test/dao-test-custom-factory/examples/schema.rs @@ -0,0 +1,10 @@ +use cosmwasm_schema::write_api; +use dao_test_custom_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + } +} diff --git a/contracts/test/dao-test-custom-factory/src/contract.rs b/contracts/test/dao-test-custom-factory/src/contract.rs new file mode 100644 index 000000000..7091c027a --- /dev/null +++ b/contracts/test/dao-test-custom-factory/src/contract.rs @@ -0,0 +1,497 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response, + StdResult, SubMsg, Uint128, WasmMsg, +}; +use cw2::set_contract_version; +use cw721::{Cw721QueryMsg, NumTokensResponse}; +use cw721_base::InstantiateMsg as Cw721InstantiateMsg; +use cw_ownable::Ownership; +use cw_storage_plus::Item; +use cw_tokenfactory_issuer::msg::{ + ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, +}; +use cw_utils::{one_coin, parse_reply_instantiate_data}; +use dao_interface::{ + nft::NftFactoryCallback, + state::ModuleInstantiateCallback, + token::{InitialBalance, NewTokenInfo, TokenFactoryCallback}, + voting::{ActiveThresholdQuery, Query as VotingModuleQueryMsg}, +}; +use dao_voting::threshold::{ + assert_valid_absolute_count_threshold, assert_valid_percentage_threshold, ActiveThreshold, + ActiveThresholdResponse, +}; + +use crate::{ + error::ContractError, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, +}; + +const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const INSTANTIATE_ISSUER_REPLY_ID: u64 = 1; +const INSTANTIATE_NFT_REPLY_ID: u64 = 2; + +const DAO: Item = Item::new("dao"); +const INITIAL_NFTS: Item> = Item::new("initial_nfts"); +const NFT_CONTRACT: Item = Item::new("nft_contract"); +const VOTING_MODULE: Item = Item::new("voting_module"); +const TOKEN_INFO: Item = Item::new("token_info"); + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + Ok(Response::new().add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::NftFactory { + code_id, + cw721_instantiate_msg, + initial_nfts, + } => execute_nft_factory( + deps, + env, + info, + cw721_instantiate_msg, + code_id, + initial_nfts, + ), + ExecuteMsg::NftFactoryWithFunds { + code_id, + cw721_instantiate_msg, + initial_nfts, + } => execute_nft_factory_with_funds( + deps, + env, + info, + cw721_instantiate_msg, + code_id, + initial_nfts, + ), + ExecuteMsg::NftFactoryNoCallback {} => execute_nft_factory_no_callback(deps, env, info), + ExecuteMsg::NftFactoryWrongCallback {} => { + execute_nft_factory_wrong_callback(deps, env, info) + } + ExecuteMsg::TokenFactoryFactory(token) => { + execute_token_factory_factory(deps, env, info, token) + } + ExecuteMsg::TokenFactoryFactoryWithFunds(token) => { + execute_token_factory_factory_with_funds(deps, env, info, token) + } + ExecuteMsg::TokenFactoryFactoryNoCallback {} => { + execute_token_factory_factory_no_callback(deps, env, info) + } + ExecuteMsg::TokenFactoryFactoryWrongCallback {} => { + execute_token_factory_factory_wrong_callback(deps, env, info) + } + ExecuteMsg::ValidateNftDao {} => execute_validate_nft_dao(deps, info), + } +} + +/// An example factory that instantiates a new NFT contract +/// A more realistic example would be something like a minter contract that creates +/// an NFT along with a minter contract for sales like on Stargaze. +pub fn execute_nft_factory( + deps: DepsMut, + _env: Env, + info: MessageInfo, + cw721_instantiate_msg: Cw721InstantiateMsg, + code_id: u64, + initial_nfts: Vec, +) -> Result { + // Save voting module address + VOTING_MODULE.save(deps.storage, &info.sender)?; + + // Query for DAO + let dao: Addr = deps + .querier + .query_wasm_smart(info.sender, &VotingModuleQueryMsg::Dao {})?; + + // Save DAO and TOKEN_INFO for use in replies + DAO.save(deps.storage, &dao)?; + + // Save initial NFTs for use in replies + INITIAL_NFTS.save(deps.storage, &initial_nfts)?; + + // Override minter to be the DAO address + let msg = to_binary(&Cw721InstantiateMsg { + name: cw721_instantiate_msg.name, + symbol: cw721_instantiate_msg.symbol, + minter: dao.to_string(), + })?; + + // Instantiate new contract, further setup is handled in the + // SubMsg reply. + let msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + admin: Some(dao.to_string()), + code_id, + msg, + funds: vec![], + label: "cw_tokenfactory_issuer".to_string(), + }, + INSTANTIATE_NFT_REPLY_ID, + ); + + Ok(Response::new().add_submessage(msg)) +} + +/// Requires one coin sent to test funds pass through for factory contracts +pub fn execute_nft_factory_with_funds( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw721_instantiate_msg: Cw721InstantiateMsg, + code_id: u64, + initial_nfts: Vec, +) -> Result { + // Validate one coin was sent + one_coin(&info)?; + + execute_nft_factory( + deps, + env, + info, + cw721_instantiate_msg, + code_id, + initial_nfts, + ) +} + +/// No callback for testing +pub fn execute_nft_factory_no_callback( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok(Response::new()) +} + +/// Wrong callback for testing +pub fn execute_nft_factory_wrong_callback( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok(Response::new().set_data(to_binary(&TokenFactoryCallback { + denom: "wrong".to_string(), + token_contract: None, + module_instantiate_callback: None, + })?)) +} + +/// An example factory that instantiates a cw_tokenfactory_issuer contract +/// A more realistic example would be something like a DeFi Pool or Augmented +/// bonding curve. +pub fn execute_token_factory_factory( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token: NewTokenInfo, +) -> Result { + // Save voting module address + VOTING_MODULE.save(deps.storage, &info.sender)?; + + // Query for DAO + let dao: Addr = deps + .querier + .query_wasm_smart(info.sender, &VotingModuleQueryMsg::Dao {})?; + + // Save DAO and TOKEN_INFO for use in replies + DAO.save(deps.storage, &dao)?; + TOKEN_INFO.save(deps.storage, &token)?; + + // Instantiate new contract, further setup is handled in the + // SubMsg reply. + let msg = SubMsg::reply_on_success( + WasmMsg::Instantiate { + admin: Some(dao.to_string()), + code_id: token.token_issuer_code_id, + msg: to_binary(&IssuerInstantiateMsg::NewToken { + subdenom: token.subdenom, + })?, + funds: vec![], + label: "cw_tokenfactory_issuer".to_string(), + }, + INSTANTIATE_ISSUER_REPLY_ID, + ); + + Ok(Response::new().add_submessage(msg)) +} + +/// Requires one coin sent to test funds pass through for factory contracts +pub fn execute_token_factory_factory_with_funds( + deps: DepsMut, + env: Env, + info: MessageInfo, + token: NewTokenInfo, +) -> Result { + // Validate one coin was sent + one_coin(&info)?; + + execute_token_factory_factory(deps, env, info, token) +} + +/// No callback for testing +pub fn execute_token_factory_factory_no_callback( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok(Response::new()) +} + +/// Wrong callback for testing +pub fn execute_token_factory_factory_wrong_callback( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, +) -> Result { + Ok(Response::new().set_data(to_binary(&NftFactoryCallback { + nft_contract: "nope".to_string(), + module_instantiate_callback: None, + })?)) +} + +/// Example method called in the ModuleInstantiateCallback providing +/// an example for checking the DAO has been setup correctly. +pub fn execute_validate_nft_dao( + deps: DepsMut, + info: MessageInfo, +) -> Result { + // Load the collection and voting module address + let collection_addr = NFT_CONTRACT.load(deps.storage)?; + let voting_module = VOTING_MODULE.load(deps.storage)?; + + // Query the collection owner and check that it's the DAO. + let owner: Ownership = deps.querier.query_wasm_smart( + collection_addr.clone(), + &cw721_base::msg::QueryMsg::::Ownership {}, + )?; + match owner.owner { + Some(owner) => { + if owner != info.sender { + return Err(ContractError::Unauthorized {}); + } + } + None => return Err(ContractError::Unauthorized {}), + } + + // Query the total supply of the NFT contract + let nft_supply: NumTokensResponse = deps + .querier + .query_wasm_smart(collection_addr.clone(), &Cw721QueryMsg::NumTokens {})?; + + // Check greater than zero + if nft_supply.count == 0 { + return Err(ContractError::NoInitialNfts {}); + } + + // Query active threshold + let active_threshold: ActiveThresholdResponse = deps + .querier + .query_wasm_smart(voting_module, &ActiveThresholdQuery::ActiveThreshold {})?; + + // If Active Threshold absolute count is configured, + // check the count is not greater than supply. + // Percentage is validated in the voting module contract. + if let Some(ActiveThreshold::AbsoluteCount { count }) = active_threshold.active_threshold { + assert_valid_absolute_count_threshold(count, Uint128::new(nft_supply.count.into()))?; + } + + Ok(Response::new()) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Info {} => query_info(deps), + } +} + +pub fn query_info(deps: Deps) -> StdResult { + let info = cw2::get_contract_version(deps.storage)?; + to_binary(&dao_interface::voting::InfoResponse { info }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + match msg.id { + INSTANTIATE_ISSUER_REPLY_ID => { + // Load DAO address and TOKEN_INFO + let dao = DAO.load(deps.storage)?; + let token = TOKEN_INFO.load(deps.storage)?; + let voting_module = VOTING_MODULE.load(deps.storage)?; + + // Parse issuer address from instantiate reply + let issuer_addr = parse_reply_instantiate_data(msg)?.contract_address; + + // Format the denom + let denom = format!("factory/{}/{}", &issuer_addr, token.subdenom); + + let initial_supply = token + .initial_balances + .iter() + .fold(Uint128::zero(), |previous, new_balance| { + previous + new_balance.amount + }); + let total_supply = initial_supply + token.initial_dao_balance.unwrap_or_default(); + + // Here we validate the active threshold to show how validation should be done + // in a factory contract. + let active_threshold: ActiveThresholdResponse = deps + .querier + .query_wasm_smart(voting_module, &ActiveThresholdQuery::ActiveThreshold {})?; + + if let Some(threshold) = active_threshold.active_threshold { + match threshold { + ActiveThreshold::Percentage { percent } => { + assert_valid_percentage_threshold(percent)?; + } + ActiveThreshold::AbsoluteCount { count } => { + assert_valid_absolute_count_threshold(count, initial_supply)?; + } + } + } + + // Msgs to be executed to finalize setup + let mut msgs: Vec = vec![]; + + // Grant an allowance to mint the initial supply + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::SetMinterAllowance { + address: env.contract.address.to_string(), + allowance: total_supply, + })?, + funds: vec![], + }); + + // Call issuer contract to mint tokens for initial balances + token + .initial_balances + .iter() + .for_each(|b: &InitialBalance| { + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::Mint { + to_address: b.address.clone(), + amount: b.amount, + }) + .unwrap_or_default(), + funds: vec![], + }); + }); + + // Add initial DAO balance to initial_balances if nonzero. + if let Some(initial_dao_balance) = token.initial_dao_balance { + if !initial_dao_balance.is_zero() { + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::Mint { + to_address: dao.to_string(), + amount: initial_dao_balance, + })?, + funds: vec![], + }); + } + } + + // Begin update issuer contract owner to be the DAO, this is a + // two-step ownership transfer. + msgs.push(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::UpdateOwnership( + cw_ownable::Action::TransferOwnership { + new_owner: dao.to_string(), + expiry: None, + }, + ))?, + funds: vec![], + }); + + // DAO must accept ownership transfer. Here we include a + // ModuleInstantiateCallback message that will be called by the + // dao-dao-core contract when voting module instantiation is + // complete. + let callback = ModuleInstantiateCallback { + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: issuer_addr.clone(), + msg: to_binary(&IssuerExecuteMsg::UpdateOwnership( + cw_ownable::Action::AcceptOwnership {}, + ))?, + funds: vec![], + })], + }; + + // Responses for `dao-voting-token-staked` MUST include a + // TokenFactoryCallback. + Ok(Response::new() + .add_messages(msgs) + .set_data(to_binary(&TokenFactoryCallback { + denom, + token_contract: Some(issuer_addr.to_string()), + module_instantiate_callback: Some(callback), + })?)) + } + INSTANTIATE_NFT_REPLY_ID => { + // Parse nft address from instantiate reply + let nft_address = parse_reply_instantiate_data(msg)?.contract_address; + + // Save NFT contract for use in validation reply + NFT_CONTRACT.save(deps.storage, &deps.api.addr_validate(&nft_address)?)?; + + let initial_nfts = INITIAL_NFTS.load(deps.storage)?; + + // Add mint messages that will be called by the DAO in the + // ModuleInstantiateCallback + let mut msgs: Vec = initial_nfts + .iter() + .flat_map(|nft| -> Result { + Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: nft_address.clone(), + funds: vec![], + msg: nft.clone(), + })) + }) + .collect::>(); + + // Clear space + INITIAL_NFTS.remove(deps.storage); + + // After DAO mints NFT, it calls back into the factory contract + // To validate the setup. NOTE: other patterns could be used for this + // but factory contracts SHOULD validate setups. + msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&ExecuteMsg::ValidateNftDao {})?, + funds: vec![], + })); + + // Responses for `dao-voting-cw721-staked` MUST include a + // NftFactoryCallback. + Ok(Response::new().set_data(to_binary(&NftFactoryCallback { + nft_contract: nft_address.to_string(), + module_instantiate_callback: Some(ModuleInstantiateCallback { msgs }), + })?)) + } + _ => Err(ContractError::UnknownReplyId { id: msg.id }), + } +} diff --git a/contracts/test/dao-test-custom-factory/src/error.rs b/contracts/test/dao-test-custom-factory/src/error.rs new file mode 100644 index 000000000..f7d753d54 --- /dev/null +++ b/contracts/test/dao-test-custom-factory/src/error.rs @@ -0,0 +1,31 @@ +use cosmwasm_std::StdError; +use cw_utils::{ParseReplyError, PaymentError}; +use dao_voting::threshold::ActiveThresholdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error(transparent)] + ActiveThresholdError(#[from] ActiveThresholdError), + + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), + + #[error(transparent)] + PaymentError(#[from] PaymentError), + + #[error("New NFT contract must be instantiated with at least one NFT")] + NoInitialNfts {}, + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Got a submessage reply with unknown id: {id}")] + UnknownReplyId { id: u64 }, + + #[error("Factory message must serialize to WasmMsg::Execute")] + UnsupportedFactoryMsg {}, +} diff --git a/contracts/test/dao-test-custom-factory/src/lib.rs b/contracts/test/dao-test-custom-factory/src/lib.rs new file mode 100644 index 000000000..3915b791e --- /dev/null +++ b/contracts/test/dao-test-custom-factory/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +mod error; +pub mod msg; + +pub use crate::error::ContractError; diff --git a/contracts/test/dao-test-custom-factory/src/msg.rs b/contracts/test/dao-test-custom-factory/src/msg.rs new file mode 100644 index 000000000..42128cd2e --- /dev/null +++ b/contracts/test/dao-test-custom-factory/src/msg.rs @@ -0,0 +1,44 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Binary; +use cw721_base::InstantiateMsg as Cw721InstantiateMsg; +use dao_interface::token::NewTokenInfo; + +#[cw_serde] +pub struct InstantiateMsg {} + +#[cw_serde] +pub enum ExecuteMsg { + /// Example NFT factory implementation + NftFactory { + code_id: u64, + cw721_instantiate_msg: Cw721InstantiateMsg, + initial_nfts: Vec, + }, + /// Example NFT factory implentation that execpts funds + NftFactoryWithFunds { + code_id: u64, + cw721_instantiate_msg: Cw721InstantiateMsg, + initial_nfts: Vec, + }, + /// Used for testing no callback + NftFactoryNoCallback {}, + /// Used for testing wrong callback + NftFactoryWrongCallback {}, + /// Example Factory Implementation + TokenFactoryFactory(NewTokenInfo), + /// Example Factory Implementation that accepts funds + TokenFactoryFactoryWithFunds(NewTokenInfo), + /// Used for testing no callback + TokenFactoryFactoryNoCallback {}, + /// Used for testing wrong callback + TokenFactoryFactoryWrongCallback {}, + /// Validate NFT DAO + ValidateNftDao {}, +} + +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + #[returns(dao_interface::voting::InfoResponse)] + Info {}, +} diff --git a/contracts/test/dao-voting-cw20-balance/.cargo/config b/contracts/test/dao-voting-cw20-balance/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/test/dao-voting-cw20-balance/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/test-contracts/dao-voting-cw20-balance/.gitignore b/contracts/test/dao-voting-cw20-balance/.gitignore similarity index 100% rename from test-contracts/dao-voting-cw20-balance/.gitignore rename to contracts/test/dao-voting-cw20-balance/.gitignore diff --git a/test-contracts/dao-voting-cw20-balance/Cargo.toml b/contracts/test/dao-voting-cw20-balance/Cargo.toml similarity index 100% rename from test-contracts/dao-voting-cw20-balance/Cargo.toml rename to contracts/test/dao-voting-cw20-balance/Cargo.toml diff --git a/test-contracts/dao-voting-cw20-balance/README.md b/contracts/test/dao-voting-cw20-balance/README.md similarity index 100% rename from test-contracts/dao-voting-cw20-balance/README.md rename to contracts/test/dao-voting-cw20-balance/README.md diff --git a/test-contracts/dao-voting-cw20-balance/examples/schema.rs b/contracts/test/dao-voting-cw20-balance/examples/schema.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/examples/schema.rs rename to contracts/test/dao-voting-cw20-balance/examples/schema.rs diff --git a/test-contracts/dao-voting-cw20-balance/schema/execute_msg.json b/contracts/test/dao-voting-cw20-balance/schema/execute_msg.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/execute_msg.json rename to contracts/test/dao-voting-cw20-balance/schema/execute_msg.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/info_response.json b/contracts/test/dao-voting-cw20-balance/schema/info_response.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/info_response.json rename to contracts/test/dao-voting-cw20-balance/schema/info_response.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/instantiate_msg.json b/contracts/test/dao-voting-cw20-balance/schema/instantiate_msg.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/instantiate_msg.json rename to contracts/test/dao-voting-cw20-balance/schema/instantiate_msg.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/query_msg.json b/contracts/test/dao-voting-cw20-balance/schema/query_msg.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/query_msg.json rename to contracts/test/dao-voting-cw20-balance/schema/query_msg.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/total_power_at_height_response.json b/contracts/test/dao-voting-cw20-balance/schema/total_power_at_height_response.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/total_power_at_height_response.json rename to contracts/test/dao-voting-cw20-balance/schema/total_power_at_height_response.json diff --git a/test-contracts/dao-voting-cw20-balance/schema/voting_power_at_height_response.json b/contracts/test/dao-voting-cw20-balance/schema/voting_power_at_height_response.json similarity index 100% rename from test-contracts/dao-voting-cw20-balance/schema/voting_power_at_height_response.json rename to contracts/test/dao-voting-cw20-balance/schema/voting_power_at_height_response.json diff --git a/test-contracts/dao-voting-cw20-balance/src/contract.rs b/contracts/test/dao-voting-cw20-balance/src/contract.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/contract.rs rename to contracts/test/dao-voting-cw20-balance/src/contract.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/error.rs b/contracts/test/dao-voting-cw20-balance/src/error.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/error.rs rename to contracts/test/dao-voting-cw20-balance/src/error.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/lib.rs b/contracts/test/dao-voting-cw20-balance/src/lib.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/lib.rs rename to contracts/test/dao-voting-cw20-balance/src/lib.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/msg.rs b/contracts/test/dao-voting-cw20-balance/src/msg.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/msg.rs rename to contracts/test/dao-voting-cw20-balance/src/msg.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/state.rs b/contracts/test/dao-voting-cw20-balance/src/state.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/state.rs rename to contracts/test/dao-voting-cw20-balance/src/state.rs diff --git a/test-contracts/dao-voting-cw20-balance/src/tests.rs b/contracts/test/dao-voting-cw20-balance/src/tests.rs similarity index 100% rename from test-contracts/dao-voting-cw20-balance/src/tests.rs rename to contracts/test/dao-voting-cw20-balance/src/tests.rs diff --git a/contracts/voting/dao-voting-cw721-staked/Cargo.toml b/contracts/voting/dao-voting-cw721-staked/Cargo.toml index a52107cca..1191b5a85 100644 --- a/contracts/voting/dao-voting-cw721-staked/Cargo.toml +++ b/contracts/voting/dao-voting-cw721-staked/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dao-voting-cw721-staked" -authors = ["CypherApe cypherape@protonmail.com"] +authors = ["CypherApe cypherape@protonmail.com", "Jake Hartnell", "ekez"] description = "A DAO DAO voting module based on staked cw721 tokens." edition = { workspace = true } license = { workspace = true } @@ -11,8 +11,16 @@ version = { workspace = true } crate-type = ["cdylib", "rlib"] [features] +# for more explicit tests, cargo test --features=backtraces backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports library = [] +# use test tube feature to enable test-tube integration tests, for example +# cargo test --features "test-tube" +test-tube = [] +# when writing tests you may wish to enable test-tube as a default feature +# default = ["test-tube"] + [dependencies] cosmwasm-std = { workspace = true } @@ -30,13 +38,15 @@ dao-dao-macros = { workspace = true } dao-hooks = { workspace = true } dao-interface = { workspace = true } dao-voting = { workspace = true } -sg-std = { workspace = true } -sg721 = { workspace = true } thiserror = { workspace = true } [dev-dependencies] anyhow = { workspace = true } cw-multi-test = { workspace = true } -dao-testing = { workspace = true } -sg721-base = { workspace = true, features = ["library"] } -sg-multi-test = { workspace = true } +dao-proposal-single = { workspace = true } +dao-proposal-hook-counter = { workspace = true } +dao-test-custom-factory = { workspace = true } +dao-testing = { workspace = true, features = ["test-tube"] } +osmosis-std = { workpsace = true } +osmosis-test-tube = { workspace = true } +serde = { workspace = true } diff --git a/contracts/voting/dao-voting-cw721-staked/README.md b/contracts/voting/dao-voting-cw721-staked/README.md index ac91953ae..595faec5f 100644 --- a/contracts/voting/dao-voting-cw721-staked/README.md +++ b/contracts/voting/dao-voting-cw721-staked/README.md @@ -7,4 +7,12 @@ This is a basic implementation of an NFT staking contract. Staked tokens can be unbonded with a configurable unbonding period. Staked balances can be queried at any arbitrary height by external contracts. This contract implements the interface needed to be a DAO DAO [voting module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module). -`dao-voting-cw721-staked` can be used with existing NFT collections or to create a new `cw721` or `sg721` collections upon instantiation (with the DAO as admin and `minter` / `creator`). +`dao-voting-cw721-staked` can be used with an `existing` NFT collection or to create a `new` `cw721` collection upon instantiation (with the DAO as admin and `minter`). + +To support Stargaze NFTs and other custom NFT contracts or setups with minters (such as the Stargaze Open Edition minter), this contract also supports a `factory` pattern which takes a single `WasmMsg::Execute` message that calls into a custom factory contract. + +**NOTE:** when using the factory pattern, it is important to only use a trusted factory contract, as all validation happens in the factory contract. + +Those implementing custom factory contracts MUST handle any validation that is to happen, and the custom `WasmMsg::Execute` message MUST include `NftFactoryCallback` data respectively. + +The [dao-test-custom-factory contract](../test/dao-test-custom-factory) provides an example of how this can be done and is used for tests. It is NOT production ready, but meant to serve as an example for building factory contracts. diff --git a/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json b/contracts/voting/dao-voting-cw721-staked/schema/dao-voting-cw721-staked.json index aabc38089..d7d0bd586 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 @@ -137,6 +137,7 @@ "NftContract": { "oneOf": [ { + "description": "Uses an existing cw721 or sg721 token contract.", "type": "object", "required": [ "existing" @@ -149,7 +150,7 @@ ], "properties": { "address": { - "description": "Address of an already instantiated cw721 token contract.", + "description": "Address of an already instantiated cw721 or sg721 token contract.", "type": "string" } }, @@ -159,6 +160,7 @@ "additionalProperties": false }, { + "description": "Creates a new NFT collection used for staking and governance.", "type": "object", "required": [ "new" @@ -198,6 +200,19 @@ } }, "additionalProperties": false + }, + { + "description": "Uses a factory contract that must return the address of the NFT contract. The binary must serialize to a `WasmMsg::Execute` message. Validation happens in the factory contract itself, so be sure to use a trusted factory contract.", + "type": "object", + "required": [ + "factory" + ], + "properties": { + "factory": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false } ] }, @@ -250,6 +265,7 @@ "additionalProperties": false }, { + "description": "Claim NFTs that have been unstaked for the specified duration.", "type": "object", "required": [ "claim_nfts" @@ -263,6 +279,7 @@ "additionalProperties": false }, { + "description": "Updates the contract configuration, namely unstaking duration. Only callable by the DAO that initialized this voting contract.", "type": "object", "required": [ "update_config" @@ -288,6 +305,7 @@ "additionalProperties": false }, { + "description": "Adds a hook which is called on staking / unstaking events. Only callable by the DAO that initialized this voting contract.", "type": "object", "required": [ "add_hook" @@ -309,6 +327,7 @@ "additionalProperties": false }, { + "description": "Removes a hook which is called on staking / unstaking events. Only callable by the DAO that initialized this voting contract.", "type": "object", "required": [ "remove_hook" @@ -330,7 +349,7 @@ "additionalProperties": false }, { - "description": "Sets the active threshold to a new value. Only the instantiator this contract (a DAO most likely) may call this method.", + "description": "Sets the active threshold to a new value. Only callable by the DAO that initialized this voting contract.", "type": "object", "required": [ "update_active_threshold" diff --git a/contracts/voting/dao-voting-cw721-staked/src/contract.rs b/contracts/voting/dao-voting-cw721-staked/src/contract.rs index 82e6ac050..982d40e78 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/contract.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/contract.rs @@ -1,17 +1,21 @@ -use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - from_binary, to_binary, Addr, Binary, CosmosMsg, Decimal, Deps, DepsMut, Empty, Env, - MessageInfo, Reply, Response, StdError, StdResult, SubMsg, Uint128, Uint256, WasmMsg, + from_binary, to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Reply, + Response, StdError, StdResult, SubMsg, Uint128, Uint256, WasmMsg, }; use cw2::{get_contract_version, set_contract_version, ContractVersion}; use cw721::{Cw721QueryMsg, Cw721ReceiveMsg, NumTokensResponse}; use cw_storage_plus::Bound; -use cw_utils::{parse_reply_instantiate_data, Duration}; +use cw_utils::{parse_reply_execute_data, parse_reply_instantiate_data, Duration}; use dao_hooks::nft_stake::{stake_nft_hook_msgs, unstake_nft_hook_msgs}; -use dao_interface::voting::IsActiveResponse; -use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; +use dao_interface::state::ModuleInstantiateCallback; +use dao_interface::{nft::NftFactoryCallback, voting::IsActiveResponse}; +use dao_voting::duration::validate_duration; +use dao_voting::threshold::{ + assert_valid_absolute_count_threshold, assert_valid_percentage_threshold, ActiveThreshold, + ActiveThresholdResponse, +}; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg}; use crate::state::{ @@ -24,39 +28,29 @@ pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-voting-cw721-staked"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); const INSTANTIATE_NFT_CONTRACT_REPLY_ID: u64 = 0; -const VALIDATE_ABSOLUTE_COUNT_FOR_NEW_NFT_CONTRACTS: u64 = 1; +const VALIDATE_SUPPLY_REPLY_ID: u64 = 1; +const FACTORY_EXECUTE_REPLY_ID: u64 = 2; // We multiply by this when calculating needed power for being active // when using active threshold with percent const PRECISION_FACTOR: u128 = 10u128.pow(9); -#[cw_serde] +// Supported NFT instantiation messages pub enum NftInstantiateMsg { Cw721(cw721_base::InstantiateMsg), - Sg721(sg721::InstantiateMsg), } impl NftInstantiateMsg { - fn modify_instantiate_msg(&mut self, minter: &str, dao: &str) { + fn modify_instantiate_msg(&mut self, minter: &str) { match self { // Update minter for cw721 NFTs NftInstantiateMsg::Cw721(msg) => msg.minter = minter.to_string(), - NftInstantiateMsg::Sg721(msg) => { - // Update minter and collection creator for sg721 NFTs - // The collection creator is the only one able to call certain methods - // in sg721 contracts - msg.minter = minter.to_string(); - // This should be the DAO, which will be able to control metadata about - // the collection as well as royalties - msg.collection_info.creator = dao.to_string(); - } } } fn to_binary(&self) -> Result { match self { NftInstantiateMsg::Cw721(msg) => to_binary(&msg), - NftInstantiateMsg::Sg721(msg) => to_binary(&msg), } } } @@ -68,10 +62,6 @@ pub fn try_deserialize_nft_instantiate_msg( return Ok(NftInstantiateMsg::Cw721(cw721_msg)); } - if let Ok(sg721_msg) = from_binary::(&instantiate_msg) { - return Ok(NftInstantiateMsg::Sg721(sg721_msg)); - } - Err(ContractError::NftInstantiateError {}) } @@ -86,27 +76,28 @@ pub fn instantiate( DAO.save(deps.storage, &info.sender)?; + // Validate unstaking duration + validate_duration(msg.unstaking_duration)?; + + // Validate active threshold if configured if let Some(active_threshold) = msg.active_threshold.as_ref() { match active_threshold { ActiveThreshold::Percentage { percent } => { - if percent > &Decimal::percent(100) || percent.is_zero() { - return Err(ContractError::InvalidActivePercentage {}); - } + assert_valid_percentage_threshold(*percent)?; } ActiveThreshold::AbsoluteCount { count } => { - // Check Absolute count is not zero - if count.is_zero() { - return Err(ContractError::ZeroActiveCount {}); - } - - // Check Absolute count is less than the supply of NFTs for existing NFT contracts + // Check Absolute count is less than the supply of NFTs for existing + // NFT contracts. For new NFT contracts, we will check this in the reply. if let NftContract::Existing { ref address } = msg.nft_contract { let nft_supply: NumTokensResponse = deps .querier .query_wasm_smart(address, &Cw721QueryMsg::NumTokens {})?; - if count > &Uint128::new(nft_supply.count.into()) { - return Err(ContractError::InvalidActiveCount {}); - } + // Check the absolute count is less than the supply of NFTs and + // greater than zero. + assert_valid_absolute_count_threshold( + *count, + Uint128::new(nft_supply.count.into()), + )?; } } } @@ -133,17 +124,12 @@ pub fn instantiate( msg: instantiate_msg, initial_nfts, } => { - // Deserialize the binary msg into either cw721 or sg721 + // Deserialize the binary msg into cw721 let mut instantiate_msg = try_deserialize_nft_instantiate_msg(instantiate_msg)?; // Modify the InstantiateMsg such that the minter is now this contract. // We will update ownership of the NFT contract to be the DAO in the submessage reply. - // - // NOTE: sg721 also has a creator that is set in the `collection_info` field, - // we override this with the address of the DAO (the sender of this message). - // In sg721 the `creator` address controls metadata and royalties. - instantiate_msg - .modify_instantiate_msg(env.contract.address.as_str(), info.sender.as_str()); + instantiate_msg.modify_instantiate_msg(env.contract.address.as_str()); // Check there is at least one NFT to initialize if initial_nfts.is_empty() { @@ -176,6 +162,35 @@ pub fn instantiate( .add_attribute("method", "instantiate") .add_submessage(instantiate_msg)) } + NftContract::Factory(binary) => match from_binary(&binary)? { + WasmMsg::Execute { + msg: wasm_msg, + contract_addr, + funds, + } => { + // Save config with empty nft_address + let config = Config { + nft_address: Addr::unchecked(""), + unstaking_duration: msg.unstaking_duration, + }; + CONFIG.save(deps.storage, &config)?; + + // Call factory contract. Use only a trusted factory contract, + // as this is a critical security component and valdiation of + // setup will happen in the factory. + Ok(Response::new() + .add_attribute("action", "intantiate") + .add_submessage(SubMsg::reply_on_success( + WasmMsg::Execute { + contract_addr, + msg: wasm_msg, + funds, + }, + FACTORY_EXECUTE_REPLY_ID, + ))) + } + _ => Err(ContractError::UnsupportedFactoryMsg {}), + }, } } @@ -370,6 +385,9 @@ pub fn execute_update_config( return Err(ContractError::Unauthorized {}); } + // Validate unstaking duration + validate_duration(duration)?; + config.unstaking_duration = duration; CONFIG.save(deps.storage, &config)?; @@ -435,17 +453,20 @@ pub fn execute_update_active_threshold( return Err(ContractError::Unauthorized {}); } + let config = CONFIG.load(deps.storage)?; 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 {}); - } + assert_valid_percentage_threshold(percent)?; } ActiveThreshold::AbsoluteCount { count } => { - if count.is_zero() { - return Err(ContractError::ZeroActiveCount {}); - } + let nft_supply: NumTokensResponse = deps + .querier + .query_wasm_smart(config.nft_address, &Cw721QueryMsg::NumTokens {})?; + assert_valid_absolute_count_threshold( + count, + Uint128::new(nft_supply.count.into()), + )?; } } ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?; @@ -681,7 +702,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result Result Err(ContractError::NftInstantiateError {}), } } - VALIDATE_ABSOLUTE_COUNT_FOR_NEW_NFT_CONTRACTS => { - // Check that absolute count is not greater than supply + VALIDATE_SUPPLY_REPLY_ID => { + // Check that NFTs have actually been minted, and that supply is greater than zero // NOTE: we have to check this in a reply as it is potentially possible // to include non-mint messages in `initial_nfts`. + // + // Load config for nft contract address + let collection_addr = CONFIG.load(deps.storage)?.nft_address; + + // Query the total supply of the NFT contract + let nft_supply: NumTokensResponse = deps + .querier + .query_wasm_smart(collection_addr.clone(), &Cw721QueryMsg::NumTokens {})?; + + // Check greater than zero + if nft_supply.count == 0 { + return Err(ContractError::NoInitialNfts {}); + } + + // If Active Threshold absolute count is configured, + // check the count is not greater than supply if let Some(ActiveThreshold::AbsoluteCount { count }) = ACTIVE_THRESHOLD.may_load(deps.storage)? { - // Load config for nft contract address - let collection_addr = CONFIG.load(deps.storage)?.nft_address; + assert_valid_absolute_count_threshold( + count, + Uint128::new(nft_supply.count.into()), + )?; + } - // Query the total supply of the NFT contract - let supply: NumTokensResponse = deps - .querier - .query_wasm_smart(collection_addr, &Cw721QueryMsg::NumTokens {})?; + // On setup success, have the DAO complete the second part of + // ownership transfer by accepting ownership in a + // ModuleInstantiateCallback. + let callback = to_binary(&ModuleInstantiateCallback { + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: collection_addr.to_string(), + msg: to_binary( + &&cw721_base::msg::ExecuteMsg::::UpdateOwnership( + cw721_base::Action::AcceptOwnership {}, + ), + )?, + funds: vec![], + })], + })?; + + Ok(Response::new().set_data(callback)) + } + FACTORY_EXECUTE_REPLY_ID => { + // Parse reply data + let res = parse_reply_execute_data(msg)?; + match res.data { + Some(data) => { + let mut config = CONFIG.load(deps.storage)?; + + // Parse info from the callback, this will fail + // if incorrectly formatted. + let info: NftFactoryCallback = from_binary(&data)?; + + // Validate NFT contract address + let nft_address = deps.api.addr_validate(&info.nft_contract)?; + + // Validate that this is an NFT with a query + deps.querier.query_wasm_smart::( + nft_address.clone(), + &Cw721QueryMsg::NumTokens {}, + )?; + + // Update NFT contract + config.nft_address = nft_address; + CONFIG.save(deps.storage, &config)?; + + // Construct the response + let mut res = Response::new().add_attribute("nft_contract", info.nft_contract); + + // If a callback has been configured, set the module + // instantiate callback data. + if let Some(callback) = info.module_instantiate_callback { + res = res.set_data(to_binary(&callback)?); + } - // Chec the count is not greater than supply - if count > Uint128::new(supply.count.into()) { - return Err(ContractError::InvalidActiveCount {}); + Ok(res) } + None => Err(ContractError::NoFactoryCallback {}), } - Ok(Response::new()) } _ => 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 15e2cecea..287c7a509 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/error.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/error.rs @@ -1,4 +1,6 @@ use cosmwasm_std::{Addr, StdError}; +use cw_utils::ParseReplyError; +use dao_voting::threshold::ActiveThresholdError; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -6,17 +8,20 @@ pub enum ContractError { #[error(transparent)] Std(#[from] StdError), - #[error("Can not stake that which has already been staked")] - AlreadyStaked {}, + #[error(transparent)] + ActiveThresholdError(#[from] ActiveThresholdError), #[error(transparent)] HookError(#[from] cw_hooks::HookError), - #[error("Active threshold count is greater than supply")] - InvalidActiveCount {}, + #[error(transparent)] + ParseReplyError(#[from] ParseReplyError), - #[error("Active threshold percentage must be greater than 0 and less than 1")] - InvalidActivePercentage {}, + #[error(transparent)] + UnstakingDurationError(#[from] dao_voting::duration::UnstakingDurationError), + + #[error("Can not stake that which has already been staked")] + AlreadyStaked {}, #[error("Invalid token. Got ({received}), expected ({expected})")] InvalidToken { received: Addr, expected: Addr }, @@ -27,6 +32,9 @@ pub enum ContractError { #[error("New NFT contract must be instantiated with at least one NFT")] NoInitialNfts {}, + #[error("Factory contract did not implment the required NftFactoryCallback interface")] + NoFactoryCallback {}, + #[error("Nothing to claim")] NothingToClaim {}, @@ -45,8 +53,8 @@ pub enum ContractError { #[error("Got a submessage reply with unknown id: {id}")] UnknownReplyId { id: u64 }, - #[error("Active threshold count must be greater than zero")] - ZeroActiveCount {}, + #[error("Factory message must serialize to WasmMsg::Execute")] + UnsupportedFactoryMsg {}, #[error("Can't unstake zero NFTs.")] ZeroUnstake {}, diff --git a/contracts/voting/dao-voting-cw721-staked/src/msg.rs b/contracts/voting/dao-voting-cw721-staked/src/msg.rs index f47d82533..837851ed3 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/msg.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/msg.rs @@ -8,10 +8,12 @@ use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; #[cw_serde] #[allow(clippy::large_enum_variant)] pub enum NftContract { + /// Uses an existing cw721 or sg721 token contract. Existing { - /// Address of an already instantiated cw721 token contract. + /// Address of an already instantiated cw721 or sg721 token contract. address: String, }, + /// Creates a new NFT collection used for staking and governance. New { /// Code ID for cw721 token contract. code_id: u64, @@ -23,6 +25,11 @@ pub enum NftContract { /// valid mint message for the corresponding cw721 contract. initial_nfts: Vec, }, + /// Uses a factory contract that must return the address of the NFT contract. + /// The binary must serialize to a `WasmMsg::Execute` message. + /// Validation happens in the factory contract itself, so be sure to use a + /// trusted factory contract. + Factory(Binary), } #[cw_serde] @@ -46,22 +53,20 @@ pub enum ExecuteMsg { /// Unstakes the specified token_ids on behalf of the /// sender. token_ids must have unique values and have non-zero /// length. - Unstake { - token_ids: Vec, - }, + Unstake { token_ids: Vec }, + /// Claim NFTs that have been unstaked for the specified duration. ClaimNfts {}, - UpdateConfig { - duration: Option, - }, - AddHook { - addr: String, - }, - RemoveHook { - addr: String, - }, - /// Sets the active threshold to a new value. Only the - /// instantiator this contract (a DAO most likely) may call this - /// method. + /// Updates the contract configuration, namely unstaking duration. + /// Only callable by the DAO that initialized this voting contract. + UpdateConfig { duration: Option }, + /// Adds a hook which is called on staking / unstaking events. + /// Only callable by the DAO that initialized this voting contract. + AddHook { addr: String }, + /// Removes a hook which is called on staking / unstaking events. + /// Only callable by the DAO that initialized this voting contract. + RemoveHook { addr: String }, + /// Sets the active threshold to a new value. + /// Only callable by the DAO that initialized this voting contract. UpdateActiveThreshold { new_threshold: Option, }, diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs new file mode 100644 index 000000000..d87edace7 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/integration_tests.rs @@ -0,0 +1,158 @@ +use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Empty, Uint128, WasmMsg}; +use cw721_base::{ + msg::{ + ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg, + QueryMsg as Cw721QueryMsg, + }, + MinterResponse, +}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo}, +}; +use dao_testing::test_tube::{cw721_base::Cw721Base, dao_dao_core::DaoCore}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{ActiveThreshold, PercentageThreshold, Threshold}, +}; +use osmosis_test_tube::{Account, OsmosisTestApp, RunnerError}; + +use crate::{ + msg::{InstantiateMsg, NftContract, QueryMsg}, + state::Config, + testing::test_tube_env::Cw721VotingContract, +}; + +use super::test_tube_env::{TestEnv, TestEnvBuilder}; + +#[test] +fn test_full_integration_with_factory() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + + // Setup defaults to creating a NFT DAO with the factory contract + // This does not use funds when instantiating the NFT contract. + // We will test that below. + let TestEnv { + vp_contract, + proposal_single, + custom_factory, + accounts, + cw721, + .. + } = env.setup(&app); + + // Test instantiating a DAO with a factory contract that requires funds + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_binary(&InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: custom_factory.contract_addr.clone(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWithFunds { + code_id: cw721.code_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: accounts[0].address(), + }, + initial_nfts: vec![to_binary( + &Cw721ExecuteMsg::::Mint { + owner: accounts[0].address(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }, + ) + .unwrap()], + }, + ) + .unwrap(), + funds: vec![Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.code_id, + msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiating without funds fails + let err = DaoCore::new(&app, &msg, &accounts[0], &[]).unwrap_err(); + + // Error is insufficient funds as no funds were sent + assert_eq!( + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: 0uosmo is smaller than 1000uosmo: insufficient funds".to_string() + }, + err + ); + + // Instantiate DAO succeeds with funds + let dao = DaoCore::new( + &app, + &msg, + &accounts[0], + &[Coin { + amount: Uint128::new(1000), + denom: "uosmo".to_string(), + }], + ) + .unwrap(); + + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + Cw721VotingContract::new_with_values(&app, vp_contract.code_id, vp_addr.to_string()) + .unwrap(); + + let vp_config: Config = vp_contract.query(&QueryMsg::Config {}).unwrap(); + let cw721_contract = + Cw721Base::new_with_values(&app, cw721.code_id, vp_config.nft_address.to_string()).unwrap(); + + // Check DAO was initialized to minter + let minter: MinterResponse = cw721_contract.query(&Cw721QueryMsg::Minter {}).unwrap(); + assert_eq!(minter.minter, Some(dao.contract_addr.to_string())); +} diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs index ea43bc797..de0824f52 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/mod.rs @@ -5,6 +5,16 @@ mod instantiate; mod queries; mod tests; +// Integrationg tests using an actual chain binary, requires +// the "test-tube" feature to be enabled +// cargo test --features test-tube +#[cfg(test)] +#[cfg(feature = "test-tube")] +mod integration_tests; +#[cfg(test)] +#[cfg(feature = "test-tube")] +mod test_tube_env; + use cosmwasm_std::Addr; use cw_multi_test::{App, Executor}; use cw_utils::Duration; diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs new file mode 100644 index 000000000..cea9d24a1 --- /dev/null +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/test_tube_env.rs @@ -0,0 +1,317 @@ +// The code is used in tests but reported as dead code +// see https://github.com/rust-lang/rust/issues/46379 +#![allow(dead_code)] + +use crate::{ + msg::{ExecuteMsg, InstantiateMsg, NftContract, QueryMsg}, + state::Config, +}; + +use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Empty, WasmMsg}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo, ProposalModule}, +}; +use dao_voting::{ + pre_propose::PreProposeInfo, threshold::PercentageThreshold, threshold::Threshold, +}; + +use cw721_base::msg::{ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg}; +use dao_testing::test_tube::{ + cw721_base::Cw721Base, dao_dao_core::DaoCore, dao_proposal_single::DaoProposalSingle, + dao_test_custom_factory::CustomFactoryContract, +}; +use dao_voting::threshold::ActiveThreshold; +use osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse; +use osmosis_test_tube::{ + Account, Bank, Module, OsmosisTestApp, RunnerError, RunnerExecuteResult, RunnerResult, + SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::path::PathBuf; + +pub const DENOM: &str = "ucat"; +pub const JUNO: &str = "ujuno"; + +pub struct TestEnv<'a> { + pub app: &'a OsmosisTestApp, + pub dao: DaoCore<'a>, + pub proposal_single: DaoProposalSingle<'a>, + pub custom_factory: CustomFactoryContract<'a>, + pub vp_contract: Cw721VotingContract<'a>, + pub accounts: Vec, + pub cw721: Cw721Base<'a>, +} + +impl<'a> TestEnv<'a> { + pub fn bank(&self) -> Bank<'_, OsmosisTestApp> { + Bank::new(self.app) + } +} + +pub struct TestEnvBuilder {} + +impl TestEnvBuilder { + pub fn new() -> Self { + Self {} + } + + // Full DAO setup + pub fn setup(self, app: &'_ OsmosisTestApp) -> TestEnv<'_> { + let accounts = app + .init_accounts(&[Coin::new(1000000000000000u128, "uosmo")], 10) + .unwrap(); + + // Upload all needed code ids + let vp_contract_id = Cw721VotingContract::upload(app, &accounts[0]).unwrap(); + let proposal_single_id = DaoProposalSingle::upload(app, &accounts[0]).unwrap(); + let cw721_id = Cw721Base::upload(app, &accounts[0]).unwrap(); + + // Instantiate Custom Factory + let custom_factory = CustomFactoryContract::new( + app, + &dao_test_custom_factory::msg::InstantiateMsg {}, + &accounts[0], + ) + .unwrap(); + + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract_id, + msg: to_binary(&InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: custom_factory.contract_addr.clone(), + msg: to_binary(&dao_test_custom_factory::msg::ExecuteMsg::NftFactory { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: accounts[0].address(), + }, + initial_nfts: vec![to_binary( + &Cw721ExecuteMsg::::Mint { + owner: accounts[0].address(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }, + ) + .unwrap()], + }) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single_id, + msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO + let dao = DaoCore::new(app, &msg, &accounts[0], &[]).unwrap(); + + // Get voting module address, setup vp_contract helper + let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let vp_contract = + Cw721VotingContract::new_with_values(app, vp_contract_id, vp_addr.to_string()).unwrap(); + + let vp_config: Config = vp_contract.query(&QueryMsg::Config {}).unwrap(); + + let cw721 = + Cw721Base::new_with_values(app, cw721_id, vp_config.nft_address.to_string()).unwrap(); + + // Get proposal module address, setup proposal_single helper + let proposal_modules: Vec = dao + .query(&DaoQueryMsg::ProposalModules { + limit: None, + start_after: None, + }) + .unwrap(); + let proposal_single = DaoProposalSingle::new_with_values( + app, + proposal_single_id, + proposal_modules[0].address.to_string(), + ) + .unwrap(); + + TestEnv { + app, + dao, + vp_contract, + proposal_single, + custom_factory, + accounts, + cw721, + } + } +} + +#[derive(Debug)] +pub struct Cw721VotingContract<'a> { + pub app: &'a OsmosisTestApp, + pub contract_addr: String, + pub code_id: u64, +} + +impl<'a> Cw721VotingContract<'a> { + pub fn deploy( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + pub fn instantiate( + app: &'a OsmosisTestApp, + code_id: u64, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn execute( + &self, + msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, msg, funds, signer) + } + + pub fn query(&self, msg: &QueryMsg) -> RunnerResult + where + T: ?Sized + DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_cw721_staked.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("..") + .join("artifacts") + .join("dao_voting_cw721_staked-aarch64.wasm"), + ) + .unwrap(), + } + } +} diff --git a/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs b/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs index 9f37af952..2f2ada85d 100644 --- a/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs +++ b/contracts/voting/dao-voting-cw721-staked/src/testing/tests.rs @@ -1,22 +1,19 @@ use cosmwasm_std::testing::{mock_dependencies, mock_env}; -use cosmwasm_std::{to_binary, Addr, Decimal, Empty, Uint128}; -use cw721::OwnerOfResponse; +use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Empty, Uint128, WasmMsg}; use cw721_base::msg::{ExecuteMsg as Cw721ExecuteMsg, InstantiateMsg as Cw721InstantiateMsg}; use cw721_controllers::{NftClaim, NftClaimsResponse}; -use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor}; +use cw_multi_test::{next_block, App, BankSudo, Executor, SudoMsg}; use cw_utils::Duration; use dao_interface::voting::IsActiveResponse; -use dao_testing::contracts::{cw721_base_contract, voting_cw721_staked_contract}; +use dao_testing::contracts::{ + cw721_base_contract, dao_test_custom_factory, voting_cw721_staked_contract, +}; use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; -use sg721::{CollectionInfo, RoyaltyInfoResponse, UpdateCollectionInfoMsg}; -use sg721_base::msg::CollectionInfoResponse; -use sg_multi_test::StargazeApp; -use sg_std::StargazeMsgWrapper; use crate::{ contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg}, - state::{Config, MAX_CLAIMS}, + state::MAX_CLAIMS, testing::{ execute::{ claim_nfts, mint_and_stake_nft, mint_nft, stake_nft, unstake_nfts, update_config, @@ -200,6 +197,13 @@ fn test_update_config() -> anyhow::Result<()> { } ); + // Update config to invalid duration fails + let err = update_config(&mut app, &module, CREATOR_ADDR, Some(Duration::Time(0))).unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Invalid unstaking duration, unstaking duration cannot be 0".to_string() + ); + // Update duration update_config(&mut app, &module, CREATOR_ADDR, Some(Duration::Time(1)))?; @@ -414,6 +418,45 @@ fn test_add_remove_hooks() -> anyhow::Result<()> { Ok(()) } +#[test] +fn test_instantiate_with_invalid_duration_fails() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Extension { + msg: Empty {}, + }) + .unwrap()], + }, + unstaking_duration: None, + active_threshold: None, + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "New NFT contract must be instantiated with at least one NFT".to_string() + ); +} + #[test] #[should_panic(expected = "Active threshold count must be greater than zero")] fn test_instantiate_zero_active_threshold_count() { @@ -455,7 +498,7 @@ fn test_instantiate_zero_active_threshold_count() { } #[test] -#[should_panic(expected = "Active threshold count is greater than supply")] +#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] fn test_instantiate_invalid_active_threshold_count_new_nft() { let mut app = App::default(); let cw721_id = app.store_code(cw721_base_contract()); @@ -495,7 +538,7 @@ fn test_instantiate_invalid_active_threshold_count_new_nft() { } #[test] -#[should_panic(expected = "Active threshold count is greater than supply")] +#[should_panic(expected = "Absolute count threshold cannot be greater than the total token supply")] fn test_instantiate_invalid_active_threshold_count_existing_nft() { let mut app = App::default(); let module_id = app.store_code(voting_cw721_staked_contract()); @@ -808,7 +851,7 @@ fn test_update_active_threshold() { let msg = ExecuteMsg::UpdateActiveThreshold { new_threshold: Some(ActiveThreshold::AbsoluteCount { - count: Uint128::new(100), + count: Uint128::new(1), }), }; @@ -832,13 +875,15 @@ fn test_update_active_threshold() { assert_eq!( resp.active_threshold, Some(ActiveThreshold::AbsoluteCount { - count: Uint128::new(100) + count: Uint128::new(1) }) ); } #[test] -#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +#[should_panic( + expected = "Active threshold percentage must be greater than 0 and not greater than 1" +)] fn test_active_threshold_percentage_gt_100() { let mut app = App::default(); let cw721_id = app.store_code(cw721_base_contract()); @@ -878,7 +923,9 @@ fn test_active_threshold_percentage_gt_100() { } #[test] -#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +#[should_panic( + expected = "Active threshold percentage must be greater than 0 and not greater than 1" +)] fn test_active_threshold_percentage_lte_0() { let mut app = App::default(); let cw721_id = app.store_code(cw721_base_contract()); @@ -957,10 +1004,10 @@ fn test_invalid_instantiate_msg() { } #[test] -fn test_no_initial_nfts_fails() { +fn test_invalid_initial_nft_msg() { 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 cw721_id = app.store_code(cw721_base_contract()); let err = app .instantiate_contract( @@ -976,12 +1023,13 @@ fn test_no_initial_nfts_fails() { minter: CREATOR_ADDR.to_string(), }) .unwrap(), - initial_nfts: vec![], + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Extension { + msg: Empty {}, + }) + .unwrap()], }, unstaking_duration: None, - active_threshold: Some(ActiveThreshold::Percentage { - percent: Decimal::percent(1), - }), + active_threshold: None, }, &[], "cw721_voting", @@ -994,150 +1042,393 @@ fn test_no_initial_nfts_fails() { ); } -// Setup Stargaze contracts for multi-test -fn sg721_base_contract() -> Box> { - let contract = ContractWrapper::new( - sg721_base::entry::execute, - sg721_base::entry::instantiate, - sg721_base::entry::query, - ); - Box::new(contract) -} +#[test] +fn test_invalid_initial_nft_msg_wrong_absolute_count() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); -// Stargze contracts need a custom message wrapper -fn voting_sg721_staked_contract() -> Box> { - let contract = ContractWrapper::new_with_empty( - crate::contract::execute, - crate::contract::instantiate, - crate::contract::query, - ) - .with_reply_empty(crate::contract::reply); - Box::new(contract) + let err = app + .instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::New { + code_id: cw721_id, + label: "Test NFT".to_string(), + msg: to_binary(&Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }) + .unwrap(), + initial_nfts: vec![ + to_binary(&Cw721ExecuteMsg::::Extension { msg: Empty {} }) + .unwrap(), + to_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap(), + ], + }, + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(2), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "Absolute count threshold cannot be greater than the total token supply".to_string() + ); } -// I can create new Stargaze NFT collection when creating a dao-voting-cw721-staked contract #[test] -fn test_instantiate_with_new_sg721_collection() -> anyhow::Result<()> { - let mut app = StargazeApp::default(); - let module_id = app.store_code(voting_sg721_staked_contract()); - let sg721_id = app.store_code(sg721_base_contract()); +fn test_no_initial_nfts_fails() { + let mut app = App::default(); + let cw721_id = app.store_code(cw721_base_contract()); + let module_id = app.store_code(voting_cw721_staked_contract()); - let module_addr = app + let err = app .instantiate_contract( module_id, Addr::unchecked(CREATOR_ADDR), &InstantiateMsg { nft_contract: NftContract::New { - code_id: sg721_id, + code_id: cw721_id, label: "Test NFT".to_string(), - msg: to_binary(&sg721::InstantiateMsg { + msg: to_binary(&Cw721InstantiateMsg { name: "Test NFT".to_string(), symbol: "TEST".to_string(), minter: CREATOR_ADDR.to_string(), - collection_info: CollectionInfo { - creator: CREATOR_ADDR.to_string(), - description: "Test NFT".to_string(), - image: "https://example.com/image.jpg".to_string(), - external_link: None, - explicit_content: None, - start_trading_time: None, - royalty_info: None, - }, - })?, - initial_nfts: vec![to_binary(&sg721::ExecuteMsg::::Mint { - owner: CREATOR_ADDR.to_string(), - token_uri: Some("https://example.com".to_string()), - token_id: "1".to_string(), - extension: Empty {}, - })?], + }) + .unwrap(), + initial_nfts: vec![], }, unstaking_duration: None, - active_threshold: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), }, &[], "cw721_voting", None, ) - .unwrap(); - - let config: Config = app - .wrap() - .query_wasm_smart(module_addr, &QueryMsg::Config {})?; - let sg721_addr = config.nft_address; + .unwrap_err(); + assert_eq!( + err.root_cause().to_string(), + "New NFT contract must be instantiated with at least one NFT".to_string() + ); +} - // Check that the NFT contract was created - let owner: OwnerOfResponse = app.wrap().query_wasm_smart( - sg721_addr.clone(), - &cw721::Cw721QueryMsg::OwnerOf { - token_id: "1".to_string(), - include_expired: None, - }, - )?; - assert_eq!(owner.owner, CREATOR_ADDR); +#[test] +fn test_factory() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + let factory_id = app.store_code(dao_test_custom_factory()); - // Check that collection info creator is set to the DAO (in this case CREATOR_ADDR) - // Normally the DAO would instantiate this contract - let creator: CollectionInfoResponse = app - .wrap() - .query_wasm_smart(sg721_addr, &sg721_base::msg::QueryMsg::CollectionInfo {})?; - assert_eq!(creator.creator, CREATOR_ADDR.to_string()); + // Instantiate factory + let factory_addr = app + .instantiate_contract( + factory_id, + Addr::unchecked(CREATOR_ADDR), + &dao_test_custom_factory::msg::InstantiateMsg {}, + &[], + "test factory".to_string(), + None, + ) + .unwrap(); - Ok(()) + // Instantiate using factory succeeds + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_binary(&dao_test_custom_factory::msg::ExecuteMsg::NftFactory { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }, + initial_nfts: vec![], + }) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); } #[test] -#[should_panic(expected = "Active threshold count is greater than supply")] -fn test_instantiate_with_new_sg721_collection_abs_count_validation() { - let mut app = StargazeApp::default(); - let module_id = app.store_code(voting_sg721_staked_contract()); - let sg721_id = app.store_code(sg721_base_contract()); +fn test_factory_with_funds_pass_through() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + let factory_id = app.store_code(dao_test_custom_factory()); + + // Mint some tokens to creator + app.sudo(SudoMsg::Bank(BankSudo::Mint { + to_address: CREATOR_ADDR.to_string(), + amount: vec![Coin { + denom: "ujuno".to_string(), + amount: Uint128::new(10000), + }], + })) + .unwrap(); - // Test edge case + // Instantiate factory + let factory_addr = app + .instantiate_contract( + factory_id, + Addr::unchecked(CREATOR_ADDR), + &dao_test_custom_factory::msg::InstantiateMsg {}, + &[], + "test factory".to_string(), + None, + ) + .unwrap(); + + // Instantiate without funds fails app.instantiate_contract( module_id, - Addr::unchecked("contract0"), + Addr::unchecked(CREATOR_ADDR), &InstantiateMsg { - nft_contract: NftContract::New { - code_id: sg721_id, - label: "Test NFT".to_string(), - msg: to_binary(&sg721::InstantiateMsg { - name: "Test NFT".to_string(), - symbol: "TEST".to_string(), - minter: "contract0".to_string(), - collection_info: CollectionInfo { - creator: "contract0".to_string(), - description: "Test NFT".to_string(), - image: "https://example.com/image.jpg".to_string(), - external_link: None, - explicit_content: None, - start_trading_time: None, - royalty_info: None, - }, + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWithFunds { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }, + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + ) + .unwrap(), + funds: vec![], }) .unwrap(), - initial_nfts: vec![ - to_binary(&sg721::ExecuteMsg::::Mint { - owner: "contract0".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(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap_err(); + + // Instantiate using factory succeeds + let funds = vec![Coin { + denom: "ujuno".to_string(), + amount: Uint128::new(100), + }]; + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWithFunds { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), + }, + initial_nfts: vec![to_binary(&Cw721ExecuteMsg::::Mint { + owner: CREATOR_ADDR.to_string(), + token_uri: Some("https://example.com".to_string()), + token_id: "1".to_string(), + extension: Empty {}, + }) + .unwrap()], + }, + ) .unwrap(), - to_binary(&sg721::ExecuteMsg::::UpdateCollectionInfo { - collection_info: UpdateCollectionInfoMsg:: { - description: None, - image: None, - external_link: None, - explicit_content: None, - royalty_info: None, + funds: funds.clone(), + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &funds, + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Factory message must serialize to WasmMsg::Execute")] +fn test_unsupported_factory_msg() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let cw721_id = app.store_code(cw721_base_contract()); + + // Instantiate using factory succeeds + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Instantiate { + code_id: cw721_id, + msg: to_binary(&dao_test_custom_factory::msg::ExecuteMsg::NftFactory { + code_id: cw721_id, + cw721_instantiate_msg: Cw721InstantiateMsg { + name: "Test NFT".to_string(), + symbol: "TEST".to_string(), + minter: CREATOR_ADDR.to_string(), }, + initial_nfts: vec![], }) .unwrap(), - ], - }, + admin: None, + label: "Test NFT".to_string(), + funds: vec![], + }) + .unwrap(), + ), unstaking_duration: None, - active_threshold: Some(ActiveThreshold::AbsoluteCount { - count: Uint128::new(2), + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic( + expected = "Error parsing into type dao_interface::nft::NftFactoryCallback: unknown field `denom`, expected `nft_contract`" +)] +fn test_factory_wrong_callback() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let _cw721_id = app.store_code(cw721_base_contract()); + let factory_id = app.store_code(dao_test_custom_factory()); + + // Instantiate factory + let factory_addr = app + .instantiate_contract( + factory_id, + Addr::unchecked(CREATOR_ADDR), + &dao_test_custom_factory::msg::InstantiateMsg {}, + &[], + "test factory".to_string(), + None, + ) + .unwrap(); + + // Instantiate using factory succeeds + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryWrongCallback {}, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), + }), + }, + &[], + "cw721_voting", + None, + ) + .unwrap(); +} + +#[test] +#[should_panic(expected = "Invalid reply from sub-message: Missing reply data")] +fn test_factory_no_callback() { + let mut app = App::default(); + let module_id = app.store_code(voting_cw721_staked_contract()); + let _cw721_id = app.store_code(cw721_base_contract()); + let factory_id = app.store_code(dao_test_custom_factory()); + + // Instantiate factory + let factory_addr = app + .instantiate_contract( + factory_id, + Addr::unchecked(CREATOR_ADDR), + &dao_test_custom_factory::msg::InstantiateMsg {}, + &[], + "test factory".to_string(), + None, + ) + .unwrap(); + + // Instantiate using factory succeeds + app.instantiate_contract( + module_id, + Addr::unchecked(CREATOR_ADDR), + &InstantiateMsg { + nft_contract: NftContract::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.to_string(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::NftFactoryNoCallback {}, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: None, + active_threshold: Some(ActiveThreshold::Percentage { + percent: Decimal::percent(1), }), }, &[], diff --git a/contracts/voting/dao-voting-token-staked/Cargo.toml b/contracts/voting/dao-voting-token-staked/Cargo.toml index 60d2b5c90..5a66b2eb3 100644 --- a/contracts/voting/dao-voting-token-staked/Cargo.toml +++ b/contracts/voting/dao-voting-token-staked/Cargo.toml @@ -41,11 +41,11 @@ cw-tokenfactory-issuer = { workspace = true, features = ["library"] } [dev-dependencies] anyhow = { workspace = true } -# TODO use upstream when new release is tagged -cw-multi-test = { git = "https://github.com/CosmWasm/cw-multi-test.git", rev = "d38db7752b9f054c395d6108453f8b321e4cab02" } +cw-multi-test = { workspace = true } cw-tokenfactory-issuer = { workspace = true } dao-proposal-single = { workspace = true } dao-proposal-hook-counter = { workspace = true } +dao-test-custom-factory = { workspace = true } dao-testing = { workspace = true, features = ["test-tube"] } osmosis-std = { workpsace = true } osmosis-test-tube = { workspace = true } diff --git a/contracts/voting/dao-voting-token-staked/README.md b/contracts/voting/dao-voting-token-staked/README.md index c9275c390..288a6cda8 100644 --- a/contracts/voting/dao-voting-token-staked/README.md +++ b/contracts/voting/dao-voting-token-staked/README.md @@ -80,3 +80,13 @@ Example insantiation mesggage: NOTE: if using an existing Token Factory token, double check the Token Factory admin and consider changing the Token Factory to be the DAO after the DAO is created. +### Use a factory +Occassionally, more customization is needed. Maybe you want to have an Augmented Bonding Curve contract or LP pool that requires additional setup? It's possible with factory contracts! + +The `factory` pattern takes a single `WasmMsg::Execute` message that calls into a custom factory contract. + +**NOTE:** when using the factory pattern, it is important to only use a trusted factory contract, as all validation happens in the factory contract. + +Those implementing custom factory contracts MUST handle any validation that is to happen, and the custom `WasmMsg::Execute` message MUST include `TokenFactoryCallback` data respectively. + +The [dao-test-custom-factory contract](../test/dao-test-custom-factory) provides an example of how this can be done and is used for tests. It is NOT production ready, but meant to serve as an example for building factory contracts. diff --git a/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json b/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json index b102a0e25..078fd00b0 100644 --- a/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json +++ b/contracts/voting/dao-voting-token-staked/schema/dao-voting-token-staked.json @@ -92,6 +92,10 @@ } ] }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, "Decimal": { "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" @@ -299,6 +303,19 @@ } }, "additionalProperties": false + }, + { + "description": "Uses a factory contract that must return the denom, optionally a Token Contract address. The binary must serialize to a `WasmMsg::Execute` message. Validation happens in the factory contract itself, so be sure to use a trusted factory contract.", + "type": "object", + "required": [ + "factory" + ], + "properties": { + "factory": { + "$ref": "#/definitions/Binary" + } + }, + "additionalProperties": false } ] }, @@ -659,6 +676,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "token_contract" + ], + "properties": { + "token_contract": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -753,19 +783,6 @@ } }, "additionalProperties": false - }, - { - "type": "object", - "required": [ - "token_contract" - ], - "properties": { - "token_contract": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false } ] }, @@ -1123,9 +1140,21 @@ }, "token_contract": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Addr", - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" + "title": "Nullable_Addr", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ], + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } }, "total_power_at_height": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/contracts/voting/dao-voting-token-staked/src/contract.rs b/contracts/voting/dao-voting-token-staked/src/contract.rs index b9d692348..93bb5c8e3 100644 --- a/contracts/voting/dao-voting-token-staked/src/contract.rs +++ b/contracts/voting/dao-voting-token-staked/src/contract.rs @@ -2,8 +2,8 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ - coins, to_binary, BankMsg, BankQuery, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Order, Reply, Response, StdResult, SubMsg, Uint128, Uint256, WasmMsg, + coins, from_binary, to_binary, BankMsg, BankQuery, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, + MessageInfo, Order, Reply, Response, StdResult, SubMsg, Uint128, Uint256, WasmMsg, }; use cw2::{get_contract_version, set_contract_version, ContractVersion}; use cw_controllers::ClaimsResponse; @@ -11,11 +11,14 @@ use cw_storage_plus::Bound; use cw_tokenfactory_issuer::msg::{ DenomUnit, ExecuteMsg as IssuerExecuteMsg, InstantiateMsg as IssuerInstantiateMsg, Metadata, }; -use cw_utils::{maybe_addr, must_pay, parse_reply_instantiate_data, Duration}; +use cw_utils::{ + maybe_addr, must_pay, parse_reply_execute_data, parse_reply_instantiate_data, Duration, +}; use dao_hooks::stake::{stake_hook_msgs, unstake_hook_msgs}; -use dao_interface::state::ModuleInstantiateCallback; -use dao_interface::voting::{ - IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse, +use dao_interface::{ + state::ModuleInstantiateCallback, + token::{InitialBalance, NewTokenInfo, TokenFactoryCallback}, + voting::{IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse}, }; use dao_voting::{ duration::validate_duration, @@ -27,8 +30,8 @@ use dao_voting::{ use crate::error::ContractError; use crate::msg::{ - DenomResponse, ExecuteMsg, GetHooksResponse, InitialBalance, InstantiateMsg, - ListStakersResponse, MigrateMsg, NewTokenInfo, QueryMsg, StakerBalanceResponse, TokenInfo, + DenomResponse, ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, + QueryMsg, StakerBalanceResponse, TokenInfo, }; use crate::state::{ Config, ACTIVE_THRESHOLD, CLAIMS, CONFIG, DAO, DENOM, HOOKS, MAX_CLAIMS, STAKED_BALANCES, @@ -43,6 +46,7 @@ const MAX_LIMIT: u32 = 30; const DEFAULT_LIMIT: u32 = 10; const INSTANTIATE_TOKEN_FACTORY_ISSUER_REPLY_ID: u64 = 0; +const FACTORY_EXECUTE_REPLY_ID: u64 = 2; // We multiply by this when calculating needed power for being active // when using active threshold with percent @@ -122,6 +126,29 @@ pub fn instantiate( .add_attribute("token", "new_token") .add_submessage(issuer_instantiate_msg)) } + TokenInfo::Factory(binary) => match from_binary(&binary)? { + WasmMsg::Execute { + msg, + contract_addr, + funds, + } => { + // Call factory contract. Use only a trusted factory contract, + // as this is a critical security component and valdiation of + // setup will happen in the factory. + Ok(Response::new() + .add_attribute("action", "intantiate") + .add_attribute("token", "custom_factory") + .add_submessage(SubMsg::reply_on_success( + WasmMsg::Execute { + contract_addr, + msg, + funds, + }, + FACTORY_EXECUTE_REPLY_ID, + ))) + } + _ => Err(ContractError::UnsupportedFactoryMsg {}), + }, } } @@ -380,7 +407,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::IsActive {} => query_is_active(deps), QueryMsg::ActiveThreshold {} => query_active_threshold(deps), QueryMsg::GetHooks {} => to_binary(&query_hooks(deps)?), - QueryMsg::TokenContract {} => to_binary(&TOKEN_ISSUER_CONTRACT.load(deps.storage)?), + QueryMsg::TokenContract {} => to_binary(&TOKEN_ISSUER_CONTRACT.may_load(deps.storage)?), } } @@ -687,14 +714,48 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result unreachable!(), } } + FACTORY_EXECUTE_REPLY_ID => { + // Parse reply + let res = parse_reply_execute_data(msg)?; + match res.data { + Some(data) => { + // Parse info from the callback, this will fail + // if incorrectly formatted. + let info: TokenFactoryCallback = from_binary(&data)?; + + // Save Denom + DENOM.save(deps.storage, &info.denom)?; + + // Save token issuer contract if one is returned + if let Some(ref token_contract) = info.token_contract { + TOKEN_ISSUER_CONTRACT + .save(deps.storage, &deps.api.addr_validate(token_contract)?)?; + } + + // Construct the response + let mut res = Response::new() + .add_attribute("denom", info.denom) + .add_attribute("token_contract", info.token_contract.unwrap_or_default()); + + // If a callback has been configured, set the module + // instantiate callback data. + if let Some(callback) = info.module_instantiate_callback { + res = res.set_data(to_binary(&callback)?); + } + + Ok(res) + } + None => Err(ContractError::NoFactoryCallback {}), + } + } _ => Err(ContractError::UnknownReplyId { id: msg.id }), } } diff --git a/contracts/voting/dao-voting-token-staked/src/error.rs b/contracts/voting/dao-voting-token-staked/src/error.rs index 27bea213a..7086f9bb6 100644 --- a/contracts/voting/dao-voting-token-staked/src/error.rs +++ b/contracts/voting/dao-voting-token-staked/src/error.rs @@ -29,6 +29,9 @@ pub enum ContractError { #[error("Can only unstake less than or equal to the amount you have staked")] InvalidUnstakeAmount {}, + #[error("Factory contract did not implment the required TokenFactoryCallback interface")] + NoFactoryCallback {}, + #[error("Nothing to claim")] NothingToClaim {}, @@ -41,6 +44,9 @@ pub enum ContractError { #[error("Got a submessage reply with unknown id: {id}")] UnknownReplyId { id: u64 }, + #[error("Factory message must serialize to WasmMsg::Execute")] + UnsupportedFactoryMsg {}, + #[error("Amount being unstaked must be non-zero")] ZeroUnstake {}, } diff --git a/contracts/voting/dao-voting-token-staked/src/msg.rs b/contracts/voting/dao-voting-token-staked/src/msg.rs index e08123680..52c71baa9 100644 --- a/contracts/voting/dao-voting-token-staked/src/msg.rs +++ b/contracts/voting/dao-voting-token-staked/src/msg.rs @@ -1,47 +1,10 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Uint128; -use cw_tokenfactory_issuer::msg::DenomUnit; +use cosmwasm_std::{Binary, Uint128}; use cw_utils::Duration; -use dao_dao_macros::{active_query, token_query, voting_module_query}; +use dao_dao_macros::{active_query, voting_module_query}; +use dao_interface::token::NewTokenInfo; use dao_voting::threshold::{ActiveThreshold, ActiveThresholdResponse}; -#[cw_serde] -pub struct InitialBalance { - pub amount: Uint128, - pub address: String, -} - -#[cw_serde] -pub struct NewDenomMetadata { - /// The name of the token (e.g. "Cat Coin") - pub name: String, - /// The description of the token - pub description: String, - /// The ticker symbol of the token (e.g. "CAT") - pub symbol: String, - /// The unit commonly used in communication (e.g. "cat") - pub display: String, - /// Used define additional units of the token (e.g. "tiger") - /// These must have an exponent larger than 0. - pub additional_denom_units: Option>, -} - -#[cw_serde] -pub struct NewTokenInfo { - /// The code id of the cw-tokenfactory-issuer contract - pub token_issuer_code_id: u64, - /// The subdenom of the token to create, will also be used as an alias - /// for the denom. The Token Factory denom will have the format of - /// factory/{contract_address}/{subdenom} - pub subdenom: String, - /// Optional metadata for the token, this can additionally be set later. - pub metadata: Option, - /// The initial balances to set for the token, cannot be empty. - pub initial_balances: Vec, - /// Optional balance to mint for the DAO. - pub initial_dao_balance: Option, -} - #[cw_serde] pub enum TokenInfo { /// Uses an existing Token Factory token and creates a new issuer contract. @@ -54,6 +17,11 @@ pub enum TokenInfo { /// Creates a new Token Factory token via the issue contract with the DAO automatically /// setup as admin and owner. New(NewTokenInfo), + /// Uses a factory contract that must return the denom, optionally a Token Contract address. + /// The binary must serialize to a `WasmMsg::Execute` message. + /// Validation happens in the factory contract itself, so be sure to use a + /// trusted factory contract. + Factory(Binary), } #[cw_serde] @@ -91,7 +59,6 @@ pub enum ExecuteMsg { #[active_query] #[voting_module_query] -#[token_query] #[cw_serde] #[derive(QueryResponses)] pub enum QueryMsg { @@ -110,6 +77,8 @@ pub enum QueryMsg { ActiveThreshold {}, #[returns(GetHooksResponse)] GetHooks {}, + #[returns(Option)] + TokenContract {}, } #[cw_serde] diff --git a/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs b/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs index ab56d26f1..3498764ec 100644 --- a/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs +++ b/contracts/voting/dao-voting-token-staked/src/tests/multitest/tests.rs @@ -1174,7 +1174,9 @@ fn test_update_active_threshold() { } #[test] -#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +#[should_panic( + expected = "Active threshold percentage must be greater than 0 and not greater than 1" +)] fn test_active_threshold_percentage_gt_100() { let mut app = App::default(); @@ -1195,7 +1197,9 @@ fn test_active_threshold_percentage_gt_100() { } #[test] -#[should_panic(expected = "Active threshold percentage must be greater than 0 and less than 1")] +#[should_panic( + expected = "Active threshold percentage must be greater than 0 and not greater than 1" +)] fn test_active_threshold_percentage_lte_0() { let mut app = App::default(); diff --git a/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs index 2c1db58f3..07ef96891 100644 --- a/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs +++ b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/integration_tests.rs @@ -1,16 +1,27 @@ -use cosmwasm_std::{Addr, Coin, Uint128}; -use cw_tokenfactory_issuer::msg::DenomUnit; -use dao_voting::threshold::{ActiveThreshold, ActiveThresholdError}; +use cosmwasm_std::{to_binary, Addr, Coin, Decimal, Uint128, WasmMsg}; +use cw_ownable::Ownership; +use cw_tokenfactory_issuer::msg::{DenomUnit, QueryMsg as IssuerQueryMsg}; +use cw_utils::Duration; +use dao_interface::{ + msg::QueryMsg as DaoQueryMsg, + state::{Admin, ModuleInstantiateInfo}, + token::{InitialBalance, NewDenomMetadata, NewTokenInfo}, +}; +use dao_testing::test_tube::{cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore}; +use dao_voting::{ + pre_propose::PreProposeInfo, + threshold::{ActiveThreshold, ActiveThresholdError, PercentageThreshold, Threshold}, +}; use osmosis_std::types::cosmos::bank::v1beta1::QueryBalanceRequest; -use osmosis_test_tube::{Account, OsmosisTestApp}; +use osmosis_test_tube::{Account, OsmosisTestApp, RunnerError}; use crate::{ - msg::{ExecuteMsg, InitialBalance, InstantiateMsg, NewDenomMetadata, NewTokenInfo, TokenInfo}, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg, TokenInfo}, tests::test_tube::test_env::TokenVotingContract, ContractError, }; -use super::test_env::{TestEnv, TestEnvBuilder}; +use super::test_env::{TestEnv, TestEnvBuilder, DENOM}; #[test] fn test_full_integration_correct_setup() { @@ -311,3 +322,411 @@ fn test_instantiate_no_initial_balances_fails() { TokenVotingContract::execute_submessage_error(ContractError::InitialBalancesError {}) ); } + +#[test] +fn test_factory() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + tf_issuer, + vp_contract, + proposal_single, + custom_factory, + accounts, + .. + } = env.full_dao_setup(&app); + + let factory_addr = custom_factory.unwrap().contract_addr.to_string(); + + // Instantiate a new voting contract using the factory pattern + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.clone(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactory( + NewTokenInfo { + token_issuer_code_id: tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: None, + initial_balances: vec![InitialBalance { + address: accounts[0].address(), + amount: Uint128::new(100), + }], + initial_dao_balance: None, + }, + ), + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.unwrap().code_id, + msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO succeeds + let dao = DaoCore::new(&app, &msg, &accounts[0], &vec![]).unwrap(); + + // Query voting module + let voting_module: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); + let voting = + TokenVotingContract::new_with_values(&app, vp_contract.code_id, voting_module.to_string()) + .unwrap(); + + // Query denom + let denom = voting.query_denom().unwrap().denom; + + // Query token contract + let token_contract: Addr = voting.query(&QueryMsg::TokenContract {}).unwrap(); + + // Check the TF denom is as expected + assert_eq!(denom, format!("factory/{}/{}", token_contract, DENOM)); + + // Check issuer ownership is the DAO and the ModuleInstantiateCallback + // has successfully accepted ownership. + let issuer = + TokenfactoryIssuer::new_with_values(&app, tf_issuer.code_id, token_contract.to_string()) + .unwrap(); + let ownership: Ownership = issuer.query(&IssuerQueryMsg::Ownership {}).unwrap(); + let owner = ownership.owner.unwrap(); + assert_eq!(owner, dao.contract_addr); +} + +#[test] +fn test_factory_funds_pass_through() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + tf_issuer, + vp_contract, + proposal_single, + custom_factory, + accounts, + .. + } = env.full_dao_setup(&app); + + let factory_addr = custom_factory.unwrap().contract_addr.to_string(); + + // Instantiate a new voting contract using the factory pattern + let mut msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.clone(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactoryWithFunds( + NewTokenInfo { + token_issuer_code_id: tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: None, + initial_balances: vec![InitialBalance { + address: accounts[0].address(), + amount: Uint128::new(100), + }], + initial_dao_balance: None, + }, + ), + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.unwrap().code_id, + msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO fails because no funds to create the token were sent + let err = DaoCore::new(&app, &msg, &accounts[0], &vec![]).unwrap_err(); + + // Check error is no funds sent + assert_eq!( + err, + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: dispatch: submessages: No funds sent: execute wasm contract failed".to_string(), + } + ); + + // Include funds in ModuleInstantiateInfo + let funds = vec![Coin { + denom: "uosmo".to_string(), + amount: Uint128::new(100), + }]; + msg.voting_module_instantiate_info = ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr, + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactoryWithFunds( + NewTokenInfo { + token_issuer_code_id: tf_issuer.code_id, + subdenom: DENOM.to_string(), + metadata: None, + initial_balances: vec![InitialBalance { + address: accounts[0].address(), + amount: Uint128::new(100), + }], + initial_dao_balance: None, + }, + ), + ) + .unwrap(), + funds: funds.clone(), + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: funds.clone(), + label: "DAO DAO Voting Module".to_string(), + }; + + // Creating the DAO now succeeds + DaoCore::new(&app, &msg, &accounts[0], &funds).unwrap(); +} + +#[test] +fn test_factory_no_callback() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + vp_contract, + proposal_single, + custom_factory, + accounts, + .. + } = env.full_dao_setup(&app); + + let factory_addr = custom_factory.unwrap().contract_addr.to_string(); + + // Instantiate a new voting contract using the factory pattern + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.clone(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactoryNoCallback{}, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.unwrap().code_id, + msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO fails because no callback + let err = DaoCore::new(&app, &msg, &accounts[0], &vec![]).unwrap_err(); + + // Check error is no reply data + assert_eq!( + err, + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: dispatch: submessages: reply: Invalid reply from sub-message: Missing reply data: execute wasm contract failed".to_string(), + } + ); +} + +#[test] +fn test_factory_wrong_callback() { + let app = OsmosisTestApp::new(); + let env = TestEnvBuilder::new(); + let TestEnv { + vp_contract, + proposal_single, + custom_factory, + accounts, + .. + } = env.full_dao_setup(&app); + + let factory_addr = custom_factory.unwrap().contract_addr.to_string(); + + // Instantiate a new voting contract using the factory pattern + let msg = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that makes DAO tooling".to_string(), + image_url: None, + automatically_add_cw20s: false, + automatically_add_cw721s: false, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: vp_contract.code_id, + msg: to_binary(&InstantiateMsg { + token_info: TokenInfo::Factory( + to_binary(&WasmMsg::Execute { + contract_addr: factory_addr.clone(), + msg: to_binary( + &dao_test_custom_factory::msg::ExecuteMsg::TokenFactoryFactoryWrongCallback{}, + ) + .unwrap(), + funds: vec![], + }) + .unwrap(), + ), + unstaking_duration: Some(Duration::Time(2)), + active_threshold: Some(ActiveThreshold::AbsoluteCount { + count: Uint128::new(75), + }), + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Voting Module".to_string(), + }, + proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { + code_id: proposal_single.unwrap().code_id, + msg: to_binary(&dao_proposal_single::msg::InstantiateMsg { + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Majority {}, + quorum: PercentageThreshold::Percent(Decimal::percent(35)), + }, + max_voting_period: Duration::Time(432000), + allow_revoting: false, + only_members_execute: true, + close_proposal_on_execution_failure: false, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + }) + .unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "DAO DAO Proposal Module".to_string(), + }], + initial_items: None, + }; + + // Instantiate DAO fails because of wrong callback + let err = DaoCore::new(&app, &msg, &accounts[0], &vec![]).unwrap_err(); + + // Check error is wrong reply type + assert_eq!( + err, + RunnerError::ExecuteError { + msg: "failed to execute message; message index: 0: dispatch: submessages: dispatch: submessages: reply: Error parsing into type dao_interface::token::TokenFactoryCallback: unknown field `nft_contract`, expected one of `denom`, `token_contract`, `module_instantiate_callback`: execute wasm contract failed".to_string(), + } + ); +} diff --git a/contracts/voting/dao-voting-token-staked/src/tests/test_tube/test_env.rs b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/test_env.rs index 5287cec34..a9916c48d 100644 --- a/contracts/voting/dao-voting-token-staked/src/tests/test_tube/test_env.rs +++ b/contracts/voting/dao-voting-token-staked/src/tests/test_tube/test_env.rs @@ -3,7 +3,7 @@ #![allow(dead_code)] use crate::{ - msg::{ExecuteMsg, InitialBalance, InstantiateMsg, NewTokenInfo, QueryMsg, TokenInfo}, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg, TokenInfo}, ContractError, }; @@ -13,6 +13,7 @@ use cw_utils::Duration; use dao_interface::{ msg::QueryMsg as DaoQueryMsg, state::{Admin, ModuleInstantiateInfo, ProposalModule}, + token::{InitialBalance, NewDenomMetadata, NewTokenInfo}, voting::{IsActiveResponse, VotingPowerAtHeightResponse}, }; use dao_voting::{ @@ -21,7 +22,7 @@ use dao_voting::{ use dao_testing::test_tube::{ cw_tokenfactory_issuer::TokenfactoryIssuer, dao_dao_core::DaoCore, - dao_proposal_single::DaoProposalSingle, + dao_proposal_single::DaoProposalSingle, dao_test_custom_factory::CustomFactoryContract, }; use dao_voting::threshold::ActiveThreshold; use osmosis_std::types::{ @@ -41,6 +42,7 @@ pub struct TestEnv<'a> { pub app: &'a OsmosisTestApp, pub dao: Option>, pub proposal_single: Option>, + pub custom_factory: Option>, pub vp_contract: TokenVotingContract<'a>, pub tf_issuer: TokenfactoryIssuer<'a>, pub accounts: Vec, @@ -135,7 +137,7 @@ impl TestEnvBuilder { token_info: TokenInfo::New(NewTokenInfo { token_issuer_code_id: issuer_id, subdenom: DENOM.to_string(), - metadata: Some(crate::msg::NewDenomMetadata { + metadata: Some(NewDenomMetadata { description: "Awesome token, get it meow!".to_string(), additional_denom_units: Some(vec![DenomUnit { denom: "cat".to_string(), @@ -168,6 +170,7 @@ impl TestEnvBuilder { accounts, dao: None, proposal_single: None, + custom_factory: None, tf_issuer, vp_contract, } @@ -206,7 +209,7 @@ impl TestEnvBuilder { token_info: TokenInfo::New(NewTokenInfo { token_issuer_code_id: issuer_id, subdenom: DENOM.to_string(), - metadata: Some(crate::msg::NewDenomMetadata { + metadata: Some(NewDenomMetadata { description: "Awesome token, get it meow!".to_string(), additional_denom_units: Some(vec![DenomUnit { denom: "cat".to_string(), @@ -227,6 +230,7 @@ impl TestEnvBuilder { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Voting Module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { @@ -245,13 +249,14 @@ impl TestEnvBuilder { }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO Proposal Module".to_string(), }], initial_items: None, }; // Instantiate DAO - let dao = DaoCore::new(app, &msg, &accounts[0]).unwrap(); + let dao = DaoCore::new(app, &msg, &accounts[0], &vec![]).unwrap(); // Get voting module address, setup vp_contract helper let vp_addr: Addr = dao.query(&DaoQueryMsg::VotingModule {}).unwrap(); @@ -277,11 +282,20 @@ impl TestEnvBuilder { TokenVotingContract::query(&vp_contract, &QueryMsg::TokenContract {}).unwrap(); let tf_issuer = TokenfactoryIssuer::new_with_values(app, issuer_id, issuer_addr).unwrap(); + // Instantiate Custom Factory + let custom_factory = CustomFactoryContract::new( + app, + &dao_test_custom_factory::msg::InstantiateMsg {}, + &accounts[0], + ) + .unwrap(); + TestEnv { app, dao: Some(dao), vp_contract, proposal_single: Some(proposal_single), + custom_factory: Some(custom_factory), tf_issuer, accounts, } diff --git a/justfile b/justfile index 1aff4f8ad..5ceec3503 100644 --- a/justfile +++ b/justfile @@ -59,14 +59,14 @@ workspace-optimize: --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --platform linux/arm64 \ - cosmwasm/workspace-optimizer-arm64:0.13.0; \ + cosmwasm/workspace-optimizer-arm64:0.14.0; \ elif [[ $(uname -m) == 'aarch64' ]]; then docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --platform linux/arm64 \ - cosmwasm/workspace-optimizer-arm64:0.13.0; \ + cosmwasm/workspace-optimizer-arm64:0.14.0; \ elif [[ $(uname -m) == 'x86_64' ]]; then docker run --rm -v "$(pwd)":/code \ --mount type=volume,source="$(basename "$(pwd)")_cache",target=/target \ --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ --platform linux/amd64 \ - cosmwasm/workspace-optimizer:0.13.0; fi + cosmwasm/workspace-optimizer:0.14.0; fi diff --git a/packages/dao-interface/Cargo.toml b/packages/dao-interface/Cargo.toml index c82abc5e4..f05475090 100644 --- a/packages/dao-interface/Cargo.toml +++ b/packages/dao-interface/Cargo.toml @@ -15,6 +15,7 @@ cw20 = { workspace = true } cw721 = { workspace = true } cw-hooks = { workspace = true } cw-utils = { workspace = true } +osmosis-std = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } diff --git a/packages/dao-interface/src/lib.rs b/packages/dao-interface/src/lib.rs index f46f381c2..aae0678fd 100644 --- a/packages/dao-interface/src/lib.rs +++ b/packages/dao-interface/src/lib.rs @@ -2,7 +2,9 @@ pub mod migrate_msg; pub mod msg; +pub mod nft; pub mod proposal; pub mod query; pub mod state; +pub mod token; pub mod voting; diff --git a/packages/dao-interface/src/nft.rs b/packages/dao-interface/src/nft.rs new file mode 100644 index 000000000..82e7da52e --- /dev/null +++ b/packages/dao-interface/src/nft.rs @@ -0,0 +1,9 @@ +use cosmwasm_schema::cw_serde; + +use crate::state::ModuleInstantiateCallback; + +#[cw_serde] +pub struct NftFactoryCallback { + pub nft_contract: String, + pub module_instantiate_callback: Option, +} diff --git a/packages/dao-interface/src/state.rs b/packages/dao-interface/src/state.rs index 3a0841f9b..0bd445de3 100644 --- a/packages/dao-interface/src/state.rs +++ b/packages/dao-interface/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Binary, CosmosMsg, WasmMsg}; +use cosmwasm_std::{Addr, Binary, Coin, CosmosMsg, WasmMsg}; /// Top level config type for core module. #[cw_serde] @@ -60,6 +60,8 @@ pub struct ModuleInstantiateInfo { /// CosmWasm level admin of the instantiated contract. See: /// pub admin: Option, + /// Funds to be sent to the instantiated contract. + pub funds: Vec, /// Label for the instantiated contract. pub label: String, } @@ -73,7 +75,7 @@ impl ModuleInstantiateInfo { }), code_id: self.code_id, msg: self.msg, - funds: vec![], + funds: self.funds, label: self.label, } } @@ -98,6 +100,7 @@ mod tests { msg: to_binary("foo").unwrap(), admin: None, label: "bar".to_string(), + funds: vec![], }; assert_eq!( no_admin.into_wasm_msg(Addr::unchecked("ekez")), @@ -120,6 +123,7 @@ mod tests { addr: "core".to_string(), }), label: "bar".to_string(), + funds: vec![], }; assert_eq!( no_admin.into_wasm_msg(Addr::unchecked("ekez")), @@ -140,6 +144,7 @@ mod tests { msg: to_binary("foo").unwrap(), admin: Some(Admin::CoreModule {}), label: "bar".to_string(), + funds: vec![], }; assert_eq!( no_admin.into_wasm_msg(Addr::unchecked("ekez")), diff --git a/packages/dao-interface/src/token.rs b/packages/dao-interface/src/token.rs new file mode 100644 index 000000000..c735a15c4 --- /dev/null +++ b/packages/dao-interface/src/token.rs @@ -0,0 +1,52 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint128; + +// These are Cosmos Proto types used for Denom Metadata. +// We re-export them here for convenience. +pub use osmosis_std::types::cosmos::bank::v1beta1::{DenomUnit, Metadata}; + +use crate::state::ModuleInstantiateCallback; + +#[cw_serde] +pub struct InitialBalance { + pub amount: Uint128, + pub address: String, +} + +#[cw_serde] +pub struct NewDenomMetadata { + /// The name of the token (e.g. "Cat Coin") + pub name: String, + /// The description of the token + pub description: String, + /// The ticker symbol of the token (e.g. "CAT") + pub symbol: String, + /// The unit commonly used in communication (e.g. "cat") + pub display: String, + /// Used define additional units of the token (e.g. "tiger") + /// These must have an exponent larger than 0. + pub additional_denom_units: Option>, +} + +#[cw_serde] +pub struct NewTokenInfo { + /// The code id of the cw-tokenfactory-issuer contract + pub token_issuer_code_id: u64, + /// The subdenom of the token to create, will also be used as an alias + /// for the denom. The Token Factory denom will have the format of + /// factory/{contract_address}/{subdenom} + pub subdenom: String, + /// Optional metadata for the token, this can additionally be set later. + pub metadata: Option, + /// The initial balances to set for the token, cannot be empty. + pub initial_balances: Vec, + /// Optional balance to mint for the DAO. + pub initial_dao_balance: Option, +} + +#[cw_serde] +pub struct TokenFactoryCallback { + pub denom: String, + pub token_contract: Option, + pub module_instantiate_callback: Option, +} diff --git a/packages/dao-interface/src/voting.rs b/packages/dao-interface/src/voting.rs index f2df11a44..cf6d43707 100644 --- a/packages/dao-interface/src/voting.rs +++ b/packages/dao-interface/src/voting.rs @@ -30,6 +30,11 @@ pub enum Query { IsActive {}, } +#[cw_serde] +pub enum ActiveThresholdQuery { + ActiveThreshold {}, +} + #[cw_serde] pub struct VotingPowerAtHeightResponse { pub power: Uint128, diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index 0416bdc84..9698f9e24 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -49,6 +49,7 @@ dao-pre-propose-multiple = { workspace = true } dao-pre-propose-single = { workspace = true } dao-proposal-condorcet = { workspace = true } dao-proposal-single = { workspace = true } +dao-test-custom-factory = { workspace = true } dao-voting = { workspace = true } dao-voting-cw20-balance = { workspace = true } dao-voting-cw20-staked = { workspace = true } diff --git a/packages/dao-testing/src/contracts.rs b/packages/dao-testing/src/contracts.rs index b2fb42b27..a0418a48f 100644 --- a/packages/dao-testing/src/contracts.rs +++ b/packages/dao-testing/src/contracts.rs @@ -196,3 +196,13 @@ pub fn stake_cw20_v03_contract() -> Box> { ); Box::new(contract) } + +pub fn dao_test_custom_factory() -> Box> { + let contract = ContractWrapper::new( + dao_test_custom_factory::contract::execute, + dao_test_custom_factory::contract::instantiate, + dao_test_custom_factory::contract::query, + ) + .with_reply(dao_test_custom_factory::contract::reply); + Box::new(contract) +} diff --git a/packages/dao-testing/src/helpers.rs b/packages/dao-testing/src/helpers.rs index ed95ea770..9bccd53c7 100644 --- a/packages/dao-testing/src/helpers.rs +++ b/packages/dao-testing/src/helpers.rs @@ -70,12 +70,14 @@ pub fn instantiate_with_cw20_balances_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: governance_code_id, msg: governance_instantiate, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -153,6 +155,7 @@ pub fn instantiate_with_staked_balances_governance( }) .unwrap(), admin: None, + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { @@ -160,6 +163,7 @@ pub fn instantiate_with_staked_balances_governance( label: "DAO DAO governance module.".to_string(), admin: Some(Admin::CoreModule {}), msg: governance_instantiate, + funds: vec![], }], initial_items: None, }; @@ -272,12 +276,14 @@ pub fn instantiate_with_staking_active_threshold( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id, msg: governance_instantiate, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, @@ -344,12 +350,14 @@ pub fn instantiate_with_cw4_groups_governance( }) .unwrap(), admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO voting module".to_string(), }, proposal_modules_instantiate_info: vec![ModuleInstantiateInfo { code_id: core_code_id, msg: proposal_module_instantiate, admin: Some(Admin::CoreModule {}), + funds: vec![], label: "DAO DAO governance module".to_string(), }], initial_items: None, diff --git a/packages/dao-testing/src/test_tube/cw721_base.rs b/packages/dao-testing/src/test_tube/cw721_base.rs new file mode 100644 index 000000000..84c80b8fe --- /dev/null +++ b/packages/dao-testing/src/test_tube/cw721_base.rs @@ -0,0 +1,128 @@ +use cosmwasm_std::{Coin, Empty}; +use cw721_base::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct Cw721Base<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> Cw721Base<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("cw721_base.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("cw721_base-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/dao_dao_core.rs b/packages/dao-testing/src/test_tube/dao_dao_core.rs index 0b9f6752f..3fc9b73e7 100644 --- a/packages/dao-testing/src/test_tube/dao_dao_core.rs +++ b/packages/dao-testing/src/test_tube/dao_dao_core.rs @@ -21,10 +21,9 @@ impl<'a> DaoCore<'a> { app: &'a OsmosisTestApp, instantiate_msg: &InstantiateMsg, signer: &SigningAccount, + funds: &[Coin], ) -> Result { let wasm = Wasm::new(app); - let token_creation_fee = Coin::new(10000000, "uosmo"); - let code_id = wasm .store_code(&Self::get_wasm_byte_code(), None, signer)? .data @@ -36,7 +35,7 @@ impl<'a> DaoCore<'a> { &instantiate_msg, Some(&signer.address()), None, - &[token_creation_fee], + funds, signer, )? .data diff --git a/packages/dao-testing/src/test_tube/dao_test_custom_factory.rs b/packages/dao-testing/src/test_tube/dao_test_custom_factory.rs new file mode 100644 index 000000000..754374884 --- /dev/null +++ b/packages/dao-testing/src/test_tube/dao_test_custom_factory.rs @@ -0,0 +1,129 @@ +use cosmwasm_std::Coin; +use dao_test_custom_factory::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + ContractError, +}; +use osmosis_test_tube::{ + osmosis_std::types::cosmwasm::wasm::v1::MsgExecuteContractResponse, Account, Module, + OsmosisTestApp, RunnerError, RunnerExecuteResult, SigningAccount, Wasm, +}; +use serde::de::DeserializeOwned; +use std::fmt::Debug; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct CustomFactoryContract<'a> { + pub app: &'a OsmosisTestApp, + pub code_id: u64, + pub contract_addr: String, +} + +impl<'a> CustomFactoryContract<'a> { + pub fn new( + app: &'a OsmosisTestApp, + instantiate_msg: &InstantiateMsg, + signer: &SigningAccount, + ) -> Result { + let wasm = Wasm::new(app); + let token_creation_fee = Coin::new(10000000, "uosmo"); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + let contract_addr = wasm + .instantiate( + code_id, + &instantiate_msg, + Some(&signer.address()), + None, + &[token_creation_fee], + signer, + )? + .data + .address; + + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + pub fn new_with_values( + app: &'a OsmosisTestApp, + code_id: u64, + contract_addr: String, + ) -> Result { + Ok(Self { + app, + code_id, + contract_addr, + }) + } + + /// uploads contract and returns a code ID + pub fn upload(app: &OsmosisTestApp, signer: &SigningAccount) -> Result { + let wasm = Wasm::new(app); + + let code_id = wasm + .store_code(&Self::get_wasm_byte_code(), None, signer)? + .data + .code_id; + + Ok(code_id) + } + + // executes + pub fn execute( + &self, + execute_msg: &ExecuteMsg, + funds: &[Coin], + signer: &SigningAccount, + ) -> RunnerExecuteResult { + let wasm = Wasm::new(self.app); + wasm.execute(&self.contract_addr, execute_msg, funds, signer) + } + + // queries + pub fn query(&self, query_msg: &QueryMsg) -> Result + where + T: DeserializeOwned, + { + let wasm = Wasm::new(self.app); + wasm.query(&self.contract_addr, query_msg) + } + + fn get_wasm_byte_code() -> Vec { + let manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let byte_code = std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_test_custom_factory.wasm"), + ); + match byte_code { + Ok(byte_code) => byte_code, + // On arm processors, the above path is not found, so we try the following path + Err(_) => std::fs::read( + manifest_path + .join("..") + .join("..") + .join("artifacts") + .join("dao_test_custom_factory-aarch64.wasm"), + ) + .unwrap(), + } + } + + pub fn execute_error(err: ContractError) -> RunnerError { + RunnerError::ExecuteError { + msg: format!( + "failed to execute message; message index: 0: {}: execute wasm contract failed", + err + ), + } + } +} diff --git a/packages/dao-testing/src/test_tube/mod.rs b/packages/dao-testing/src/test_tube/mod.rs index a5f997792..0af2999a4 100644 --- a/packages/dao-testing/src/test_tube/mod.rs +++ b/packages/dao-testing/src/test_tube/mod.rs @@ -8,8 +8,14 @@ #[cfg(feature = "test-tube")] pub mod cw_tokenfactory_issuer; +#[cfg(feature = "test-tube")] +pub mod cw721_base; + #[cfg(feature = "test-tube")] pub mod dao_dao_core; #[cfg(feature = "test-tube")] pub mod dao_proposal_single; + +#[cfg(feature = "test-tube")] +pub mod dao_test_custom_factory; diff --git a/packages/dao-voting/src/pre_propose.rs b/packages/dao-voting/src/pre_propose.rs index 52bd3e7a5..7671425a3 100644 --- a/packages/dao-voting/src/pre_propose.rs +++ b/packages/dao-voting/src/pre_propose.rs @@ -116,6 +116,7 @@ mod tests { code_id: 42, msg: to_binary("foo").unwrap(), admin: None, + funds: vec![], label: "pre-propose-9000".to_string(), }, }; diff --git a/packages/dao-voting/src/threshold.rs b/packages/dao-voting/src/threshold.rs index e27e46c7b..bbb2668cb 100644 --- a/packages/dao-voting/src/threshold.rs +++ b/packages/dao-voting/src/threshold.rs @@ -28,7 +28,7 @@ pub enum ActiveThresholdError { #[error("Absolute count threshold cannot be greater than the total token supply")] InvalidAbsoluteCount {}, - #[error("Active threshold percentage must be greater than 0 and less than 1")] + #[error("Active threshold percentage must be greater than 0 and not greater than 1")] InvalidActivePercentage {}, #[error("Active threshold count must be greater than zero")]