diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 42af0ab81..66119a9f8 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -21,7 +21,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly-2023-02-02 + toolchain: nightly target: wasm32-unknown-unknown override: true diff --git a/.github/workflows/test_tube.yml b/.github/workflows/test_tube.yml index 3b1a584cb..909a8c839 100644 --- a/.github/workflows/test_tube.yml +++ b/.github/workflows/test_tube.yml @@ -17,6 +17,19 @@ jobs: - name: Checkout sources uses: actions/checkout@v3 + # https://github.com/orgs/community/discussions/25678 + - name: Free up disk space on runner + run: | + df -h + rm -rf /usr/share/dotnet/ + sudo apt-get remove -y 'php.*' + sudo apt-get remove -y '^dotnet-.*' + sudo apt-get remove -y azure-cli firefox powershell mono-devel + sudo apt-get autoremove -y + sudo apt-get clean + df -h + shell: bash + - name: Install latest nightly toolchain uses: actions-rs/toolchain@v1 with: diff --git a/Cargo.lock b/Cargo.lock index b1e7d67d1..0766e9ed9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,7 +68,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -79,7 +79,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -203,7 +203,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.72", + "syn 2.0.74", "which", ] @@ -333,18 +333,21 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" dependencies = [ "serde", ] [[package]] name = "cc" -version = "1.1.6" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" +dependencies = [ + "shlex", +] [[package]] name = "cexpr" @@ -418,9 +421,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cosm-orc" @@ -533,32 +536,31 @@ dependencies = [ [[package]] name = "cosmwasm-crypto" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd50718a2b6830ce9eb5d465de5a018a12e71729d66b70807ce97e6dd14f931d" +checksum = "0f862b355f7e47711e0acfe6af92cb3fd8fd5936b66a9eaa338b51edabd1e77d" dependencies = [ "digest 0.10.7", - "ecdsa 0.16.9", "ed25519-zebra", - "k256 0.13.1", + "k256 0.13.3", "rand_core 0.6.4", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "242e98e7a231c122e08f300d9db3262d1007b51758a8732cd6210b3e9faa4f3a" +checksum = "cd85de6467cd1073688c86b39833679ae6db18cf4771471edd9809f15f1679f1" dependencies = [ "syn 1.0.109", ] [[package]] name = "cosmwasm-schema" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7879036156092ad1c22fe0d7316efc5a5eceec2bc3906462a2560215f2a2f929" +checksum = "5b4cd28147a66eba73720b47636a58097a979ad8c8bfdb4ed437ebcbfe362576" dependencies = [ "cosmwasm-schema-derive", "schemars", @@ -569,9 +571,9 @@ dependencies = [ [[package]] name = "cosmwasm-schema-derive" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb57855fbfc83327f8445ae0d413b1a05ac0d68c396ab4d122b2abd7bb82cb6" +checksum = "9acd45c63d41bc9b16bc6dc7f6bd604a8c2ad29ce96c8f3c96d7fc8ef384392e" dependencies = [ "proc-macro2", "quote", @@ -580,9 +582,9 @@ dependencies = [ [[package]] name = "cosmwasm-std" -version = "1.5.5" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c1556156fdf892a55cced6115968b961eaaadd6f724a2c2cb7d1e168e32dd3" +checksum = "2685c2182624b2e9e17f7596192de49a3f86b7a0c9a5f6b25c1df5e24592e836" dependencies = [ "base64 0.21.7", "bech32", @@ -612,9 +614,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" dependencies = [ "libc", ] @@ -2059,6 +2061,30 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-proposal-incentives" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.5.0", + "cw-hooks 2.5.0", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "dao-dao-core 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-proposal-single 2.5.0", + "dao-testing", + "dao-voting 2.5.0", + "thiserror", +] + [[package]] name = "dao-proposal-multiple" version = "2.4.1" @@ -2273,6 +2299,7 @@ dependencies = [ "dao-pre-propose-multiple 2.5.0", "dao-pre-propose-single 2.5.0", "dao-proposal-condorcet", + "dao-proposal-incentives", "dao-proposal-single 2.5.0", "dao-test-custom-factory", "dao-voting 0.1.0", @@ -2282,6 +2309,7 @@ dependencies = [ "dao-voting-cw4 2.5.0", "dao-voting-cw721-roles", "dao-voting-cw721-staked", + "dao-voting-incentives", "dao-voting-onft-staked", "dao-voting-token-staked", "osmosis-std", @@ -2461,6 +2489,30 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-voting-incentives" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-denom 2.5.0", + "cw-hooks 2.5.0", + "cw-multi-test", + "cw-ownable", + "cw-storage-plus 1.2.0", + "cw-utils 1.0.3", + "cw2 1.1.2", + "cw20 1.1.2", + "cw20-base 1.1.2", + "dao-dao-core 2.5.0", + "dao-hooks 2.5.0", + "dao-interface 2.5.0", + "dao-proposal-single 2.5.0", + "dao-testing", + "dao-voting 2.5.0", + "thiserror", +] + [[package]] name = "dao-voting-onft-staked" version = "2.5.0" @@ -2846,7 +2898,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -2949,7 +3001,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.2.6", + "indexmap 2.4.0", "slab", "tokio", "tokio-util", @@ -3001,6 +3053,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -3179,9 +3237,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.6" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" dependencies = [ "equivalent", "hashbrown 0.14.5", @@ -3223,11 +3281,11 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ - "hermit-abi", + "hermit-abi 0.4.0", "libc", "windows-sys 0.52.0", ] @@ -3241,6 +3299,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 = "itertools" version = "0.12.1" @@ -3258,9 +3325,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] @@ -3291,9 +3358,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if", "ecdsa 0.16.9", @@ -3326,9 +3393,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.156" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" [[package]] name = "libloading" @@ -3337,7 +3404,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets", ] [[package]] @@ -3393,13 +3460,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -3432,16 +3500,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_threads" version = "0.1.7" @@ -3453,9 +3511,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.1" +version = "0.36.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" +checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" dependencies = [ "memchr", ] @@ -3655,7 +3713,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3686,7 +3744,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3723,9 +3781,12 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "prettyplease" @@ -3734,7 +3795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3798,10 +3859,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -3869,9 +3930,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -4074,7 +4135,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4146,9 +4207,9 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" dependencies = [ "serde_derive", ] @@ -4182,13 +4243,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.208" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4199,16 +4260,17 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] name = "serde_json" -version = "1.0.120" +version = "1.0.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -4221,7 +4283,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4230,7 +4292,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.2.6", + "indexmap 2.4.0", "itoa", "ryu", "serde", @@ -4445,9 +4507,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -4698,7 +4760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d506c7664333e246f564949bee4ed39062aa0f11918e6f5a95f553cdad65c274" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4734,7 +4796,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4771,19 +4833,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.1" +version = "1.39.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" +checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -4798,13 +4859,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -4933,15 +4994,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -4962,7 +5023,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] [[package]] @@ -5054,9 +5115,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vote-hooks" @@ -5097,34 +5158,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5132,28 +5194,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -5208,11 +5270,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5221,37 +5283,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -5260,46 +5307,28 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5312,48 +5341,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5381,6 +5386,27 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.74", +] + [[package]] name = "zeroize" version = "1.8.1" @@ -5398,5 +5424,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.74", ] diff --git a/Cargo.toml b/Cargo.toml index 1d98d3a94..3e302b79f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -111,6 +111,7 @@ dao-pre-propose-multiple = { path = "./contracts/pre-propose/dao-pre-propose-mul dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single", version = "2.5.0" } dao-proposal-condorcet = { path = "./contracts/proposal/dao-proposal-condorcet", version = "2.5.0" } dao-proposal-hook-counter = { path = "./contracts/test/dao-proposal-hook-counter", version = "2.5.0" } +dao-proposal-incentives = { path = "./contracts/external/dao-proposal-incentives", version = "2.5.0" } dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "2.5.0" } dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "2.5.0" } dao-proposal-sudo = { path = "./contracts/test/dao-proposal-sudo", version = "2.5.0" } @@ -123,6 +124,7 @@ dao-voting-cw20-staked = { path = "./contracts/voting/dao-voting-cw20-staked", v dao-voting-cw4 = { path = "./contracts/voting/dao-voting-cw4", version = "2.5.0" } dao-voting-cw721-roles = { path = "./contracts/voting/dao-voting-cw721-roles", version = "2.5.0" } dao-voting-cw721-staked = { path = "./contracts/voting/dao-voting-cw721-staked", version = "2.5.0" } +dao-voting-incentives = { path = "./contracts/external/dao-voting-incentives", version = "2.5.0" } dao-voting-onft-staked = { path = "./contracts/voting/dao-voting-onft-staked", version = "2.5.0" } dao-voting-token-staked = { path = "./contracts/voting/dao-voting-token-staked", version = "2.5.0" } diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index b1cca4f3d..c894c9f69 100644 --- a/contracts/dao-dao-core/schema/dao-dao-core.json +++ b/contracts/dao-dao-core/schema/dao-dao-core.json @@ -1796,6 +1796,27 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "proposal_module" + ], + "properties": { + "proposal_module": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Gets the active proposal modules associated with the contract.", "type": "object", @@ -3033,6 +3054,54 @@ } } }, + "proposal_module": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ProposalModule", + "description": "Top level type describing a proposal module.", + "type": "object", + "required": [ + "address", + "prefix", + "status" + ], + "properties": { + "address": { + "description": "The address of the proposal module.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "prefix": { + "description": "The URL prefix of this proposal module as derived from the module ID. Prefixes are mapped to letters, e.g. 0 is 'A', and 26 is 'AA'.", + "type": "string" + }, + "status": { + "description": "The status of the proposal module, e.g. 'Enabled' or 'Disabled.'", + "allOf": [ + { + "$ref": "#/definitions/ProposalModuleStatus" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "ProposalModuleStatus": { + "description": "The status of a proposal module.", + "type": "string", + "enum": [ + "enabled", + "disabled" + ] + } + } + }, "proposal_module_count": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "ProposalModuleCountResponse", diff --git a/contracts/dao-dao-core/src/contract.rs b/contracts/dao-dao-core/src/contract.rs index 1ee52a165..8fc57df32 100644 --- a/contracts/dao-dao-core/src/contract.rs +++ b/contracts/dao-dao-core/src/contract.rs @@ -582,6 +582,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ProposalModules { start_after, limit } => { query_proposal_modules(deps, start_after, limit) } + QueryMsg::ProposalModule { address } => query_proposal_module(deps, address), QueryMsg::ProposalModuleCount {} => query_proposal_module_count(deps), QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, height), QueryMsg::VotingModule {} => query_voting_module(deps), @@ -598,6 +599,13 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } } +pub fn query_proposal_module(deps: Deps, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + let proposal_module = &PROPOSAL_MODULES.load(deps.storage, address)?; + + to_json_binary(&proposal_module) +} + pub fn query_admin(deps: Deps) -> StdResult { let admin = ADMIN.load(deps.storage)?; to_json_binary(&admin) diff --git a/contracts/external/dao-proposal-incentives/.cargo/config b/contracts/external/dao-proposal-incentives/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/external/dao-proposal-incentives/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/external/dao-proposal-incentives/Cargo.toml b/contracts/external/dao-proposal-incentives/Cargo.toml new file mode 100644 index 000000000..5831b3c1b --- /dev/null +++ b/contracts/external/dao-proposal-incentives/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name ="dao-proposal-incentives" +authors = ["Jake Hartnell ", "Gabe "] +description = "A contract that implements incentives for creating a successful proposal in a DAO." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +dao-hooks = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } +thiserror = { workspace = true } +cw-utils = { workspace = true } +cw-denom = { workspace = true } +cw-ownable = { workspace = true } +cw20 = { workspace = true } + +[dev-dependencies] +cosmwasm-schema = { workspace = true } +cw-multi-test = { workspace = true } +dao-dao-core = { workspace = true, features = ["library"] } +cw20-base = { workspace = true, features = ["library"] } +dao-testing = { workspace = true } +dao-proposal-single = { workspace = true, features = ["library"] } +cw-hooks = { workspace = true } \ No newline at end of file diff --git a/contracts/external/dao-proposal-incentives/README.md b/contracts/external/dao-proposal-incentives/README.md new file mode 100644 index 000000000..cf8732d3d --- /dev/null +++ b/contracts/external/dao-proposal-incentives/README.md @@ -0,0 +1,60 @@ +# dao-proposal-incentives + +> **WARNING:** THIS CONTRACT IS NOT AUDITED AND IS EXPERIMENTAL. USE AT YOUR OWN RISK. + +## Overview + +The `dao-proposal-incentives` contract empowers DAOs to boost member engagement by automatically rewarding successful proposals. This approach encourages active participation and high-quality contributions to DAO governance. + +### Key Features + +- Automatic rewards for passed proposals +- Support for both native and CW20 tokens +- Dynamic incentive adjustment +- Seamless integration with existing DAO modules + +## How It Works + +1. **Setup**: The DAO instantiates the contract and sets initial reward parameters. +2. **Funding**: The contract is funded with tokens for rewards. +2. **Integration**: The contract is added as a proposal hook to the DAO's voting module. +3. **Proposal Lifecycle**: When a proposal passes, the contract automatically rewards the proposer. +4. **Flexible Management**: The DAO can adjust reward amounts and token types as needed. + +## Usage Guide + +### Instantiation + +To set up the contract, provide: + +- `owner`: The DAO's address (for sending proposal hooks) +- `proposal_incentives`: Reward configuration using `ProposalIncentivesUnchecked` + +Example: +```rust +let msg = InstantiateMsg { + owner: "dao_address".to_string(), + proposal_incentives: ProposalIncentivesUnchecked { + rewards_per_proposal: Uint128::new(1000), + denom: UncheckedDenom::Native("ujuno".to_string()), + }, +}; +``` + +### Configuration + +1. Add this contract as a `ProposalHook` to your DAO's voting module (`dao-voting-single` or `dao-voting-multiple`). +2. Ensure the DAO is set as the contract `owner` for proper management. + +### Key Functions + +#### Execute Messages + +1. **ProposalHook(ProposalHookMsg)**: Handles proposal status changes and reward distribution. +2. **UpdateOwnership(cw_ownable::Action)**: Manages contract ownership. +3. **UpdateProposalIncentives**: Allows the DAO to modify reward settings. +4. **Receive(Cw20ReceiveMsg)**: Processes incoming CW20 tokens for rewards. + +#### Query Messages + +- **ProposalIncentives { height: Option }**: Retrieves current or historical incentive configurations. diff --git a/contracts/external/dao-proposal-incentives/examples/schema.rs b/contracts/external/dao-proposal-incentives/examples/schema.rs new file mode 100644 index 000000000..faa86b601 --- /dev/null +++ b/contracts/external/dao-proposal-incentives/examples/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use dao_proposal_incentives::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/external/dao-proposal-incentives/schema/dao-proposal-incentives.json b/contracts/external/dao-proposal-incentives/schema/dao-proposal-incentives.json new file mode 100644 index 000000000..8af2e06a5 --- /dev/null +++ b/contracts/external/dao-proposal-incentives/schema/dao-proposal-incentives.json @@ -0,0 +1,623 @@ +{ + "contract_name": "dao-proposal-incentives", + "contract_version": "2.5.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "owner", + "proposal_incentives" + ], + "properties": { + "owner": { + "description": "The contract's owner using cw-ownable", + "type": "string" + }, + "proposal_incentives": { + "description": "Rewards to pay out for successful proposals.", + "allOf": [ + { + "$ref": "#/definitions/ProposalIncentivesUnchecked" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "ProposalIncentivesUnchecked": { + "type": "object", + "required": [ + "denom", + "rewards_per_proposal" + ], + "properties": { + "denom": { + "$ref": "#/definitions/UncheckedDenom" + }, + "rewards_per_proposal": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "UncheckedDenom": { + "description": "A denom that has not been checked to confirm it points to a valid asset.", + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Fires when a new proposal status has changed.", + "type": "object", + "required": [ + "proposal_hook" + ], + "properties": { + "proposal_hook": { + "$ref": "#/definitions/ProposalHookMsg" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_proposal_incentives" + ], + "properties": { + "update_proposal_incentives": { + "type": "object", + "required": [ + "proposal_incentives" + ], + "properties": { + "proposal_incentives": { + "$ref": "#/definitions/ProposalIncentivesUnchecked" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw20ReceiveMsg": { + "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "amount", + "msg", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ProposalHookMsg": { + "description": "An enum representing proposal hook messages. Either a new proposal hook, fired when a new proposal is created, or a proposal status hook, fired when a proposal changes status.", + "oneOf": [ + { + "type": "object", + "required": [ + "new_proposal" + ], + "properties": { + "new_proposal": { + "type": "object", + "required": [ + "id", + "proposer" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "proposer": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "proposal_status_changed" + ], + "properties": { + "proposal_status_changed": { + "type": "object", + "required": [ + "id", + "new_status", + "old_status" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "new_status": { + "type": "string" + }, + "old_status": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ProposalIncentivesUnchecked": { + "type": "object", + "required": [ + "denom", + "rewards_per_proposal" + ], + "properties": { + "denom": { + "$ref": "#/definitions/UncheckedDenom" + }, + "rewards_per_proposal": { + "$ref": "#/definitions/Uint128" + } + }, + "additionalProperties": false + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + }, + "UncheckedDenom": { + "description": "A denom that has not been checked to confirm it points to a valid asset.", + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Returns the proposal incentives", + "type": "object", + "required": [ + "proposal_incentives" + ], + "properties": { + "proposal_incentives": { + "type": "object", + "properties": { + "height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "from_compatible" + ], + "properties": { + "from_compatible": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "sudo": null, + "responses": { + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "proposal_incentives": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ProposalIncentives", + "description": "Incentives for passing successful proposals", + "type": "object", + "required": [ + "denom", + "rewards_per_proposal" + ], + "properties": { + "denom": { + "$ref": "#/definitions/CheckedDenom" + }, + "rewards_per_proposal": { + "description": "The rewards to pay out per successful proposal.", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "CheckedDenom": { + "description": "A denom that has been checked to point to a valid asset. This enum should never be constructed literally and should always be built by calling `into_checked` on an `UncheckedDenom` instance.", + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "$ref": "#/definitions/Addr" + } + }, + "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-proposal-incentives/src/contract.rs b/contracts/external/dao-proposal-incentives/src/contract.rs new file mode 100644 index 000000000..c8fc6ff73 --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/contract.rs @@ -0,0 +1,73 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cw2::set_contract_version; +use cw_ownable::get_ownership; + +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::PROPOSAL_INCENTIVES; +use crate::{execute, query, ContractError}; + +pub(crate) const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[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)?; + + // Save ownership + let ownership = cw_ownable::initialize_owner(deps.storage, deps.api, Some(&msg.owner))?; + + // Validate proposal incentives + let proposal_incentives = msg.proposal_incentives.into_checked(deps.as_ref())?; + + // Save proposal incentives config + PROPOSAL_INCENTIVES.save(deps.storage, &proposal_incentives, env.block.height)?; + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("creator", info.sender) + .add_attributes(ownership.into_attributes()) + .add_attributes(proposal_incentives.into_attributes())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::ProposalHook(msg) => execute::proposal_hook(deps, env, info, msg), + ExecuteMsg::UpdateOwnership(action) => execute::update_ownership(deps, env, info, action), + ExecuteMsg::UpdateProposalIncentives { + proposal_incentives, + } => execute::update_proposal_incentives(deps, env, info, proposal_incentives), + ExecuteMsg::Receive(cw20_receive_msg) => { + execute::receive_cw20(deps, env, info, cw20_receive_msg) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::ProposalIncentives { height } => { + to_json_binary(&query::proposal_incentives(deps, height)?) + } + QueryMsg::Ownership {} => to_json_binary(&get_ownership(deps.storage)?), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::default()) +} diff --git a/contracts/external/dao-proposal-incentives/src/error.rs b/contracts/external/dao-proposal-incentives/src/error.rs new file mode 100644 index 000000000..57a1a362c --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/error.rs @@ -0,0 +1,35 @@ +use cosmwasm_std::StdError; +use cw_denom::DenomError; +use cw_ownable::OwnershipError; +use cw_utils::{ParseReplyError, PaymentError}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("{0}")] + DenomError(#[from] DenomError), + + #[error("{0}")] + ParseReplyError(#[from] ParseReplyError), + + #[error("{0}")] + OwnershipError(#[from] OwnershipError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("An unknown reply ID was received.")] + UnknownReplyID {}, + + #[error("No reward per proposal given")] + NoRewardPerProposal {}, + + #[error("Proposal module is inactive")] + ProposalModuleIsInactive {}, +} diff --git a/contracts/external/dao-proposal-incentives/src/execute.rs b/contracts/external/dao-proposal-incentives/src/execute.rs new file mode 100644 index 000000000..95267ff27 --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/execute.rs @@ -0,0 +1,118 @@ +use cosmwasm_std::{Attribute, CosmosMsg, DepsMut, Env, MessageInfo, Response}; +use cw20::Cw20ReceiveMsg; +use cw_ownable::{assert_owner, get_ownership}; +use dao_hooks::proposal::ProposalHookMsg; +use dao_interface::{ + proposal::GenericProposalInfo, + state::{ProposalModule, ProposalModuleStatus}, +}; +use dao_voting::status::Status; + +use crate::{msg::ProposalIncentivesUnchecked, state::PROPOSAL_INCENTIVES, ContractError}; + +pub fn proposal_hook( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ProposalHookMsg, +) -> Result { + let mut msgs: Vec = vec![]; + let mut attrs: Vec = vec![]; + + // Get ownership + let ownership = get_ownership(deps.storage)?; + + if let Some(owner) = ownership.owner { + // Validate the message is coming from a proposal module of the owner (DAO) + let proposal_module = deps.querier.query_wasm_smart::( + owner, + &dao_interface::msg::QueryMsg::ProposalModule { + address: info.sender.to_string(), + }, + )?; + + // If the proposal module is disabled, then return error + if proposal_module.status == ProposalModuleStatus::Disabled { + return Err(ContractError::ProposalModuleIsInactive {}); + } + + // Check prop status and type of hook + if let ProposalHookMsg::ProposalStatusChanged { id, new_status, .. } = msg { + // If prop status is success, add message to pay out rewards + // Otherwise, do nothing + if new_status == Status::Passed.to_string() { + // Query for the proposal + let proposal_info: GenericProposalInfo = deps.querier.query_wasm_smart( + info.sender, + &dao_interface::proposal::Query::GenericProposalInfo { proposal_id: id }, + )?; + + // Load proposal incentives config + let proposal_incentives = PROPOSAL_INCENTIVES + .may_load_at_height(deps.storage, proposal_info.start_height)?; + + // Append the message if found + if let Some(proposal_incentives) = proposal_incentives { + msgs.push(proposal_incentives.denom.get_transfer_to_message( + &proposal_info.proposer, + proposal_incentives.rewards_per_proposal, + )?); + attrs = proposal_incentives.into_attributes(); + attrs.push(Attribute { + key: "proposer".to_string(), + value: proposal_info.proposer.to_string(), + }); + } + } + } + } + + Ok(Response::default() + .add_attribute("action", "proposal_hook") + .add_attributes(attrs) + .add_messages(msgs)) +} + +pub fn update_ownership( + deps: DepsMut, + env: Env, + info: MessageInfo, + action: cw_ownable::Action, +) -> Result { + let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; + + Ok(Response::new() + .add_attribute("action", "update_ownership") + .add_attributes(ownership.into_attributes())) +} + +pub fn update_proposal_incentives( + deps: DepsMut, + env: Env, + info: MessageInfo, + proposal_incentives: ProposalIncentivesUnchecked, +) -> Result { + assert_owner(deps.storage, &info.sender)?; + + // Validate proposal incentives + let proposal_incentives = proposal_incentives.into_checked(deps.as_ref())?; + + // Save the new proposal incentives + PROPOSAL_INCENTIVES.save(deps.storage, &proposal_incentives, env.block.height)?; + + Ok(Response::new() + .add_attribute("action", "update_proposal_incentives") + .add_attributes(proposal_incentives.into_attributes())) +} + +pub fn receive_cw20( + _deps: DepsMut, + _env: Env, + info: MessageInfo, + _cw20_receive_msg: Cw20ReceiveMsg, +) -> Result { + // We do not check cw20, because the expected fund can change over time + Ok(Response::new() + .add_attribute("action", "receive_cw20") + .add_attribute("cw20", info.sender)) +} diff --git a/contracts/external/dao-proposal-incentives/src/helpers.rs b/contracts/external/dao-proposal-incentives/src/helpers.rs new file mode 100644 index 000000000..fa3932a88 --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/helpers.rs @@ -0,0 +1,31 @@ +use cosmwasm_std::{Attribute, Deps}; + +use crate::{msg::ProposalIncentivesUnchecked, state::ProposalIncentives, ContractError}; + +impl ProposalIncentivesUnchecked { + pub fn into_checked(self, deps: Deps) -> Result { + if self.rewards_per_proposal.is_zero() { + return Err(ContractError::NoRewardPerProposal {}); + } + + Ok(ProposalIncentives { + rewards_per_proposal: self.rewards_per_proposal, + denom: self.denom.into_checked(deps)?, + }) + } +} + +impl ProposalIncentives { + pub fn into_attributes(&self) -> Vec { + vec![ + Attribute { + key: "reward_per_proposal".to_string(), + value: self.rewards_per_proposal.to_string(), + }, + Attribute { + key: "denom".to_string(), + value: self.denom.to_string(), + }, + ] + } +} diff --git a/contracts/external/dao-proposal-incentives/src/lib.rs b/contracts/external/dao-proposal-incentives/src/lib.rs new file mode 100644 index 000000000..9bb953dba --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod execute; +mod helpers; +pub mod msg; +pub mod query; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::ContractError; diff --git a/contracts/external/dao-proposal-incentives/src/msg.rs b/contracts/external/dao-proposal-incentives/src/msg.rs new file mode 100644 index 000000000..cb6f74186 --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/msg.rs @@ -0,0 +1,47 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Uint128; +use cw20::Cw20ReceiveMsg; +use cw_denom::UncheckedDenom; +use cw_ownable::cw_ownable_query; +use dao_hooks::proposal::ProposalHookMsg; + +use crate::state::ProposalIncentives; + +#[cw_serde] +pub struct InstantiateMsg { + /// The contract's owner using cw-ownable + pub owner: String, + /// Rewards to pay out for successful proposals. + pub proposal_incentives: ProposalIncentivesUnchecked, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Fires when a new proposal status has changed. + ProposalHook(ProposalHookMsg), + UpdateOwnership(cw_ownable::Action), + UpdateProposalIncentives { + proposal_incentives: ProposalIncentivesUnchecked, + }, + Receive(Cw20ReceiveMsg), +} + +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the proposal incentives + #[returns(ProposalIncentives)] + ProposalIncentives { height: Option }, +} + +#[cw_serde] +pub struct ProposalIncentivesUnchecked { + pub rewards_per_proposal: Uint128, + pub denom: UncheckedDenom, +} + +#[cw_serde] +pub enum MigrateMsg { + FromCompatible {}, +} diff --git a/contracts/external/dao-proposal-incentives/src/query.rs b/contracts/external/dao-proposal-incentives/src/query.rs new file mode 100644 index 000000000..5e53dceec --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/query.rs @@ -0,0 +1,14 @@ +use cosmwasm_std::{Deps, StdResult}; + +use crate::state::{ProposalIncentives, PROPOSAL_INCENTIVES}; + +pub fn proposal_incentives(deps: Deps, height: Option) -> StdResult { + match height { + Some(height) => PROPOSAL_INCENTIVES + .may_load_at_height(deps.storage, height)? + .ok_or(cosmwasm_std::StdError::NotFound { + kind: "Proposal Incentives".to_string(), + }), + None => PROPOSAL_INCENTIVES.load(deps.storage), + } +} diff --git a/contracts/external/dao-proposal-incentives/src/state.rs b/contracts/external/dao-proposal-incentives/src/state.rs new file mode 100644 index 000000000..4dffda609 --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/state.rs @@ -0,0 +1,20 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint128; +use cw_denom::CheckedDenom; +use cw_storage_plus::{SnapshotItem, Strategy}; + +/// Incentives for passing successful proposals +#[cw_serde] +pub struct ProposalIncentives { + /// The rewards to pay out per successful proposal. + pub rewards_per_proposal: Uint128, + pub denom: CheckedDenom, +} + +/// Holds ProposalIncentives state +pub const PROPOSAL_INCENTIVES: SnapshotItem = SnapshotItem::new( + "proposal_incentives", + "proposal_incentives__check", + "proposal_incentives__change", + Strategy::EveryBlock, +); diff --git a/contracts/external/dao-proposal-incentives/src/tests.rs b/contracts/external/dao-proposal-incentives/src/tests.rs new file mode 100644 index 000000000..903ef94df --- /dev/null +++ b/contracts/external/dao-proposal-incentives/src/tests.rs @@ -0,0 +1,448 @@ +use std::vec; + +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + to_json_binary, Addr, Binary, Coin, CosmosMsg, Uint128, WasmMsg, +}; + +use cw20::Cw20Coin; +use cw_denom::{CheckedDenom, UncheckedDenom}; +use cw_multi_test::{error::AnyResult, App, AppBuilder, AppResponse, Executor}; +use cw_ownable::Ownership; +use dao_testing::{ + contracts::{cw20_base_contract, dao_proposal_incentives_contract, proposal_single_contract}, + helpers::instantiate_with_cw4_groups_governance, +}; +use dao_voting::{proposal::SingleChoiceProposeMsg, threshold::Threshold}; + +use crate::{ + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, ProposalIncentivesUnchecked, QueryMsg}, + state::ProposalIncentives, +}; + +const ADMIN: &str = "admin"; +const ADDR1: &str = "addr1"; +const DENOM: &str = "juno"; + +struct Context { + app: App, + cw20_addr: Addr, + proposal_single_addr: Addr, + dao_addr: Addr, + dao_proposal_incentives_code_id: u64, +} + +fn get_context() -> Context { + // Set up app with native balances + let mut app = AppBuilder::default().build(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(ADMIN), + vec![Coin { + denom: DENOM.to_string(), + amount: Uint128::new(1000), + }], + ) + .unwrap(); + }); + + // Set up cw20 with balances + let cw20_code_id = app.store_code(cw20_base_contract()); + let cw20_addr = app + .instantiate_contract( + cw20_code_id, + Addr::unchecked(ADMIN), + &cw20_base::msg::InstantiateMsg { + name: "cw20 token".to_string(), + symbol: "cwtoken".to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: ADMIN.to_string(), + amount: Uint128::new(1000), + }], + mint: None, + marketing: None, + }, + &[], + "cw20-base", + None, + ) + .unwrap(); + + // Set up dao + let proposal_single_code_id = app.store_code(proposal_single_contract()); + let dao_addr = instantiate_with_cw4_groups_governance( + &mut app, + proposal_single_code_id, + to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + threshold: Threshold::AbsolutePercentage { + percentage: dao_voting::threshold::PercentageThreshold::Majority {}, + }, + max_voting_period: cw_utils::Duration::Height(10u64), + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + close_proposal_on_execution_failure: true, + veto: None, + }) + .unwrap(), + Some(vec![Cw20Coin { + address: ADDR1.to_string(), + amount: Uint128::one(), + }]), + ); + + // Get proposal single addr + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + dao_addr.clone(), + &dao_interface::msg::QueryMsg::ProposalModules { + start_after: None, + limit: Some(1u32), + }, + ) + .unwrap(); + assert!(!proposal_modules.is_empty()); + let proposal_single_addr = proposal_modules.first().unwrap().address.clone(); + + // Set up dao proposal incentives code id + let dao_proposal_incentives_code_id = app.store_code(dao_proposal_incentives_contract()); + + Context { + app, + cw20_addr, + dao_addr, + dao_proposal_incentives_code_id, + proposal_single_addr, + } +} + +fn vote_yes_on_proposal(context: &mut Context, proposal_id: u64) -> AnyResult { + context.app.execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Vote { + proposal_id, + vote: dao_voting::voting::Vote::Yes, + rationale: None, + }, + &[], + ) +} + +fn execute_proposal(context: &mut Context, proposal_id: u64) { + context + .app + .execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap(); +} + +#[test] +pub fn test_setup_native() { + let mut context = get_context(); + + // Cannot instantiate with 0 due + let result = context.app.instantiate_contract( + context.dao_proposal_incentives_code_id, + Addr::unchecked(ADMIN), + &InstantiateMsg { + owner: ADMIN.to_string(), + proposal_incentives: ProposalIncentivesUnchecked { + rewards_per_proposal: Uint128::zero(), + denom: UncheckedDenom::Native(DENOM.to_string()), + }, + }, + &[], + "dao_proposal_incentives".to_string(), + None, + ); + assert!(result.is_err()); + + // Can instantiate with some due + let result = context.app.instantiate_contract( + context.dao_proposal_incentives_code_id, + Addr::unchecked(ADMIN), + &InstantiateMsg { + owner: ADMIN.to_string(), + proposal_incentives: ProposalIncentivesUnchecked { + rewards_per_proposal: Uint128::new(1000), + denom: UncheckedDenom::Native(DENOM.to_string()), + }, + }, + &[], + "dao_proposal_incentives".to_string(), + None, + ); + assert!(result.is_ok()); + let dao_proposal_incentives_addr = result.unwrap(); + + // Ensure owner was set on init + let ownership: Ownership = context + .app + .wrap() + .query_wasm_smart( + dao_proposal_incentives_addr.clone(), + &QueryMsg::Ownership {}, + ) + .unwrap(); + + assert_eq!(ownership.owner, Some(ADMIN.to_string())); + + // Ensure proposal incentives was set + let proposal_incentives: ProposalIncentives = context + .app + .wrap() + .query_wasm_smart( + dao_proposal_incentives_addr.clone(), + &QueryMsg::ProposalIncentives { height: None }, + ) + .unwrap(); + assert_eq!( + proposal_incentives, + ProposalIncentives { + rewards_per_proposal: Uint128::new(1000), + denom: CheckedDenom::Native(DENOM.to_string()) + } + ); + + // Cannot update rewards to zero + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_proposal_incentives_addr.clone(), + &ExecuteMsg::UpdateProposalIncentives { + proposal_incentives: ProposalIncentivesUnchecked { + rewards_per_proposal: Uint128::zero(), + denom: UncheckedDenom::Native(DENOM.to_string()), + }, + }, + &[], + ); + assert!(result.is_err()); + + // Cannot update unauthorized + let result = context.app.execute_contract( + Addr::unchecked(ADDR1), + dao_proposal_incentives_addr.clone(), + &ExecuteMsg::UpdateProposalIncentives { + proposal_incentives: ProposalIncentivesUnchecked { + rewards_per_proposal: Uint128::one(), + denom: UncheckedDenom::Native(DENOM.to_string()), + }, + }, + &[], + ); + assert!(result.is_err()); + + // Can update rewards + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_proposal_incentives_addr.clone(), + &ExecuteMsg::UpdateProposalIncentives { + proposal_incentives: ProposalIncentivesUnchecked { + rewards_per_proposal: Uint128::one(), + denom: UncheckedDenom::Cw20(context.cw20_addr.to_string()), + }, + }, + &[], + ); + assert!(result.is_ok()); + + // Ensure proposal incentives was updated + let proposal_incentives: ProposalIncentives = context + .app + .wrap() + .query_wasm_smart( + dao_proposal_incentives_addr.clone(), + &QueryMsg::ProposalIncentives { height: None }, + ) + .unwrap(); + assert_eq!( + proposal_incentives, + ProposalIncentives { + rewards_per_proposal: Uint128::one(), + denom: CheckedDenom::Cw20(context.cw20_addr.clone()), + } + ); +} + +#[test] +pub fn test_hook() { + let mut context = get_context(); + + // Create the proposal incentives contract + let dao_proposal_incentives_addr = context + .app + .instantiate_contract( + context.dao_proposal_incentives_code_id, + Addr::unchecked(ADMIN), + &InstantiateMsg { + owner: context.dao_addr.to_string(), + proposal_incentives: ProposalIncentivesUnchecked { + rewards_per_proposal: Uint128::new(1000), + denom: UncheckedDenom::Native(DENOM.to_string()), + }, + }, + &[], + "dao_proposal_incentives".to_string(), + None, + ) + .unwrap(); + context.app.update_block(|x| x.height += 10); + + // Execute fails - unauthorized + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_proposal_incentives_addr.clone(), + &ExecuteMsg::ProposalHook( + dao_hooks::proposal::ProposalHookMsg::ProposalStatusChanged { + id: 1u64, + old_status: "open".to_string(), + new_status: "passed".to_string(), + }, + ), + &[], + ); + assert!(result.is_err()); + + // Fund the incentives contract for 1 reward + context + .app + .send_tokens( + Addr::unchecked(ADMIN), + dao_proposal_incentives_addr.clone(), + &[Coin { + denom: DENOM.to_string(), + amount: Uint128::new(1000), + }], + ) + .unwrap(); + + // Fund the incentives contract with cw20 as well to show cw20 support + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + context.cw20_addr.clone(), + &cw20::Cw20ExecuteMsg::Send { + contract: dao_proposal_incentives_addr.to_string(), + amount: Uint128::new(1000), + msg: Binary::default(), + }, + &[], + ); + assert!(result.is_ok()); + + // Propose adding a hook + let result = context.app.execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Propose(SingleChoiceProposeMsg { + title: "Add proposal hook".to_string(), + description: "Adding a proposal hook to test the dao_proposal_incentives contract" + .to_string(), + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: context.proposal_single_addr.to_string(), + msg: to_json_binary(&dao_proposal_single::msg::ExecuteMsg::AddProposalHook { + address: dao_proposal_incentives_addr.to_string(), + }) + .unwrap(), + funds: vec![], + })], + proposer: None, + vote: None, + }), + &[], + ); + assert!(result.is_ok()); + + // Vote and execute the proposal to add the proposal hook + vote_yes_on_proposal(&mut context, 1u64).unwrap(); + execute_proposal(&mut context, 1u64); + + // Query for the newly-established hook + let result: cw_hooks::HooksResponse = context + .app + .wrap() + .query_wasm_smart( + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::QueryMsg::ProposalHooks {}, + ) + .unwrap(); + assert!(result + .hooks + .contains(&dao_proposal_incentives_addr.to_string())); + + // Create a new proposal + context + .app + .execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Propose(SingleChoiceProposeMsg { + title: "Test proposal".to_string(), + description: "Testing".to_string(), + msgs: vec![], + proposer: None, + vote: None, + }), + &[], + ) + .unwrap(); + + // Assert that the proposal hook's execution has sent funds to proposer + let result = vote_yes_on_proposal(&mut context, 2u64); + assert!(result.is_ok()); + let balance = context.app.wrap().query_balance(ADDR1, DENOM).unwrap(); + assert_eq!( + balance, + Coin { + denom: DENOM.to_string(), + amount: Uint128::new(1000) + } + ); + + // Create a new proposal + context + .app + .execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Propose(SingleChoiceProposeMsg { + title: "Test proposal".to_string(), + description: "Testing".to_string(), + msgs: vec![], + proposer: None, + vote: None, + }), + &[], + ) + .unwrap(); + + // Assert that the proposal hook's failure still allows completion + // The hook is attempting to send funds when it has run out of funds + let result = vote_yes_on_proposal(&mut context, 3u64); + assert!(result.is_ok()); + assert!(result.unwrap().events.iter().any(|x| x + .attributes + .iter() + .any(|y| y.key == "removed_proposal_hook" + && y.value == format!("{0}:0", dao_proposal_incentives_addr)))); +} + +#[test] +pub fn test_migrate_update_version() { + let mut deps = mock_dependencies(); + cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); + migrate(deps.as_mut(), mock_env(), MigrateMsg::FromCompatible {}).unwrap(); + let version = cw2::get_contract_version(&deps.storage).unwrap(); + assert_eq!(version.version, CONTRACT_VERSION); + assert_eq!(version.contract, CONTRACT_NAME); +} diff --git a/contracts/external/dao-voting-incentives/.cargo/config b/contracts/external/dao-voting-incentives/.cargo/config new file mode 100644 index 000000000..336b618a1 --- /dev/null +++ b/contracts/external/dao-voting-incentives/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/external/dao-voting-incentives/Cargo.toml b/contracts/external/dao-voting-incentives/Cargo.toml new file mode 100644 index 000000000..925456573 --- /dev/null +++ b/contracts/external/dao-voting-incentives/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name ="dao-voting-incentives" +authors = ["Jake Hartnell ", "Gabe "] +description = "A contract that implements incentives for voting in a DAO." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +cw-storage-plus = { workspace = true } +cw2 = { workspace = true } +dao-hooks = { workspace = true } +dao-interface = { workspace = true } +dao-voting = { workspace = true } +thiserror = { workspace = true } +cw-utils = { workspace = true } +cw-denom = { workspace = true } +cw-ownable = { workspace = true } +cw20 = { workspace = true } + +[dev-dependencies] +cosmwasm-schema = { workspace = true } +cw-multi-test = { workspace = true } +dao-dao-core = { workspace = true, features = ["library"] } +cw20-base = { workspace = true, features = ["library"] } +cw-hooks = { workspace = true } +dao-testing = { workspace = true } +dao-hooks = { workspace = true } +dao-proposal-single = { workspace = true, features = ["library"] } \ No newline at end of file diff --git a/contracts/external/dao-voting-incentives/README.md b/contracts/external/dao-voting-incentives/README.md new file mode 100644 index 000000000..9caeb4f40 --- /dev/null +++ b/contracts/external/dao-voting-incentives/README.md @@ -0,0 +1,65 @@ +# dao-voting-incentives + +> **WARNING:** THIS CONTRACT IS NOT AUDITED AND IS EXPERIMENTAL. USE AT YOUR OWN RISK. + +The `dao-voting-incentives` contract is designed to boost participation in DAO governance by offering rewards for voting on proposals. This innovative mechanism encourages active community involvement and more representative decision-making within DAOs. + +## Features + +- Flexible reward distribution for both native and CW20 tokens +- Time-bound incentive periods +- Fair reward calculation based on voting activity + +## Instantiation + +To deploy the contract, provide the following parameters: + +```rust +pub struct InstantiateMsg { + pub owner: String, + pub denom: UncheckedDenom, + pub expiration: Expiration, +} +``` + +- `owner`: The DAO address that will manage the contract and receive vote hooks +- `denom`: The token denomination (native or CW20) to be distributed as rewards +- `expiration`: The end date of the voting incentives period + +## Setup + +1. Deploy the `dao-voting-incentives` contract +2. Add the contract's address as a `VoteHook` to your DAO's proposal module (`dao-proposal-single` or `dao-proposal-multiple`) +3. Ensure the DAO is set as the `owner` of the contract +4. Fund the contract with the specified reward tokens + +## Key Functions + +### Execute Messages + +- `VoteHook(VoteHookMsg)`: Tracks voting activity (automatically called by the proposal module) +- `Claim {}`: Allows voters to claim their earned rewards after the incentive period ends +- `Expire {}`: Finalizes the incentive period, enabling reward claims +- `UpdateOwnership(Action)`: Manages contract ownership +- `Receive(Cw20ReceiveMsg)`: Handles incoming CW20 tokens for rewards + +### Query Messages + +- `Config {}`: Retrieves the contract's configuration +- `Rewards { address: String }`: Gets the claimable rewards for a specific address +- `Votes { address: String }`: Returns the number of votes cast by an address + +## Reward Calculation + +Rewards are calculated using the following formula: + +``` +reward(user) = votes(user) * contract_balance / total_votes +``` + +This ensures a fair distribution based on each user's voting activity relative to the total participation. + +## Important Notes + +- If no votes are cast during the incentive period, all funds are returned to the owner (DAO) upon expiration +- Rewards can only be claimed after the incentive period has ended diff --git a/contracts/external/dao-voting-incentives/examples/schema.rs b/contracts/external/dao-voting-incentives/examples/schema.rs new file mode 100644 index 000000000..52981727a --- /dev/null +++ b/contracts/external/dao-voting-incentives/examples/schema.rs @@ -0,0 +1,11 @@ +use cosmwasm_schema::write_api; +use dao_voting_incentives::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg, + } +} diff --git a/contracts/external/dao-voting-incentives/schema/dao-voting-incentives.json b/contracts/external/dao-voting-incentives/schema/dao-voting-incentives.json new file mode 100644 index 000000000..b936123b4 --- /dev/null +++ b/contracts/external/dao-voting-incentives/schema/dao-voting-incentives.json @@ -0,0 +1,687 @@ +{ + "contract_name": "dao-voting-incentives", + "contract_version": "2.5.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "type": "object", + "required": [ + "denom", + "expiration", + "owner" + ], + "properties": { + "denom": { + "description": "The denom to distribute", + "allOf": [ + { + "$ref": "#/definitions/UncheckedDenom" + } + ] + }, + "expiration": { + "description": "The expiration of the voting incentives", + "allOf": [ + { + "$ref": "#/definitions/Expiration" + } + ] + }, + "owner": { + "description": "The contract's owner using cw-ownable", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + }, + "UncheckedDenom": { + "description": "A denom that has not been checked to confirm it points to a valid asset.", + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "oneOf": [ + { + "description": "Fires when a new vote is cast.", + "type": "object", + "required": [ + "vote_hook" + ], + "properties": { + "vote_hook": { + "$ref": "#/definitions/VoteHookMsg" + } + }, + "additionalProperties": false + }, + { + "description": "Claim rewards", + "type": "object", + "required": [ + "claim" + ], + "properties": { + "claim": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Expire the voting incentives period", + "type": "object", + "required": [ + "expire" + ], + "properties": { + "expire": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_ownership" + ], + "properties": { + "update_ownership": { + "$ref": "#/definitions/Action" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Action": { + "description": "Actions that can be taken to alter the contract's ownership", + "oneOf": [ + { + "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.", + "type": "object", + "required": [ + "transfer_ownership" + ], + "properties": { + "transfer_ownership": { + "type": "object", + "required": [ + "new_owner" + ], + "properties": { + "expiry": { + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "new_owner": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.", + "type": "string", + "enum": [ + "accept_ownership" + ] + }, + { + "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.", + "type": "string", + "enum": [ + "renounce_ownership" + ] + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec. See also .", + "type": "string" + }, + "Cw20ReceiveMsg": { + "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg", + "type": "object", + "required": [ + "amount", + "msg", + "sender" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "msg": { + "$ref": "#/definitions/Binary" + }, + "sender": { + "type": "string" + } + }, + "additionalProperties": false + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + }, + "VoteHookMsg": { + "description": "An enum representing vote hooks, fired when new votes are cast.", + "oneOf": [ + { + "type": "object", + "required": [ + "new_vote" + ], + "properties": { + "new_vote": { + "type": "object", + "required": [ + "proposal_id", + "vote", + "voter" + ], + "properties": { + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "vote": { + "type": "string" + }, + "voter": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "oneOf": [ + { + "description": "Returns the config", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the rewards for the given address.", + "type": "object", + "required": [ + "rewards" + ], + "properties": { + "rewards": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the votes count for the given address", + "type": "object", + "required": [ + "votes" + ], + "properties": { + "votes": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Query the contract's ownership information", + "type": "object", + "required": [ + "ownership" + ], + "properties": { + "ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "oneOf": [ + { + "type": "object", + "required": [ + "from_compatible" + ], + "properties": { + "from_compatible": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "sudo": null, + "responses": { + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "description": "Top level config type for core module.", + "type": "object", + "required": [ + "automatically_add_cw20s", + "automatically_add_cw721s", + "description", + "name" + ], + "properties": { + "automatically_add_cw20s": { + "description": "If true the contract will automatically add received cw20 tokens to its treasury.", + "type": "boolean" + }, + "automatically_add_cw721s": { + "description": "If true the contract will automatically add received cw721 tokens to its treasury.", + "type": "boolean" + }, + "dao_uri": { + "description": "The URI for the DAO as defined by the DAOstar standard ", + "type": [ + "string", + "null" + ] + }, + "description": { + "description": "A description of the contract.", + "type": "string" + }, + "image_url": { + "description": "An optional image URL for displaying alongside the contract.", + "type": [ + "string", + "null" + ] + }, + "name": { + "description": "The name of the contract.", + "type": "string" + } + }, + "additionalProperties": false + }, + "ownership": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Ownership_for_String", + "description": "The contract's ownership info", + "type": "object", + "properties": { + "owner": { + "description": "The contract's current owner. `None` if the ownership has been renounced.", + "type": [ + "string", + "null" + ] + }, + "pending_expiry": { + "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.", + "anyOf": [ + { + "$ref": "#/definitions/Expiration" + }, + { + "type": "null" + } + ] + }, + "pending_owner": { + "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "oneOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "$ref": "#/definitions/Timestamp" + } + }, + "additionalProperties": false + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Timestamp": { + "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```", + "allOf": [ + { + "$ref": "#/definitions/Uint64" + } + ] + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, + "rewards": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RewardResponse", + "type": "object", + "required": [ + "amount", + "denom", + "is_claimable" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "$ref": "#/definitions/CheckedDenom" + }, + "is_claimable": { + "type": "boolean" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "CheckedDenom": { + "description": "A denom that has been checked to point to a valid asset. This enum should never be constructed literally and should always be built by calling `into_checked` on an `UncheckedDenom` instance.", + "oneOf": [ + { + "description": "A native (bank module) asset.", + "type": "object", + "required": [ + "native" + ], + "properties": { + "native": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "description": "A cw20 asset.", + "type": "object", + "required": [ + "cw20" + ], + "properties": { + "cw20": { + "$ref": "#/definitions/Addr" + } + }, + "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" + } + } + }, + "votes": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "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-voting-incentives/src/contract.rs b/contracts/external/dao-voting-incentives/src/contract.rs new file mode 100644 index 000000000..ec439fe57 --- /dev/null +++ b/contracts/external/dao-voting-incentives/src/contract.rs @@ -0,0 +1,96 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, +}; +use cw2::set_contract_version; +use cw_ownable::get_ownership; +use cw_utils::Expiration; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::{Config, CONFIG}; +use crate::{execute, query}; + +pub(crate) const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME"); +pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[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)?; + + // Save ownership + let ownership = cw_ownable::initialize_owner(deps.storage, deps.api, Some(&msg.owner))?; + + // Validate denom + let denom = msg.denom.into_checked(deps.as_ref())?; + + // Validate expiration + if msg.expiration.is_expired(&env.block) { + return Err(ContractError::AlreadyExpired {}); + } + if let Expiration::Never {} = msg.expiration { + return Err(ContractError::NotExpired { + expiration: Expiration::Never {}, + }); + } + + // Save voting incentives config + CONFIG.save( + deps.storage, + &Config { + start_height: env.block.height, + expiration: msg.expiration, + denom: denom.clone(), + total_votes: Uint128::zero(), + expiration_balance: None, + }, + )?; + + Ok(Response::new() + .add_attribute("method", "instantiate") + .add_attribute("creator", info.sender) + .add_attribute("expiration", msg.expiration.to_string()) + .add_attribute("denom", denom.to_string()) + .add_attributes(ownership.into_attributes())) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Claim {} => execute::claim(deps, env, info), + ExecuteMsg::VoteHook(msg) => execute::vote_hook(deps, env, info, msg), + ExecuteMsg::Expire {} => execute::expire(deps, env, info), + ExecuteMsg::UpdateOwnership(action) => execute::update_ownership(deps, env, info, action), + ExecuteMsg::Receive(cw20_receive_msg) => { + execute::receive_cw20(deps, env, info, cw20_receive_msg) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Rewards { address } => to_json_binary(&query::rewards(deps, env, address)?), + QueryMsg::Config {} => to_json_binary(&query::config(deps)?), + QueryMsg::Ownership {} => to_json_binary(&get_ownership(deps.storage)?), + QueryMsg::Votes { address } => to_json_binary(&query::votes(deps, address)?), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + // Set contract to version to latest + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + Ok(Response::default()) +} diff --git a/contracts/external/dao-voting-incentives/src/error.rs b/contracts/external/dao-voting-incentives/src/error.rs new file mode 100644 index 000000000..64b3298b3 --- /dev/null +++ b/contracts/external/dao-voting-incentives/src/error.rs @@ -0,0 +1,44 @@ +use cosmwasm_std::{CheckedMultiplyFractionError, OverflowError, StdError}; +use cw_denom::{CheckedDenom, DenomError}; +use cw_ownable::OwnershipError; +use cw_utils::Expiration; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + DenomError(#[from] DenomError), + + #[error("{0}")] + OwnershipError(#[from] OwnershipError), + + #[error("{0}")] + CheckedMultiplyFractionError(#[from] CheckedMultiplyFractionError), + + #[error("{0}")] + OverflowError(#[from] OverflowError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("NotExpired")] + NotExpired { expiration: Expiration }, + + #[error("AlreadyExpired")] + AlreadyExpired {}, + + #[error("Proposal module is inactive")] + ProposalModuleIsInactive {}, + + #[error("UnexpectedFunds")] + UnexpectedFunds { + expected: CheckedDenom, + received: CheckedDenom, + }, + + #[error("NothingToClaim")] + NothingToClaim {}, +} diff --git a/contracts/external/dao-voting-incentives/src/execute.rs b/contracts/external/dao-voting-incentives/src/execute.rs new file mode 100644 index 000000000..a4d312467 --- /dev/null +++ b/contracts/external/dao-voting-incentives/src/execute.rs @@ -0,0 +1,245 @@ +use cosmwasm_std::{Attribute, DepsMut, Env, MessageInfo, Response, StdResult, Uint128}; +use cw20::Cw20ReceiveMsg; +use cw_denom::CheckedDenom; +use cw_ownable::get_ownership; +use dao_hooks::vote::VoteHookMsg; +use dao_interface::{ + proposal::GenericProposalInfo, + state::{ProposalModule, ProposalModuleStatus}, +}; + +use crate::{ + state::{reward, CONFIG, GENERIC_PROPOSAL_INFO, USER_PROPOSAL_HAS_VOTED, USER_VOTE_COUNT}, + ContractError, +}; + +pub fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result { + // Ensure the user has something to claim + if !USER_VOTE_COUNT.has(deps.storage, &info.sender) { + return Err(ContractError::NothingToClaim {}); + } + + // Get reward information + let reward = reward(deps.as_ref(), &env.contract.address, &info.sender)?; + + // If the user has rewards, then we should generate a message + let mut msgs = vec![]; + if !reward.amount.is_zero() { + msgs.push( + reward + .denom + .get_transfer_to_message(&info.sender, reward.amount)?, + ); + } + + // Clean state + USER_VOTE_COUNT.remove(deps.storage, &info.sender); + + Ok(Response::new() + .add_attribute("action", "claim") + .add_attribute("denom", reward.denom.to_string()) + .add_attribute("amount", reward.amount) + .add_messages(msgs)) +} + +pub fn update_ownership( + deps: DepsMut, + env: Env, + info: MessageInfo, + action: cw_ownable::Action, +) -> Result { + let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?; + + Ok(Response::new() + .add_attribute("action", "update_ownership") + .add_attributes(ownership.into_attributes())) +} + +pub fn vote_hook( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: VoteHookMsg, +) -> Result { + let mut attrs: Vec = vec![]; + + // Get ownership + let ownership = get_ownership(deps.storage)?; + + if let Some(owner) = ownership.owner { + // Validate the message is coming from a proposal module of the owner (DAO) + let proposal_module = deps.querier.query_wasm_smart::( + owner, + &dao_interface::msg::QueryMsg::ProposalModule { + address: info.sender.to_string(), + }, + )?; + + // If the proposal module is disabled, then return error + if proposal_module.status == ProposalModuleStatus::Disabled { + return Err(ContractError::ProposalModuleIsInactive {}); + } + + // Check type of hook + match msg { + VoteHookMsg::NewVote { + proposal_id, voter, .. + } => { + if let Ok(voter) = deps.api.addr_validate(&voter) { + // Get config + let mut config = CONFIG.load(deps.storage)?; + + // Check if the voting incentives have expired + if config.expiration.is_expired(&env.block) { + return Err(ContractError::AlreadyExpired {}); + } + + // Get the proposal info + // If we have a value in the cache, then return the value + // If we don't have a value, then query for the value and set it in the cache + let proposal_info = if GENERIC_PROPOSAL_INFO + .has(deps.storage, (&info.sender, proposal_id)) + { + GENERIC_PROPOSAL_INFO.load(deps.storage, (&info.sender, proposal_id))? + } else { + let proposal_info: GenericProposalInfo = deps.querier.query_wasm_smart( + info.sender.clone(), + &dao_interface::proposal::Query::GenericProposalInfo { proposal_id }, + )?; + + GENERIC_PROPOSAL_INFO.save( + deps.storage, + (&info.sender, proposal_id), + &proposal_info, + )?; + + proposal_info + }; + + // Check if the vote came from a proposal at or after the start of the voting incentives + if proposal_info.start_height >= config.start_height { + // Check if the user has already voted for the proposal + if !USER_PROPOSAL_HAS_VOTED.has(deps.storage, (&voter, proposal_id)) { + // Increment counts + let user_votes = USER_VOTE_COUNT.update( + deps.storage, + &voter, + |x| -> StdResult { + Ok(x.unwrap_or_default().checked_add(Uint128::one())?) + }, + )?; + config.total_votes = config.total_votes.checked_add(Uint128::one())?; + CONFIG.save(deps.storage, &config)?; + + // Set has voted + USER_PROPOSAL_HAS_VOTED.save( + deps.storage, + (&voter, proposal_id), + &true, + )?; + + // Set attributes + attrs = vec![ + Attribute { + key: "total_votes".to_string(), + value: config.total_votes.to_string(), + }, + Attribute { + key: "user_votes".to_string(), + value: user_votes.to_string(), + }, + Attribute { + key: "user".to_string(), + value: voter.to_string(), + }, + ]; + } + } + } + } + } + } + + Ok(Response::new() + .add_attribute("action", "vote_hook") + .add_attributes(attrs)) +} + +pub fn expire(deps: DepsMut, env: Env, _info: MessageInfo) -> Result { + // Get the config + let mut config = CONFIG.load(deps.storage)?; + + // If already expired, then return an error + if config.expiration_balance.is_some() { + return Err(ContractError::AlreadyExpired {}); + } + + // Ensure the voting incentives period has passed expiration + if !config.expiration.is_expired(&env.block) { + return Err(ContractError::NotExpired { + expiration: config.expiration, + }); + } + + // Get the available balance to distribute + let balance = config + .denom + .query_balance(&deps.querier, &env.contract.address)?; + + // Save the balance + config.expiration_balance = Some(balance); + CONFIG.save(deps.storage, &config)?; + + // If no votes have occurred, then funds should be sent to the owner + let mut msgs = vec![]; + if USER_VOTE_COUNT.is_empty(deps.storage) { + let ownership = get_ownership(deps.storage)?; + + if let Some(owner) = ownership.owner { + msgs.push(config.denom.get_transfer_to_message(&owner, balance)?); + } + } + + // Clean state + GENERIC_PROPOSAL_INFO.clear(deps.storage); + USER_PROPOSAL_HAS_VOTED.clear(deps.storage); + + Ok(Response::new() + .add_attribute("action", "expire") + .add_attribute("balance", balance) + .add_messages(msgs)) +} + +pub fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + _cw20_receive_msg: Cw20ReceiveMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Do not accept unexpected cw20 + if config.expiration.is_expired(&env.block) { + return Err(ContractError::AlreadyExpired {}); + } + match &config.denom { + CheckedDenom::Native(_) => { + return Err(ContractError::UnexpectedFunds { + expected: config.denom, + received: CheckedDenom::Cw20(info.sender), + }) + } + CheckedDenom::Cw20(expected_cw20) => { + if expected_cw20 != info.sender { + return Err(ContractError::UnexpectedFunds { + expected: config.denom, + received: CheckedDenom::Cw20(info.sender), + }); + } + } + } + + Ok(Response::new() + .add_attribute("action", "receive_cw20") + .add_attribute("cw20", info.sender)) +} diff --git a/contracts/external/dao-voting-incentives/src/lib.rs b/contracts/external/dao-voting-incentives/src/lib.rs new file mode 100644 index 000000000..bed930ba1 --- /dev/null +++ b/contracts/external/dao-voting-incentives/src/lib.rs @@ -0,0 +1,13 @@ +#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))] + +pub mod contract; +mod error; +pub mod execute; +pub mod msg; +pub mod query; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::ContractError; diff --git a/contracts/external/dao-voting-incentives/src/msg.rs b/contracts/external/dao-voting-incentives/src/msg.rs new file mode 100644 index 000000000..72b1a6dd6 --- /dev/null +++ b/contracts/external/dao-voting-incentives/src/msg.rs @@ -0,0 +1,57 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Uint128; +use cw20::Cw20ReceiveMsg; +use cw_denom::{CheckedDenom, UncheckedDenom}; +use cw_ownable::cw_ownable_query; +use cw_utils::Expiration; +use dao_hooks::vote::VoteHookMsg; +use dao_interface::state::Config; + +#[cw_serde] +pub struct InstantiateMsg { + /// The contract's owner using cw-ownable + pub owner: String, + /// The denom to distribute + pub denom: UncheckedDenom, + /// The expiration of the voting incentives + pub expiration: Expiration, +} + +#[cw_serde] +pub enum ExecuteMsg { + /// Fires when a new vote is cast. + VoteHook(VoteHookMsg), + /// Claim rewards + Claim {}, + /// Expire the voting incentives period + Expire {}, + UpdateOwnership(cw_ownable::Action), + Receive(Cw20ReceiveMsg), +} + +#[cw_ownable_query] +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the config + #[returns(Config)] + Config {}, + /// Returns the rewards for the given address. + #[returns(RewardResponse)] + Rewards { address: String }, + /// Returns the votes count for the given address + #[returns(Uint128)] + Votes { address: String }, +} + +#[cw_serde] +pub enum MigrateMsg { + FromCompatible {}, +} + +#[cw_serde] +pub struct RewardResponse { + pub denom: CheckedDenom, + pub amount: Uint128, + pub is_claimable: bool, +} diff --git a/contracts/external/dao-voting-incentives/src/query.rs b/contracts/external/dao-voting-incentives/src/query.rs new file mode 100644 index 000000000..622180c2e --- /dev/null +++ b/contracts/external/dao-voting-incentives/src/query.rs @@ -0,0 +1,25 @@ +use cosmwasm_std::{Deps, Env, StdError, StdResult, Uint128}; + +use crate::{ + msg::RewardResponse, + state::{self, Config, CONFIG, USER_VOTE_COUNT}, +}; + +pub fn rewards(deps: Deps, env: Env, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + state::reward(deps, &env.contract.address, &address) + .map_err(|x| StdError::GenericErr { msg: x.to_string() }) +} + +pub fn config(deps: Deps) -> StdResult { + CONFIG.load(deps.storage) +} + +pub fn votes(deps: Deps, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + Ok(USER_VOTE_COUNT + .may_load(deps.storage, &address)? + .unwrap_or_default()) +} diff --git a/contracts/external/dao-voting-incentives/src/state.rs b/contracts/external/dao-voting-incentives/src/state.rs new file mode 100644 index 000000000..7688ce408 --- /dev/null +++ b/contracts/external/dao-voting-incentives/src/state.rs @@ -0,0 +1,78 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, CheckedMultiplyFractionError, Deps, Uint128}; +use cw_denom::CheckedDenom; +use cw_storage_plus::{Item, Map}; +use cw_utils::Expiration; +use dao_interface::proposal::GenericProposalInfo; + +use crate::{msg::RewardResponse, ContractError}; + +/// Incentives for voting +#[cw_serde] +pub struct Config { + /// The start height of the voting incentives + pub start_height: u64, + /// The expiration of these voting incentives + pub expiration: Expiration, + /// The total rewards to be distributed + pub denom: CheckedDenom, + /// The total number of votes + pub total_votes: Uint128, + /// The balance at expiration + pub expiration_balance: Option, +} + +/// A map of user address to vote count +pub const USER_VOTE_COUNT: Map<&Addr, Uint128> = Map::new("user_vote_count"); +/// A map of user address with proposal id to has voted value +/// This map is useful for cases where a proposal module allows revoting, so users cannot spam votes for more rewards +pub const USER_PROPOSAL_HAS_VOTED: Map<(&Addr, u64), bool> = Map::new("user_proposal_has_voted"); +/// The voting incentives config +pub const CONFIG: Item = Item::new("config"); +/// A cache of generic proposal information (proposal_module, proposal_id) +pub const GENERIC_PROPOSAL_INFO: Map<(&Addr, u64), GenericProposalInfo> = + Map::new("generic_proposal_info"); + +/// A method to load reward information +pub fn reward(deps: Deps, contract: &Addr, addr: &Addr) -> Result { + let config = CONFIG.load(deps.storage)?; + + // Get the user's votes + let user_votes = USER_VOTE_COUNT + .may_load(deps.storage, addr)? + .unwrap_or_default(); + + match config.expiration_balance { + Some(balance) => { + // Calculate the reward + Ok(RewardResponse { + denom: config.denom, + amount: calculate_reward(config.total_votes, user_votes, balance)?, + is_claimable: true, + }) + } + None => { + // Get the current voting incentives balance + let balance = config.denom.query_balance(&deps.querier, contract)?; + + // Calculate the reward + Ok(RewardResponse { + denom: config.denom, + amount: calculate_reward(config.total_votes, user_votes, balance)?, + is_claimable: false, + }) + } + } +} + +fn calculate_reward( + total_votes: Uint128, + user_votes: Uint128, + balance: Uint128, +) -> Result { + if balance.is_zero() || user_votes.is_zero() || total_votes.is_zero() { + return Ok(Uint128::zero()); + } + + balance.checked_mul_floor((user_votes, total_votes)) +} diff --git a/contracts/external/dao-voting-incentives/src/tests.rs b/contracts/external/dao-voting-incentives/src/tests.rs new file mode 100644 index 000000000..a6fb3ebb6 --- /dev/null +++ b/contracts/external/dao-voting-incentives/src/tests.rs @@ -0,0 +1,632 @@ +use std::vec; + +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env}, + to_json_binary, Addr, Binary, Coin, CosmosMsg, Uint128, WasmMsg, +}; + +use cw20::{BalanceResponse, Cw20Coin, Cw20QueryMsg, Cw20ReceiveMsg}; +use cw_denom::{CheckedDenom, UncheckedDenom}; +use cw_multi_test::{error::AnyResult, App, AppBuilder, AppResponse, Executor}; +use cw_utils::Expiration; +use dao_testing::{ + contracts::{cw20_base_contract, dao_voting_incentives_contract, proposal_single_contract}, + helpers::instantiate_with_cw4_groups_governance, +}; +use dao_voting::{proposal::SingleChoiceProposeMsg, threshold::Threshold}; + +use crate::{ + contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, RewardResponse}, + state::Config, +}; + +const ADMIN: &str = "admin"; +const ADDR1: &str = "addr1"; +const ADDR2: &str = "addr2"; +const DENOM: &str = "juno"; + +struct Context { + app: App, + cw20_addr: Addr, + proposal_single_addr: Addr, + dao_addr: Addr, + dao_voting_incentives_code_id: u64, +} + +fn get_context() -> Context { + // Set up app with native balances + let mut app = AppBuilder::default().build(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(ADMIN), + vec![Coin { + denom: DENOM.to_string(), + amount: Uint128::new(1000), + }], + ) + .unwrap(); + }); + + // Set up cw20 with balances + let cw20_code_id = app.store_code(cw20_base_contract()); + let cw20_addr = app + .instantiate_contract( + cw20_code_id, + Addr::unchecked(ADMIN), + &cw20_base::msg::InstantiateMsg { + name: "cw20 token".to_string(), + symbol: "cwtoken".to_string(), + decimals: 6, + initial_balances: vec![Cw20Coin { + address: ADMIN.to_string(), + amount: Uint128::new(1000), + }], + mint: None, + marketing: None, + }, + &[], + "cw20-base", + None, + ) + .unwrap(); + + // Set up dao + let proposal_single_code_id = app.store_code(proposal_single_contract()); + let dao_addr = instantiate_with_cw4_groups_governance( + &mut app, + proposal_single_code_id, + to_json_binary(&dao_proposal_single::msg::InstantiateMsg { + threshold: Threshold::AbsolutePercentage { + percentage: dao_voting::threshold::PercentageThreshold::Majority {}, + }, + max_voting_period: cw_utils::Duration::Height(10u64), + min_voting_period: None, + only_members_execute: false, + allow_revoting: false, + pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {}, + close_proposal_on_execution_failure: true, + veto: None, + }) + .unwrap(), + Some(vec![ + Cw20Coin { + address: ADDR1.to_string(), + amount: Uint128::one(), + }, + Cw20Coin { + address: ADDR2.to_string(), + amount: Uint128::one(), + }, + ]), + ); + + // Get proposal single addr + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + dao_addr.clone(), + &dao_interface::msg::QueryMsg::ProposalModules { + start_after: None, + limit: Some(1u32), + }, + ) + .unwrap(); + assert!(!proposal_modules.is_empty()); + let proposal_single_addr = proposal_modules.first().unwrap().address.clone(); + + // Set up dao voting incentives code id + let dao_voting_incentives_code_id = app.store_code(dao_voting_incentives_contract()); + + Context { + app, + cw20_addr, + dao_addr, + dao_voting_incentives_code_id, + proposal_single_addr, + } +} + +fn vote_yes_on_proposal(context: &mut Context, proposal_id: u64) -> Vec> { + vec![ + context.app.execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Vote { + proposal_id, + vote: dao_voting::voting::Vote::Yes, + rationale: None, + }, + &[], + ), + context.app.execute_contract( + Addr::unchecked(ADDR2), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Vote { + proposal_id, + vote: dao_voting::voting::Vote::Yes, + rationale: None, + }, + &[], + ), + ] +} + +fn execute_proposal(context: &mut Context, proposal_id: u64) { + context + .app + .execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Execute { proposal_id }, + &[], + ) + .unwrap(); +} + +#[test] +pub fn test_instantiate_validation() { + let mut context = get_context(); + + // Check already expired is error + let result = context.app.instantiate_contract( + context.dao_voting_incentives_code_id, + Addr::unchecked(ADMIN), + &InstantiateMsg { + owner: context.dao_addr.to_string(), + denom: UncheckedDenom::Native(DENOM.to_string()), + expiration: Expiration::AtHeight(1u64), + }, + &[], + "dao_voting_incentives".to_string(), + None, + ); + assert!(result.is_err()); + + // Check expiration never is error + let result = context.app.instantiate_contract( + context.dao_voting_incentives_code_id, + Addr::unchecked(ADMIN), + &InstantiateMsg { + owner: context.dao_addr.to_string(), + denom: UncheckedDenom::Native(DENOM.to_string()), + expiration: Expiration::Never {}, + }, + &[], + "dao_voting_incentives".to_string(), + None, + ); + assert!(result.is_err()); +} + +#[test] +pub fn test_hooks() { + let mut context = get_context(); + + // Create the voting incentives contract for native + // The expiration is 10 blocks from start (12345 height) + let dao_voting_incentives_addr = context + .app + .instantiate_contract( + context.dao_voting_incentives_code_id, + Addr::unchecked(ADMIN), + &InstantiateMsg { + owner: context.dao_addr.to_string(), + denom: UncheckedDenom::Native(DENOM.to_string()), + expiration: Expiration::AtHeight(12355u64), + }, + &[], + "dao_voting_incentives".to_string(), + None, + ) + .unwrap(); + + // Also create a parallel voting incentives contract for cw20 + let dao_voting_incentives_cw20_addr = context + .app + .instantiate_contract( + context.dao_voting_incentives_code_id, + Addr::unchecked(ADMIN), + &InstantiateMsg { + owner: context.dao_addr.to_string(), + denom: UncheckedDenom::Cw20(context.cw20_addr.to_string()), + expiration: Expiration::AtHeight(12355u64), + }, + &[], + "dao_voting_incentives_cw20".to_string(), + None, + ) + .unwrap(); + + context.app.update_block(|x| x.height += 1); + + // Execute fails - unauthorized + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_voting_incentives_addr.clone(), + &ExecuteMsg::VoteHook(dao_hooks::vote::VoteHookMsg::NewVote { + proposal_id: 1u64, + voter: ADMIN.to_string(), + vote: "a fake vote".to_string(), + }), + &[], + ); + assert!(result.is_err()); + + // Fund the incentives contracts for 1000 + context + .app + .send_tokens( + Addr::unchecked(ADMIN), + dao_voting_incentives_addr.clone(), + &[Coin { + denom: DENOM.to_string(), + amount: Uint128::new(1000), + }], + ) + .unwrap(); + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + context.cw20_addr.clone(), + &cw20::Cw20ExecuteMsg::Send { + contract: dao_voting_incentives_cw20_addr.to_string(), + amount: Uint128::new(1000), + msg: Binary::default(), + }, + &[], + ); + assert!(result.is_ok()); + + // Assert the cw20 voting incentives do not accept a random cw20 token + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_voting_incentives_cw20_addr.clone(), + &ExecuteMsg::Receive(Cw20ReceiveMsg { + sender: ADMIN.to_string(), + amount: Uint128::new(1000), + msg: Binary::default(), + }), + &[], + ); + assert!(result.is_err()); + + // Propose adding both hooks + let result = context.app.execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Propose(SingleChoiceProposeMsg { + title: "Add vote hooks".to_string(), + description: "Adding 2 voting hooks to test the dao_voting_incentives contract" + .to_string(), + msgs: vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: context.proposal_single_addr.to_string(), + msg: to_json_binary(&dao_proposal_single::msg::ExecuteMsg::AddVoteHook { + address: dao_voting_incentives_addr.to_string(), + }) + .unwrap(), + funds: vec![], + }), + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: context.proposal_single_addr.to_string(), + msg: to_json_binary(&dao_proposal_single::msg::ExecuteMsg::AddVoteHook { + address: dao_voting_incentives_cw20_addr.to_string(), + }) + .unwrap(), + funds: vec![], + }), + ], + proposer: None, + vote: None, + }), + &[], + ); + assert!(result.is_ok()); + + // Vote and execute the proposal to add the vote hooks + let results = vote_yes_on_proposal(&mut context, 1u64); + for result in results { + assert!(result.is_ok()); + } + execute_proposal(&mut context, 1u64); + + // Query for the newly-established hooks + let result: cw_hooks::HooksResponse = context + .app + .wrap() + .query_wasm_smart( + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::QueryMsg::VoteHooks {}, + ) + .unwrap(); + assert!(result + .hooks + .contains(&dao_voting_incentives_addr.to_string())); + assert!(result + .hooks + .contains(&dao_voting_incentives_cw20_addr.to_string())); + + // Create a new proposal + context + .app + .execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Propose(SingleChoiceProposeMsg { + title: "Test proposal".to_string(), + description: "Testing".to_string(), + msgs: vec![], + proposer: None, + vote: None, + }), + &[], + ) + .unwrap(); + + // Trigger a vote hook + let results = vote_yes_on_proposal(&mut context, 2u64); + for result in results { + assert!(result.is_ok()); + } + + // Assert that the vote hook has incremented vote counts + let votes: Uint128 = context + .app + .wrap() + .query_wasm_smart( + dao_voting_incentives_addr.clone(), + &QueryMsg::Votes { + address: ADDR1.to_string(), + }, + ) + .unwrap(); + assert_eq!(votes, Uint128::one()); + let votes: Uint128 = context + .app + .wrap() + .query_wasm_smart( + dao_voting_incentives_addr.clone(), + &QueryMsg::Votes { + address: ADDR2.to_string(), + }, + ) + .unwrap(); + assert_eq!(votes, Uint128::one()); + let votes: Uint128 = context + .app + .wrap() + .query_wasm_smart( + dao_voting_incentives_cw20_addr.clone(), + &QueryMsg::Votes { + address: ADDR1.to_string(), + }, + ) + .unwrap(); + assert_eq!(votes, Uint128::one()); + let votes: Uint128 = context + .app + .wrap() + .query_wasm_smart( + dao_voting_incentives_cw20_addr.clone(), + &QueryMsg::Votes { + address: ADDR2.to_string(), + }, + ) + .unwrap(); + assert_eq!(votes, Uint128::one()); + let config: Config = context + .app + .wrap() + .query_wasm_smart(dao_voting_incentives_addr.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!(config.total_votes, Uint128::new(2)); + + // Blocks have passed the voting incentives' expirations + context.app.update_block(|x| x.height += 100); + + // Creating another proposal and voting should still succeed but unregister these vote hooks + context + .app + .execute_contract( + Addr::unchecked(ADDR1), + context.proposal_single_addr.clone(), + &dao_proposal_single::msg::ExecuteMsg::Propose(SingleChoiceProposeMsg { + title: "Test proposal".to_string(), + description: "Testing".to_string(), + msgs: vec![], + proposer: None, + vote: None, + }), + &[], + ) + .unwrap(); + let results = vote_yes_on_proposal(&mut context, 3u64); + for (i, result) in results.iter().enumerate() { + assert!(result.is_ok()); + if i == 0 { + // Vote hooks should unregister on the first instance of expiration + let events = &result.as_ref().unwrap().events; + assert!(events.iter().any(|x| x + .attributes + .iter() + .any(|y| y.key == "removed_vote_hook" + && y.value == format!("{0}:0", dao_voting_incentives_addr.clone())))); + assert!(events.iter().any(|x| x + .attributes + .iter() + .any(|y| y.key == "removed_vote_hook" + && y.value == format!("{0}:1", dao_voting_incentives_cw20_addr.clone())))); + } + } + + // Expire the vote hooks + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_voting_incentives_addr.clone(), + &ExecuteMsg::Expire {}, + &[], + ); + assert!(result.is_ok()); + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_voting_incentives_cw20_addr.clone(), + &ExecuteMsg::Expire {}, + &[], + ); + assert!(result.is_ok()); + + // Ensure expire errors if already expired + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_voting_incentives_addr.clone(), + &ExecuteMsg::Expire {}, + &[], + ); + assert!(result.is_err()); + + // Random person cannot claim + let result = context.app.execute_contract( + Addr::unchecked("random"), + dao_voting_incentives_addr.clone(), + &ExecuteMsg::Claim {}, + &[], + ); + assert!(result.is_err()); + + // Check rewards + let rewards: RewardResponse = context + .app + .wrap() + .query_wasm_smart( + dao_voting_incentives_addr.clone(), + &QueryMsg::Rewards { + address: ADDR1.to_string(), + }, + ) + .unwrap(); + assert_eq!( + rewards, + RewardResponse { + denom: CheckedDenom::Native(DENOM.to_string()), + amount: Uint128::new(500), + is_claimable: true, + } + ); + + // User claims rewards + let result = context.app.execute_contract( + Addr::unchecked(ADDR1), + dao_voting_incentives_addr.clone(), + &ExecuteMsg::Claim {}, + &[], + ); + assert!(result.is_ok()); + + // User balance has increased by 500, because 1000 reward with 2 voters during the period + let balance = context + .app + .wrap() + .query_balance(Addr::unchecked(ADDR1), DENOM) + .unwrap(); + assert_eq!(balance.amount, Uint128::new(500)); + + // User cannot claim again + let result = context.app.execute_contract( + Addr::unchecked(ADDR1), + dao_voting_incentives_addr.clone(), + &ExecuteMsg::Claim {}, + &[], + ); + assert!(result.is_err()); + + // User claims rewards cw20 + let result = context.app.execute_contract( + Addr::unchecked(ADDR1), + dao_voting_incentives_cw20_addr.clone(), + &ExecuteMsg::Claim {}, + &[], + ); + assert!(result.is_ok()); + + // User balance has increased by 500, because 1000 reward with 2 voters during the period + let balance_response: BalanceResponse = context + .app + .wrap() + .query_wasm_smart( + context.cw20_addr, + &Cw20QueryMsg::Balance { + address: ADDR1.to_string(), + }, + ) + .unwrap(); + assert_eq!(balance_response.balance, Uint128::new(500)); +} + +#[test] +pub fn test_expire_sends_funds_to_owner() { + let mut context = get_context(); + + // Create the voting incentives contract for native + // The expiration is 10 blocks from start (12345 height) + let dao_voting_incentives_addr = context + .app + .instantiate_contract( + context.dao_voting_incentives_code_id, + Addr::unchecked(ADMIN), + &InstantiateMsg { + owner: context.dao_addr.to_string(), + denom: UncheckedDenom::Native(DENOM.to_string()), + expiration: Expiration::AtHeight(12355u64), + }, + &[], + "dao_voting_incentives".to_string(), + None, + ) + .unwrap(); + + // Fund the incentives contracts for 1000 + context + .app + .send_tokens( + Addr::unchecked(ADMIN), + dao_voting_incentives_addr.clone(), + &[Coin { + denom: DENOM.to_string(), + amount: Uint128::new(1000), + }], + ) + .unwrap(); + + // Blocks have passed the voting incentives' expirations + context.app.update_block(|x| x.height += 100); + + // Expire the vote hooks + // No votes were received during the period, so the funds should be sent to the owner on expiration + let result = context.app.execute_contract( + Addr::unchecked(ADMIN), + dao_voting_incentives_addr.clone(), + &ExecuteMsg::Expire {}, + &[], + ); + assert!(result.is_ok()); + + // Ensure funds were sent to the DAO + let balance = context + .app + .wrap() + .query_balance(context.dao_addr, DENOM) + .unwrap(); + assert_eq!(balance.amount, Uint128::new(1000)); +} + +#[test] +pub fn test_migrate_update_version() { + let mut deps = mock_dependencies(); + cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap(); + migrate(deps.as_mut(), mock_env(), MigrateMsg::FromCompatible {}).unwrap(); + let version = cw2::get_contract_version(&deps.storage).unwrap(); + assert_eq!(version.version, CONTRACT_VERSION); + assert_eq!(version.contract, CONTRACT_NAME); +} diff --git a/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json b/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json index 31094252c..c64822ef6 100644 --- a/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json +++ b/contracts/proposal/dao-proposal-condorcet/schema/dao-proposal-condorcet.json @@ -1239,6 +1239,30 @@ } }, "additionalProperties": false + }, + { + "description": "Returns generic proposal information", + "type": "object", + "required": [ + "generic_proposal_info" + ], + "properties": { + "generic_proposal_info": { + "type": "object", + "required": [ + "proposal_id" + ], + "properties": { + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -1355,6 +1379,32 @@ "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" }, + "generic_proposal_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GenericProposalInfo", + "type": "object", + "required": [ + "proposer", + "start_height" + ], + "properties": { + "proposer": { + "$ref": "#/definitions/Addr" + }, + "start_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, "info": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "InfoResponse", diff --git a/contracts/proposal/dao-proposal-condorcet/src/contract.rs b/contracts/proposal/dao-proposal-condorcet/src/contract.rs index 3d3785002..903a1e3d9 100644 --- a/contracts/proposal/dao-proposal-condorcet/src/contract.rs +++ b/contracts/proposal/dao-proposal-condorcet/src/contract.rs @@ -260,6 +260,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Info {} => to_json_binary(&dao_interface::voting::InfoResponse { info: cw2::get_contract_version(deps.storage)?, }), + QueryMsg::GenericProposalInfo { proposal_id: _ } => unimplemented!(), } } 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 75729fda4..355379b3a 100644 --- a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json +++ b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json @@ -2177,6 +2177,30 @@ } }, "additionalProperties": false + }, + { + "description": "Returns generic proposal information", + "type": "object", + "required": [ + "generic_proposal_info" + ], + "properties": { + "generic_proposal_info": { + "type": "object", + "required": [ + "proposal_id" + ], + "properties": { + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -2679,6 +2703,32 @@ "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" }, + "generic_proposal_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GenericProposalInfo", + "type": "object", + "required": [ + "proposer", + "start_height" + ], + "properties": { + "proposer": { + "$ref": "#/definitions/Addr" + }, + "start_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, "get_vote": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "VoteResponse", diff --git a/contracts/proposal/dao-proposal-multiple/src/contract.rs b/contracts/proposal/dao-proposal-multiple/src/contract.rs index 6fc42f899..f5125f66c 100644 --- a/contracts/proposal/dao-proposal-multiple/src/contract.rs +++ b/contracts/proposal/dao-proposal-multiple/src/contract.rs @@ -26,6 +26,7 @@ use dao_voting::{ voting::{get_total_power, get_voting_power, validate_voting_period}, }; +use crate::state::{REMOVED_PROPOSAL_HOOKS_BY_INDEX, REMOVED_VOTE_HOOKS_BY_INDEX}; use crate::{msg::MigrateMsg, state::CREATION_POLICY}; use crate::{ msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, @@ -249,6 +250,7 @@ pub fn execute_propose( PROPOSALS.save(deps.storage, id, &proposal)?; + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let hooks = new_proposal_hooks(PROPOSAL_HOOKS, deps.storage, id, proposer.as_str())?; let sender = info.sender.clone(); @@ -343,6 +345,7 @@ pub fn execute_veto( PROPOSALS.save(deps.storage, proposal_id, &prop)?; // Add proposal status change hooks + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, @@ -437,6 +440,8 @@ pub fn execute_vote( prop.update_status(&env.block)?; PROPOSALS.save(deps.storage, proposal_id, &prop)?; let new_status = prop.status; + + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let change_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, @@ -444,6 +449,8 @@ pub fn execute_vote( old_status.to_string(), new_status.to_string(), )?; + + REMOVED_VOTE_HOOKS_BY_INDEX.remove(deps.storage); let vote_hooks = new_vote_hooks( VOTE_HOOKS, deps.storage, @@ -559,6 +566,7 @@ pub fn execute_execute( Response::default() }; + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, @@ -602,6 +610,7 @@ pub fn execute_close( PROPOSALS.save(deps.storage, proposal_id, &prop)?; + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, @@ -865,9 +874,16 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ProposalHooks {} => to_json_binary(&PROPOSAL_HOOKS.query_hooks(deps)?), QueryMsg::VoteHooks {} => to_json_binary(&VOTE_HOOKS.query_hooks(deps)?), QueryMsg::Dao {} => query_dao(deps), + QueryMsg::GenericProposalInfo { proposal_id } => { + query_generic_proposal_info(deps, proposal_id) + } } } +pub fn query_generic_proposal_info(deps: Deps, id: u64) -> StdResult { + to_json_binary(&PROPOSALS.load(deps.storage, id)?.into_generic()) +} + pub fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; to_json_binary(&config) @@ -1003,11 +1019,19 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { - let addr = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; + let addr = PROPOSAL_HOOKS.remove_hook_by_index_from_reply( + deps.storage, + idx, + REMOVED_PROPOSAL_HOOKS_BY_INDEX, + )?; Ok(Response::new().add_attribute("removed_proposal_hook", format!("{addr}:{idx}"))) } TaggedReplyId::FailedVoteHook(idx) => { - let addr = VOTE_HOOKS.remove_hook_by_index(deps.storage, idx)?; + let addr = VOTE_HOOKS.remove_hook_by_index_from_reply( + deps.storage, + idx, + REMOVED_VOTE_HOOKS_BY_INDEX, + )?; Ok(Response::new().add_attribute("removed vote hook", format!("{addr}:{idx}"))) } TaggedReplyId::PreProposeModuleInstantiation => { diff --git a/contracts/proposal/dao-proposal-multiple/src/proposal.rs b/contracts/proposal/dao-proposal-multiple/src/proposal.rs index 454a60762..5d760b477 100644 --- a/contracts/proposal/dao-proposal-multiple/src/proposal.rs +++ b/contracts/proposal/dao-proposal-multiple/src/proposal.rs @@ -3,6 +3,7 @@ use std::ops::Add; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, BlockInfo, StdError, StdResult, Uint128}; use cw_utils::Expiration; +use dao_interface::proposal::GenericProposalInfo; use dao_voting::{ multiple_choice::{ CheckedMultipleChoiceOption, MultipleChoiceOptionType, MultipleChoiceVotes, VotingStrategy, @@ -283,6 +284,13 @@ impl MultipleChoiceProposal { } Ok(false) } + + pub fn into_generic(self) -> GenericProposalInfo { + GenericProposalInfo { + proposer: self.proposer, + start_height: self.start_height, + } + } } #[cfg(test)] diff --git a/contracts/proposal/dao-proposal-multiple/src/state.rs b/contracts/proposal/dao-proposal-multiple/src/state.rs index 2261f6d03..26906d5cf 100644 --- a/contracts/proposal/dao-proposal-multiple/src/state.rs +++ b/contracts/proposal/dao-proposal-multiple/src/state.rs @@ -69,6 +69,10 @@ pub const BALLOTS: Map<(u64, &Addr), Ballot> = Map::new("ballots"); pub const PROPOSAL_HOOKS: Hooks = Hooks::new("proposal_hooks"); /// Consumers of vote hooks. pub const VOTE_HOOKS: Hooks = Hooks::new("vote_hooks"); +/// Lists of hook indexes removed to adjust for in submsg replies +pub const REMOVED_VOTE_HOOKS_BY_INDEX: Item> = Item::new("removed_vote_hooks_by_index"); +pub const REMOVED_PROPOSAL_HOOKS_BY_INDEX: Item> = + Item::new("removed_proposal_hooks_by_index"); /// The address of the pre-propose module associated with this /// proposal module (if any). pub const CREATION_POLICY: Item = Item::new("creation_policy"); 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 9738cadec..b06e82b80 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -2255,6 +2255,30 @@ } }, "additionalProperties": false + }, + { + "description": "Returns generic proposal information", + "type": "object", + "required": [ + "generic_proposal_info" + ], + "properties": { + "generic_proposal_info": { + "type": "object", + "required": [ + "proposal_id" + ], + "properties": { + "proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -2810,6 +2834,32 @@ "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" }, + "generic_proposal_info": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "GenericProposalInfo", + "type": "object", + "required": [ + "proposer", + "start_height" + ], + "properties": { + "proposer": { + "$ref": "#/definitions/Addr" + }, + "start_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, "get_vote": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "VoteResponse", diff --git a/contracts/proposal/dao-proposal-single/src/contract.rs b/contracts/proposal/dao-proposal-single/src/contract.rs index 1943081d1..49bf1ae91 100644 --- a/contracts/proposal/dao-proposal-single/src/contract.rs +++ b/contracts/proposal/dao-proposal-single/src/contract.rs @@ -27,7 +27,9 @@ use dao_voting::voting::{get_total_power, get_voting_power, validate_voting_peri use crate::msg::MigrateMsg; use crate::proposal::{next_proposal_id, SingleChoiceProposal}; -use crate::state::{Config, CREATION_POLICY}; +use crate::state::{ + Config, CREATION_POLICY, REMOVED_PROPOSAL_HOOKS_BY_INDEX, REMOVED_VOTE_HOOKS_BY_INDEX, +}; use crate::v1_state::{ v1_duration_to_v2, v1_expiration_to_v2, v1_status_to_v2, v1_threshold_to_v2, v1_votes_to_v2, }; @@ -250,6 +252,7 @@ pub fn execute_propose( PROPOSALS.save(deps.storage, id, &proposal)?; + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let hooks = new_proposal_hooks(PROPOSAL_HOOKS, deps.storage, id, proposer.as_str())?; // Auto cast vote if given. @@ -335,6 +338,7 @@ pub fn execute_veto( PROPOSALS.save(deps.storage, proposal_id, &prop)?; // Add proposal status change hooks + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, @@ -447,6 +451,7 @@ pub fn execute_execute( }; // Add proposal status change hooks + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, @@ -543,6 +548,7 @@ pub fn execute_vote( PROPOSALS.save(deps.storage, proposal_id, &prop)?; let new_status = prop.status; + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let change_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, @@ -551,6 +557,7 @@ pub fn execute_vote( new_status.to_string(), )?; + REMOVED_VOTE_HOOKS_BY_INDEX.remove(deps.storage); let vote_hooks = new_vote_hooks( VOTE_HOOKS, deps.storage, @@ -624,6 +631,7 @@ pub fn execute_close( PROPOSALS.save(deps.storage, proposal_id, &prop)?; // Add proposal status change hooks + REMOVED_PROPOSAL_HOOKS_BY_INDEX.remove(deps.storage); let proposal_status_changed_hooks = proposal_status_changed_hooks( PROPOSAL_HOOKS, deps.storage, @@ -845,9 +853,16 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ProposalCreationPolicy {} => query_creation_policy(deps), QueryMsg::ProposalHooks {} => to_json_binary(&PROPOSAL_HOOKS.query_hooks(deps)?), QueryMsg::VoteHooks {} => to_json_binary(&VOTE_HOOKS.query_hooks(deps)?), + QueryMsg::GenericProposalInfo { proposal_id } => { + query_generic_proposal_info(deps, proposal_id) + } } } +pub fn query_generic_proposal_info(deps: Deps, id: u64) -> StdResult { + to_json_binary(&PROPOSALS.load(deps.storage, id)?.into_generic()) +} + pub fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; to_json_binary(&config) @@ -1087,11 +1102,19 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { - let addr = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?; + let addr = PROPOSAL_HOOKS.remove_hook_by_index_from_reply( + deps.storage, + idx, + REMOVED_PROPOSAL_HOOKS_BY_INDEX, + )?; Ok(Response::new().add_attribute("removed_proposal_hook", format!("{addr}:{idx}"))) } TaggedReplyId::FailedVoteHook(idx) => { - let addr = VOTE_HOOKS.remove_hook_by_index(deps.storage, idx)?; + let addr = VOTE_HOOKS.remove_hook_by_index_from_reply( + deps.storage, + idx, + REMOVED_VOTE_HOOKS_BY_INDEX, + )?; Ok(Response::new().add_attribute("removed_vote_hook", format!("{addr}:{idx}"))) } TaggedReplyId::PreProposeModuleInstantiation => { diff --git a/contracts/proposal/dao-proposal-single/src/proposal.rs b/contracts/proposal/dao-proposal-single/src/proposal.rs index a597f3754..0cef6eec3 100644 --- a/contracts/proposal/dao-proposal-single/src/proposal.rs +++ b/contracts/proposal/dao-proposal-single/src/proposal.rs @@ -5,6 +5,7 @@ use crate::state::PROPOSAL_COUNT; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, BlockInfo, CosmosMsg, Decimal, Empty, StdResult, Storage, Uint128}; use cw_utils::Expiration; +use dao_interface::proposal::GenericProposalInfo; use dao_voting::status::Status; use dao_voting::threshold::{PercentageThreshold, Threshold}; use dao_voting::veto::VetoConfig; @@ -270,6 +271,13 @@ impl SingleChoiceProposal { } } } + + pub fn into_generic(self) -> GenericProposalInfo { + GenericProposalInfo { + proposer: self.proposer, + start_height: self.start_height, + } + } } #[cfg(test)] diff --git a/contracts/proposal/dao-proposal-single/src/state.rs b/contracts/proposal/dao-proposal-single/src/state.rs index 88e748b51..55fcf3cb6 100644 --- a/contracts/proposal/dao-proposal-single/src/state.rs +++ b/contracts/proposal/dao-proposal-single/src/state.rs @@ -74,6 +74,10 @@ pub const BALLOTS: Map<(u64, &Addr), Ballot> = Map::new("ballots"); pub const PROPOSAL_HOOKS: Hooks = Hooks::new("proposal_hooks"); /// Consumers of vote hooks. pub const VOTE_HOOKS: Hooks = Hooks::new("vote_hooks"); +/// Lists of hook indexes removed to adjust for in submsg replies +pub const REMOVED_VOTE_HOOKS_BY_INDEX: Item> = Item::new("removed_vote_hooks_by_index"); +pub const REMOVED_PROPOSAL_HOOKS_BY_INDEX: Item> = + Item::new("removed_proposal_hooks_by_index"); /// The address of the pre-propose module associated with this /// proposal module (if any). pub const CREATION_POLICY: Item = Item::new("creation_policy"); diff --git a/packages/cw-hooks/src/lib.rs b/packages/cw-hooks/src/lib.rs index fdbf43577..6f08a497d 100644 --- a/packages/cw-hooks/src/lib.rs +++ b/packages/cw-hooks/src/lib.rs @@ -102,6 +102,41 @@ impl<'a> Hooks<'a> { let hooks = hooks.into_iter().map(String::from).collect(); Ok(HooksResponse { hooks }) } + + /// Use this method to remove a hook by index from a reply + /// The removed_hook_indexes should be cleared when submsgs are sent + pub fn remove_hook_by_index_from_reply( + &self, + storage: &mut dyn Storage, + index: u64, + removed_hook_indexes: Item>, + ) -> Result { + let adjusted_index = match removed_hook_indexes.may_load(storage)? { + Some(mut removed_indexes) => { + let mut result = index; + + for removed_index in removed_indexes.iter() { + if removed_index < &index { + result -= 1; + } + } + + // Update the removed indexes + removed_indexes.push(index); + removed_hook_indexes.save(storage, &removed_indexes)?; + + result + } + None => { + // Set the removed indexes + removed_hook_indexes.save(storage, &vec![index])?; + + index + } + }; + + self.remove_hook_by_index(storage, adjusted_index) + } } #[cfg(test)] diff --git a/packages/dao-dao-macros/src/lib.rs b/packages/dao-dao-macros/src/lib.rs index 48611fddf..8b45a9a6e 100644 --- a/packages/dao-dao-macros/src/lib.rs +++ b/packages/dao-dao-macros/src/lib.rs @@ -392,6 +392,9 @@ pub fn proposal_module_query(metadata: TokenStream, input: TokenStream) -> Token /// next proposal created. #[returns(::std::primitive::u64)] NextProposalId {}, + /// Returns generic proposal information + #[returns(dao_interface::proposal::GenericProposalInfo)] + GenericProposalInfo { proposal_id: ::std::primitive::u64 }, } } .into(), diff --git a/packages/dao-dao-macros/tests/govmod.rs b/packages/dao-dao-macros/tests/govmod.rs index f69d281cb..5c437de73 100644 --- a/packages/dao-dao-macros/tests/govmod.rs +++ b/packages/dao-dao-macros/tests/govmod.rs @@ -24,5 +24,6 @@ fn proposal_module_query_derive() { Test::Foo | Test::Bar(_) | Test::Baz { .. } | Test::Dao {} => "yay", Test::Info {} => "yay", Test::NextProposalId {} => "yay", + Test::GenericProposalInfo { proposal_id: _ } => "yay", }; } diff --git a/packages/dao-hooks/src/proposal.rs b/packages/dao-hooks/src/proposal.rs index c98cfdc45..20f915d4a 100644 --- a/packages/dao-hooks/src/proposal.rs +++ b/packages/dao-hooks/src/proposal.rs @@ -8,7 +8,7 @@ use dao_voting::{ }; /// An enum representing proposal hook messages. -/// Either a new propsoal hook, fired when a new proposal is created, +/// Either a new proposal hook, fired when a new proposal is created, /// or a proposal status hook, fired when a proposal changes status. #[cw_serde] pub enum ProposalHookMsg { diff --git a/packages/dao-interface/src/msg.rs b/packages/dao-interface/src/msg.rs index 969288433..efdc777ab 100644 --- a/packages/dao-interface/src/msg.rs +++ b/packages/dao-interface/src/msg.rs @@ -193,6 +193,9 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + // Gets a proposal module + #[returns(crate::state::ProposalModule)] + ProposalModule { address: String }, /// Gets the active proposal modules associated with the /// contract. #[returns(Vec)] diff --git a/packages/dao-interface/src/proposal.rs b/packages/dao-interface/src/proposal.rs index 77076d5c2..f71868986 100644 --- a/packages/dao-interface/src/proposal.rs +++ b/packages/dao-interface/src/proposal.rs @@ -1,4 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Addr; use cw2::ContractVersion; #[cw_serde] @@ -6,6 +7,12 @@ pub struct InfoResponse { pub info: ContractVersion, } +#[cw_serde] +pub struct GenericProposalInfo { + pub proposer: Addr, + pub start_height: u64, +} + #[cw_serde] #[derive(QueryResponses)] pub enum Query { @@ -19,6 +26,9 @@ pub enum Query { /// next proposal created. #[returns(::std::primitive::u64)] NextProposalId {}, + /// Returns generic proposal information + #[returns(GenericProposalInfo)] + GenericProposalInfo { proposal_id: ::std::primitive::u64 }, } mod tests { @@ -34,6 +44,7 @@ mod tests { Query::Dao {} => (), Query::Info {} => (), Query::NextProposalId {} => (), + Query::GenericProposalInfo { proposal_id: _ } => (), } } } diff --git a/packages/dao-testing/Cargo.toml b/packages/dao-testing/Cargo.toml index 9b191be86..fa880b4d3 100644 --- a/packages/dao-testing/Cargo.toml +++ b/packages/dao-testing/Cargo.toml @@ -62,3 +62,5 @@ dao-voting-onft-staked = { workspace = true } dao-voting-token-staked = { workspace = true } voting-v1 = { workspace = true } stake-cw20-v03 = { workspace = true } +dao-proposal-incentives = { workspace = true } +dao-voting-incentives = { workspace = true } \ No newline at end of file diff --git a/packages/dao-testing/src/contracts.rs b/packages/dao-testing/src/contracts.rs index 1739ed124..082921036 100644 --- a/packages/dao-testing/src/contracts.rs +++ b/packages/dao-testing/src/contracts.rs @@ -216,3 +216,25 @@ pub fn dao_test_custom_factory() -> Box> { .with_reply(dao_test_custom_factory::contract::reply); Box::new(contract) } + +pub fn dao_proposal_incentives_contract() -> Box> { + let contract = ContractWrapper::new( + dao_proposal_incentives::contract::execute, + dao_proposal_incentives::contract::instantiate, + dao_proposal_incentives::contract::query, + ) + .with_migrate(dao_proposal_incentives::contract::migrate); + + Box::new(contract) +} + +pub fn dao_voting_incentives_contract() -> Box> { + let contract = ContractWrapper::new( + dao_voting_incentives::contract::execute, + dao_voting_incentives::contract::instantiate, + dao_voting_incentives::contract::query, + ) + .with_migrate(dao_voting_incentives::contract::migrate); + + Box::new(contract) +}