Repository: DA0-DA0/dao-contracts
Pull request #205 (2022-03-21 18:01:27)

CW4 Voting V1 Contract
Allows voting based off of a cw4 group contract.

Instantiated with initial members.
Listens for hooks to keep track of weights and snapshot them at heights.
Supports voting query interface.

Feedback appreciated


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6f904d2b..a60769f3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -559,6 +559,26 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw4-voting"
+version = "0.1.0"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cosmwasm-storage",
+ "cw-governance-interface",
+ "cw-governance-macros",
+ "cw-multi-test 0.12.1",
+ "cw-storage-plus 0.12.1",
+ "cw-utils 0.12.1",
+ "cw2 0.12.1",
+ "cw4",
+ "cw4-group",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "der"
 version = "0.4.5"
diff --git a/contracts/cw4-voting/.cargo/config b/contracts/cw4-voting/.cargo/config
new file mode 100644
index 00000000..336b618a
--- /dev/null
+++ b/contracts/cw4-voting/.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/cw4-voting/.gitignore b/contracts/cw4-voting/.gitignore
new file mode 100644
index 00000000..dfdaaa6b
--- /dev/null
+++ b/contracts/cw4-voting/.gitignore
@@ -0,0 +1,15 @@
+# Build results
+/target
+
+# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327)
+.cargo-ok
+
+# Text file backups
+**/*.rs.bk
+
+# macOS
+.DS_Store
+
+# IDEs
+*.iml
+.idea
diff --git a/contracts/cw4-voting/Cargo.lock b/contracts/cw4-voting/Cargo.lock
new file mode 100644
index 00000000..c9737778
--- /dev/null
+++ b/contracts/cw4-voting/Cargo.lock
@@ -0,0 +1,596 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "const-oid"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22"
+
+[[package]]
+name = "cosmwasm-crypto"
+version = "1.0.0-beta4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f903ebbabc0d4880dbc76148efb8be8fc29fa4bf294c440c3d70da1c8bcafff7"
+dependencies = [
+ "digest",
+ "ed25519-zebra",
+ "k256",
+ "rand_core 0.5.1",
+ "thiserror",
+]
+
+[[package]]
+name = "cosmwasm-derive"
+version = "1.0.0-beta4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "832bebef577ecb394603de8e2bf0de429b74aa364e17dec18e15ce37e71b0cae"
+dependencies = [
+ "syn",
+]
+
+[[package]]
+name = "cosmwasm-schema"
+version = "1.0.0-beta"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e4b3f6933f94acdd3ddb931af4870c2002e3331a4a8b247a4ef070dd31ccb0"
+dependencies = [
+ "schemars",
+ "serde_json",
+]
+
+[[package]]
+name = "cosmwasm-std"
+version = "1.0.0-beta4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6238c45840cc9de5a39f0f619e3a4f7c38c5d2c6ac9e3e4d72751ee045e6d7da"
+dependencies = [
+ "base64",
+ "cosmwasm-crypto",
+ "cosmwasm-derive",
+ "schemars",
+ "serde",
+ "serde-json-wasm",
+ "thiserror",
+ "uint",
+]
+
+[[package]]
+name = "cosmwasm-storage"
+version = "1.0.0-beta"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "665cf97ad42be46936f6e6739711824a9bf21c440a6c98e185a3404aef0c3b81"
+dependencies = [
+ "cosmwasm-std",
+ "serde",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.3",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
+dependencies = [
+ "byteorder",
+ "digest",
+ "rand_core 0.5.1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "cw-storage-plus"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261"
+dependencies = [
+ "cosmwasm-std",
+ "schemars",
+ "serde",
+]
+
+[[package]]
+name = "cw2"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1d81d7c359d6c1fba3aa83dad7ec6f999e512571380ae62f81257c3db569743"
+dependencies = [
+ "cosmwasm-std",
+ "cw-storage-plus",
+ "schemars",
+ "serde",
+]
+
+[[package]]
+name = "der"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e21d2d0f22cde6e88694108429775c0219760a07779bf96503b434a03d7412"
+dependencies = [
+ "const-oid",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
+
+[[package]]
+name = "ecdsa"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713c32426287891008edb98f8b5c6abb2130aa043c93a818728fcda78606f274"
+dependencies = [
+ "der",
+ "elliptic-curve",
+ "hmac",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-zebra"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409"
+dependencies = [
+ "curve25519-dalek",
+ "hex",
+ "rand_core 0.5.1",
+ "serde",
+ "sha2",
+ "thiserror",
+]
+
+[[package]]
+name = "elliptic-curve"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069397e10739989e400628cbc0556a817a8a64119d7a2315767f4456e1332c23"
+dependencies = [
+ "crypto-bigint",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core 0.6.3",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ff"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63eec06c61e487eecf0f7e6e6372e596a81922c28d33e645d6983ca6493a1af0"
+dependencies = [
+ "rand_core 0.6.3",
+ "subtle",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "group"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912"
+dependencies = [
+ "ff",
+ "rand_core 0.6.3",
+ "subtle",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+dependencies = [
+ "crypto-mac",
+ "digest",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "k256"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea"
+dependencies = [
+ "cfg-if",
+ "ecdsa",
+ "elliptic-curve",
+ "sha2",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "pkcs8"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbee84ed13e44dd82689fa18348a49934fa79cc774a344c42fc9b301c71b140a"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom 0.2.3",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "schemars"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc6ab463ae35acccb5cba66c0084c985257b797d288b6050cc2f6ac1b266cb78"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "902fdfbcf871ae8f653bddf4b2c05905ddaabc08f69d32a915787e3be0d31356"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-json-wasm"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "042ac496d97e5885149d34139bad1d617192770d7eb8f1866da2317ff4501853"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "signature"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335"
+dependencies = [
+ "digest",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "spki"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "987637c5ae6b3121aba9d513f869bd2bff11c4cc086c22473befd6649c0bd521"
+dependencies = [
+ "der",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[package]]
+name = "syn"
+version = "1.0.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "testgen-local"
+version = "0.1.0"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cosmwasm-storage",
+ "cw-storage-plus",
+ "cw2",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "typenum"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
+
+[[package]]
+name = "uint"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f"
+dependencies = [
+ "byteorder",
+ "crunchy",
+ "hex",
+ "static_assertions",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "zeroize"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd"
diff --git a/contracts/cw4-voting/Cargo.toml b/contracts/cw4-voting/Cargo.toml
new file mode 100644
index 00000000..015d85d8
--- /dev/null
+++ b/contracts/cw4-voting/Cargo.toml
@@ -0,0 +1,47 @@
+[package]
+name = "cw4-voting"
+version = "0.1.0"
+authors = ["Callum Anderson <callumanderson745@gmail.com>"]
+edition = "2018"
+
+exclude = [
+  # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
+  "contract.wasm",
+  "hash.txt",
+]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+# for more explicit tests, cargo test --features=backtraces
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all instantiate/execute/query exports
+library = []
+
+[package.metadata.scripts]
+optimize = """docker run --rm -v "$(pwd)":/code \
+  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
+  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
+  cosmwasm/rust-optimizer:0.12.5
+"""
+
+[dependencies]
+cosmwasm-std = { version = "1.0.0-beta5" }
+cosmwasm-storage = { version = "1.0.0-beta5" }
+cw-storage-plus = "0.12"
+cw2 = "0.12"
+cw-utils = "0.12"
+schemars = "0.8"
+serde = { version = "1.0", default-features = false, features = ["derive"] }
+thiserror = { version = "1.0" }
+cw-governance-macros = { version = "0.1.0", path = "../cw-governance-macros" }
+cw-governance-interface = { version = "0.1.0", path = "../cw-governance-interface" }
+cw4-group = "0.11"
+cw4 = "0.11"
+
+[dev-dependencies]
+cosmwasm-schema = { version = "1.0.0-beta5" }
+cw-multi-test = "0.12"
diff --git a/contracts/cw4-voting/README.md b/contracts/cw4-voting/README.md
new file mode 100644
index 00000000..fc5a3130
--- /dev/null
+++ b/contracts/cw4-voting/README.md
@@ -0,0 +1,4 @@
+# cw4-group voting
+
+A simple voting power module which determines voting power based on
+the weight of a user in a cw4-group contract.
diff --git a/contracts/cw4-voting/examples/schema.rs b/contracts/cw4-voting/examples/schema.rs
new file mode 100644
index 00000000..9c8b90d0
--- /dev/null
+++ b/contracts/cw4-voting/examples/schema.rs
@@ -0,0 +1,17 @@
+use std::env::current_dir;
+use std::fs::create_dir_all;
+
+use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
+
+use cw4_voting::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+
+fn main() {
+    let mut out_dir = current_dir().unwrap();
+    out_dir.push("schema");
+    create_dir_all(&out_dir).unwrap();
+    remove_schemas(&out_dir).unwrap();
+
+    export_schema(&schema_for!(InstantiateMsg), &out_dir);
+    export_schema(&schema_for!(ExecuteMsg), &out_dir);
+    export_schema(&schema_for!(QueryMsg), &out_dir);
+}
diff --git a/contracts/cw4-voting/schema/count_response.json b/contracts/cw4-voting/schema/count_response.json
new file mode 100644
index 00000000..fa1e81f3
--- /dev/null
+++ b/contracts/cw4-voting/schema/count_response.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "CountResponse",
+  "type": "object",
+  "required": [
+    "count"
+  ],
+  "properties": {
+    "count": {
+      "type": "integer",
+      "format": "int32"
+    }
+  }
+}
diff --git a/contracts/cw4-voting/schema/execute_msg.json b/contracts/cw4-voting/schema/execute_msg.json
new file mode 100644
index 00000000..6fa0a75d
--- /dev/null
+++ b/contracts/cw4-voting/schema/execute_msg.json
@@ -0,0 +1,39 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ExecuteMsg",
+  "anyOf": [
+    {
+      "type": "object",
+      "required": [
+        "increment"
+      ],
+      "properties": {
+        "increment": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "reset"
+      ],
+      "properties": {
+        "reset": {
+          "type": "object",
+          "required": [
+            "count"
+          ],
+          "properties": {
+            "count": {
+              "type": "integer",
+              "format": "int32"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    }
+  ]
+}
diff --git a/contracts/cw4-voting/schema/instantiate_msg.json b/contracts/cw4-voting/schema/instantiate_msg.json
new file mode 100644
index 00000000..e794ec1d
--- /dev/null
+++ b/contracts/cw4-voting/schema/instantiate_msg.json
@@ -0,0 +1,14 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "InstantiateMsg",
+  "type": "object",
+  "required": [
+    "count"
+  ],
+  "properties": {
+    "count": {
+      "type": "integer",
+      "format": "int32"
+    }
+  }
+}
diff --git a/contracts/cw4-voting/schema/query_msg.json b/contracts/cw4-voting/schema/query_msg.json
new file mode 100644
index 00000000..be0245bf
--- /dev/null
+++ b/contracts/cw4-voting/schema/query_msg.json
@@ -0,0 +1,18 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "QueryMsg",
+  "anyOf": [
+    {
+      "type": "object",
+      "required": [
+        "get_count"
+      ],
+      "properties": {
+        "get_count": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    }
+  ]
+}
diff --git a/contracts/cw4-voting/schema/state.json b/contracts/cw4-voting/schema/state.json
new file mode 100644
index 00000000..e18725d1
--- /dev/null
+++ b/contracts/cw4-voting/schema/state.json
@@ -0,0 +1,24 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "State",
+  "type": "object",
+  "required": [
+    "count",
+    "owner"
+  ],
+  "properties": {
+    "count": {
+      "type": "integer",
+      "format": "int32"
+    },
+    "owner": {
+      "$ref": "#/definitions/Addr"
+    }
+  },
+  "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"
+    }
+  }
+}
diff --git a/contracts/cw4-voting/src/contract.rs b/contracts/cw4-voting/src/contract.rs
new file mode 100644
index 00000000..2408f19a
--- /dev/null
+++ b/contracts/cw4-voting/src/contract.rs
@@ -0,0 +1,203 @@
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cosmwasm_std::{
+    to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdError, StdResult,
+    SubMsg, Uint128, WasmMsg,
+};
+use cw2::set_contract_version;
+use cw_utils::parse_reply_instantiate_data;
+
+use crate::error::ContractError;
+use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+use crate::state::{DAO_ADDRESS, GROUP_CONTRACT, TOTAL_WEIGHT, USER_WEIGHTS};
+
+const CONTRACT_NAME: &str = "crates.io:cw4-voting";
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+const INSTANTIATE_GROUP_REPLY_ID: u64 = 0;
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn instantiate(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: InstantiateMsg,
+) -> Result<Response, ContractError> {
+    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
+    if msg.initial_members.is_empty() {
+        return Err(ContractError::NoMembers {});
+    }
+
+    let mut total_weight = Uint128::zero();
+    for member in msg.initial_members.iter() {
+        let member_addr = deps.api.addr_validate(&member.addr)?;
+        let weight = Uint128::from(member.weight);
+        USER_WEIGHTS.save(deps.storage, &member_addr, &weight, env.block.height)?;
+        total_weight += weight;
+    }
+
+    if total_weight.is_zero() {
+        return Err(ContractError::ZeroTotalWeight {});
+    }
+    TOTAL_WEIGHT.save(deps.storage, &total_weight, env.block.height)?;
+
+    // We need to set ourself as the CW4 admin it is then transferred to the DAO in the reply
+    let msg = WasmMsg::Instantiate {
+        admin: Some(info.sender.to_string()),
+        code_id: msg.cw4_group_code_id,
+        msg: to_binary(&cw4_group::msg::InstantiateMsg {
+            admin: Some(env.contract.address.to_string()),
+            members: msg.initial_members,
+        })?,
+        funds: vec![],
+        label: env.contract.address.to_string(),
+    };
+    let msg = SubMsg::reply_on_success(msg, INSTANTIATE_GROUP_REPLY_ID);
+
+    DAO_ADDRESS.save(deps.storage, &info.sender)?;
+
+    Ok(Response::new()
+        .add_attribute("action", "instantiate")
+        .add_submessage(msg))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<Response, ContractError> {
+    match msg {
+        ExecuteMsg::MemberChangedHook { diffs } => {
+            execute_member_changed_hook(deps, env, info, diffs)
+        }
+    }
+}
+
+pub fn execute_member_changed_hook(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    diffs: Vec<cw4::MemberDiff>,
+) -> Result<Response, ContractError> {
+    let group_contract = GROUP_CONTRACT.load(deps.storage)?;
+    if info.sender != group_contract {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    let total_weight = TOTAL_WEIGHT.load(deps.storage)?;
+    // As difference can be negative we need to keep track of both
+    // In seperate counters to apply at once and prevent underflow
+    let mut positive_difference: Uint128 = Uint128::zero();
+    let mut negative_difference: Uint128 = Uint128::zero();
+    for diff in diffs {
+        let user_address = deps.api.addr_validate(&diff.key)?;
+        let weight = diff.new.unwrap_or_default();
+        let old = diff.old.unwrap_or_default();
+        // Do we need to add to positive difference or negative difference
+        if weight > old {
+            positive_difference += Uint128::from(weight - old);
+        } else {
+            negative_difference += Uint128::from(old - weight);
+        }
+        USER_WEIGHTS.save(
+            deps.storage,
+            &user_address,
+            &Uint128::from(weight),
+            env.block.height,
+        )?;
+    }
+    let new_total_weight = total_weight
+        .checked_add(positive_difference)
+        .map_err(StdError::overflow)?
+        .checked_sub(negative_difference)
+        .map_err(StdError::overflow)?;
+    TOTAL_WEIGHT.save(deps.storage, &new_total_weight, env.block.height)?;
+
+    Ok(Response::new()
+        .add_attribute("action", "member_changed_hook")
+        .add_attribute("total_weight", new_total_weight.to_string()))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::VotingPowerAtHeight { address, height } => {
+            query_voting_power_at_height(deps, env, address, height)
+        }
+        QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height),
+        QueryMsg::Info {} => query_info(deps),
+        QueryMsg::GroupContract {} => to_binary(&GROUP_CONTRACT.load(deps.storage)?),
+        QueryMsg::Dao {} => to_binary(&DAO_ADDRESS.load(deps.storage)?),
+    }
+}
+
+pub fn query_voting_power_at_height(
+    deps: Deps,
+    env: Env,
+    address: String,
+    height: Option<u64>,
+) -> StdResult<Binary> {
+    let address = deps.api.addr_validate(&address)?;
+    let height = height.unwrap_or(env.block.height);
+    let power = USER_WEIGHTS
+        .may_load_at_height(deps.storage, &address, height)?
+        .unwrap_or_default();
+
+    to_binary(&cw_governance_interface::voting::VotingPowerAtHeightResponse { power, height })
+}
+
+pub fn query_total_power_at_height(deps: Deps, env: Env, height: Option<u64>) -> StdResult<Binary> {
+    let height = height.unwrap_or(env.block.height);
+    let power = TOTAL_WEIGHT
+        .may_load_at_height(deps.storage, height)?
+        .unwrap_or_default();
+    to_binary(&cw_governance_interface::voting::TotalPowerAtHeightResponse { power, height })
+}
+
+pub fn query_info(deps: Deps) -> StdResult<Binary> {
+    let info = cw2::get_contract_version(deps.storage)?;
+    to_binary(&cw_governance_interface::voting::InfoResponse { info })
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
+    match msg.id {
+        INSTANTIATE_GROUP_REPLY_ID => {
+            let res = parse_reply_instantiate_data(msg);
+            match res {
+                Ok(res) => {
+                    let group_contract = GROUP_CONTRACT.may_load(deps.storage)?;
+                    if group_contract.is_some() {
+                        return Err(ContractError::DuplicateGroupContract {});
+                    }
+                    let group_contract = deps.api.addr_validate(&res.contract_address)?;
+                    let dao_address = DAO_ADDRESS.load(deps.storage)?;
+                    GROUP_CONTRACT.save(deps.storage, &group_contract)?;
+                    let msg1 = WasmMsg::Execute {
+                        contract_addr: group_contract.to_string(),
+                        msg: to_binary(&cw4_group::msg::ExecuteMsg::AddHook {
+                            addr: env.contract.address.to_string(),
+                        })?,
+                        funds: vec![],
+                    };
+                    // Transfer admin status to the DAO
+                    let msg2 = WasmMsg::Execute {
+                        contract_addr: group_contract.to_string(),
+                        msg: to_binary(&cw4_group::msg::ExecuteMsg::UpdateAdmin {
+                            admin: Some(dao_address.to_string()),
+                        })?,
+                        funds: vec![],
+                    };
+                    Ok(Response::default()
+                        .add_attribute("group_contract_address", group_contract)
+                        .add_message(msg1)
+                        .add_message(msg2))
+                }
+                Err(_) => Err(ContractError::GroupContractInstantiateError {}),
+            }
+        }
+        _ => Err(ContractError::UnknownReplyId { id: msg.id }),
+    }
+}
diff --git a/contracts/cw4-voting/src/error.rs b/contracts/cw4-voting/src/error.rs
new file mode 100644
index 00000000..d5bb7be3
--- /dev/null
+++ b/contracts/cw4-voting/src/error.rs
@@ -0,0 +1,26 @@
+use cosmwasm_std::StdError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("Can not change the contract's token after it has been set")]
+    DuplicateGroupContract {},
+
+    #[error("Error occured whilst instantiating group contract")]
+    GroupContractInstantiateError {},
+
+    #[error("Cannot instantiate a group contract with no initial members")]
+    NoMembers {},
+
+    #[error("Total weight of the CW4 contract cannot be zero")]
+    ZeroTotalWeight {},
+
+    #[error("Got a submessage reply with unknown id: {id}")]
+    UnknownReplyId { id: u64 },
+}
diff --git a/contracts/cw4-voting/src/lib.rs b/contracts/cw4-voting/src/lib.rs
new file mode 100644
index 00000000..8e58a2f1
--- /dev/null
+++ b/contracts/cw4-voting/src/lib.rs
@@ -0,0 +1,9 @@
+pub mod contract;
+mod error;
+pub mod msg;
+pub mod state;
+
+#[cfg(test)]
+mod tests;
+
+pub use crate::error::ContractError;
diff --git a/contracts/cw4-voting/src/msg.rs b/contracts/cw4-voting/src/msg.rs
new file mode 100644
index 00000000..ec213455
--- /dev/null
+++ b/contracts/cw4-voting/src/msg.rs
@@ -0,0 +1,23 @@
+use cw_governance_macros::voting_query;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InstantiateMsg {
+    pub cw4_group_code_id: u64,
+    pub initial_members: Vec<cw4::Member>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ExecuteMsg {
+    MemberChangedHook { diffs: Vec<cw4::MemberDiff> },
+}
+
+#[voting_query]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum QueryMsg {
+    GroupContract {},
+    Dao {},
+}
diff --git a/contracts/cw4-voting/src/state.rs b/contracts/cw4-voting/src/state.rs
new file mode 100644
index 00000000..36d0904d
--- /dev/null
+++ b/contracts/cw4-voting/src/state.rs
@@ -0,0 +1,19 @@
+use cosmwasm_std::{Addr, Uint128};
+use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy};
+
+pub const USER_WEIGHTS: SnapshotMap<&Addr, Uint128> = SnapshotMap::new(
+    "user_weights",
+    "user_weights__checkpoints",
+    "user_weights__changelog",
+    Strategy::EveryBlock,
+);
+
+pub const TOTAL_WEIGHT: SnapshotItem<Uint128> = SnapshotItem::new(
+    "total_weight",
+    "total_weight__checkpoints",
+    "total_weight__changelog",
+    Strategy::EveryBlock,
+);
+
+pub const GROUP_CONTRACT: Item<Addr> = Item::new("group_contract");
+pub const DAO_ADDRESS: Item<Addr> = Item::new("dao_address");


Test patch:
diff --git a/contracts/cw4-voting/src/tests.rs b/contracts/cw4-voting/src/tests.rs
new file mode 100644
index 00000000..1a552f05
--- /dev/null
+++ b/contracts/cw4-voting/src/tests.rs
@@ -0,0 +1,408 @@
+use cosmwasm_std::{Addr, Empty, Uint128};
+use cw2::ContractVersion;
+use cw_governance_interface::voting::{
+    InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse,
+};
+use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor};
+
+use crate::msg::{InstantiateMsg, QueryMsg};
+
+const DAO_ADDR: &str = "dao";
+const ADDR1: &str = "addr1";
+const ADDR2: &str = "addr2";
+const ADDR3: &str = "addr3";
+
+fn cw4_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw4_group::contract::execute,
+        cw4_group::contract::instantiate,
+        cw4_group::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn voting_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        crate::contract::execute,
+        crate::contract::instantiate,
+        crate::contract::query,
+    )
+    .with_reply(crate::contract::reply);
+    Box::new(contract)
+}
+
+fn instantiate_voting(app: &mut App, voting_id: u64, msg: InstantiateMsg) -> Addr {
+    app.instantiate_contract(
+        voting_id,
+        Addr::unchecked(DAO_ADDR),
+        &msg,
+        &[],
+        "voting module",
+        None,
+    )
+    .unwrap()
+}
+
+fn setup_test_case(app: &mut App) -> Addr {
+    let cw4_id = app.store_code(cw4_contract());
+    let voting_id = app.store_code(voting_contract());
+
+    let members = vec![
+        cw4::Member {
+            addr: ADDR1.to_string(),
+            weight: 1,
+        },
+        cw4::Member {
+            addr: ADDR2.to_string(),
+            weight: 1,
+        },
+        cw4::Member {
+            addr: ADDR3.to_string(),
+            weight: 1,
+        },
+    ];
+    instantiate_voting(
+        app,
+        voting_id,
+        InstantiateMsg {
+            cw4_group_code_id: cw4_id,
+            initial_members: members,
+        },
+    )
+}
+
+#[test]
+fn test_instantiate() {
+    let mut app = App::default();
+    // Valid instantiate no panics
+    let _voting_addr = setup_test_case(&mut app);
+
+    // Instantiate with no members, error
+    let voting_id = app.store_code(voting_contract());
+    let cw4_id = app.store_code(cw4_contract());
+    let msg = InstantiateMsg {
+        cw4_group_code_id: cw4_id,
+        initial_members: vec![],
+    };
+    let _err = app
+        .instantiate_contract(
+            voting_id,
+            Addr::unchecked(DAO_ADDR),
+            &msg,
+            &[],
+            "voting module",
+            None,
+        )
+        .unwrap_err();
+
+    // Instantiate with members but no weight
+    let msg = InstantiateMsg {
+        cw4_group_code_id: cw4_id,
+        initial_members: vec![
+            cw4::Member {
+                addr: ADDR1.to_string(),
+                weight: 0,
+            },
+            cw4::Member {
+                addr: ADDR2.to_string(),
+                weight: 0,
+            },
+            cw4::Member {
+                addr: ADDR3.to_string(),
+                weight: 0,
+            },
+        ],
+    };
+    let _err = app
+        .instantiate_contract(
+            voting_id,
+            Addr::unchecked(DAO_ADDR),
+            &msg,
+            &[],
+            "voting module",
+            None,
+        )
+        .unwrap_err();
+}
+
+#[test]
+fn test_contract_info() {
+    let mut app = App::default();
+    let voting_addr = setup_test_case(&mut app);
+
+    let info: InfoResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::Info {})
+        .unwrap();
+    assert_eq!(
+        info,
+        InfoResponse {
+            info: ContractVersion {
+                contract: "crates.io:cw4-voting".to_string(),
+                version: env!("CARGO_PKG_VERSION").to_string()
+            }
+        }
+    );
+
+    // Ensure group contract is set
+    let _group_contract: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::GroupContract {})
+        .unwrap();
+
+    let dao_contract: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::Dao {})
+        .unwrap();
+    assert_eq!(dao_contract, Addr::unchecked(DAO_ADDR));
+}
+
+#[test]
+fn test_power_at_height() {
+    let mut app = App::default();
+    let voting_addr = setup_test_case(&mut app);
+    app.update_block(next_block);
+
+    let cw4_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::GroupContract {})
+        .unwrap();
+
+    let addr1_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: ADDR1.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(addr1_voting_power.power, Uint128::new(1u128));
+    assert_eq!(addr1_voting_power.height, app.block_info().height);
+
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight { height: None },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(3u128));
+    assert_eq!(total_voting_power.height, app.block_info().height);
+
+    // Update ADDR1's weight to 2
+    let msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
+        remove: vec![],
+        add: vec![cw4::Member {
+            addr: ADDR1.to_string(),
+            weight: 2,
+        }],
+    };
+
+    app.execute_contract(Addr::unchecked(DAO_ADDR), cw4_addr.clone(), &msg, &[])
+        .unwrap();
+    app.update_block(next_block);
+
+    // Should now be 2
+    let addr1_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: ADDR1.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(addr1_voting_power.power, Uint128::new(2u128));
+    assert_eq!(addr1_voting_power.height, app.block_info().height);
+
+    // Check we can still get the 1 weight he had last block
+    let addr1_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: ADDR1.to_string(),
+                height: Some(app.block_info().height - 1),
+            },
+        )
+        .unwrap();
+    assert_eq!(addr1_voting_power.power, Uint128::new(1u128));
+    assert_eq!(addr1_voting_power.height, app.block_info().height - 1);
+
+    // Check total power is now 4
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight { height: None },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(4u128));
+    assert_eq!(total_voting_power.height, app.block_info().height);
+
+    // Check total power for last block is 3
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight {
+                height: Some(app.block_info().height - 1),
+            },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(3u128));
+    assert_eq!(total_voting_power.height, app.block_info().height - 1);
+
+    // Update ADDR1's weight back to 1
+    let msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
+        remove: vec![],
+        add: vec![cw4::Member {
+            addr: ADDR1.to_string(),
+            weight: 1,
+        }],
+    };
+
+    app.execute_contract(Addr::unchecked(DAO_ADDR), cw4_addr.clone(), &msg, &[])
+        .unwrap();
+    app.update_block(next_block);
+
+    // Should now be 1 again
+    let addr1_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: ADDR1.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(addr1_voting_power.power, Uint128::new(1u128));
+    assert_eq!(addr1_voting_power.height, app.block_info().height);
+
+    // Check total power for current block is now 3
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight { height: None },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(3u128));
+    assert_eq!(total_voting_power.height, app.block_info().height);
+
+    // Check total power for last block is 4
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight {
+                height: Some(app.block_info().height - 1),
+            },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(4u128));
+    assert_eq!(total_voting_power.height, app.block_info().height - 1);
+
+    // Remove address 2 completely
+    let msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
+        remove: vec![ADDR2.to_string()],
+        add: vec![],
+    };
+
+    app.execute_contract(Addr::unchecked(DAO_ADDR), cw4_addr.clone(), &msg, &[])
+        .unwrap();
+    app.update_block(next_block);
+
+    // ADDR2 power is now 0
+    let addr2_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: ADDR2.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(addr2_voting_power.power, Uint128::zero());
+    assert_eq!(addr2_voting_power.height, app.block_info().height);
+
+    // Check total power for current block is now 2
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight { height: None },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(2u128));
+    assert_eq!(total_voting_power.height, app.block_info().height);
+
+    // Check total power for last block is 3
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight {
+                height: Some(app.block_info().height - 1),
+            },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(3u128));
+    assert_eq!(total_voting_power.height, app.block_info().height - 1);
+
+    // Readd ADDR2 with 10 power
+    let msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
+        remove: vec![],
+        add: vec![cw4::Member {
+            addr: ADDR2.to_string(),
+            weight: 10,
+        }],
+    };
+
+    app.execute_contract(Addr::unchecked(DAO_ADDR), cw4_addr, &msg, &[])
+        .unwrap();
+    app.update_block(next_block);
+
+    // ADDR2 power is now 10
+    let addr2_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: ADDR2.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(addr2_voting_power.power, Uint128::new(10u128));
+    assert_eq!(addr2_voting_power.height, app.block_info().height);
+
+    // Check total power for current block is now 12
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight { height: None },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(12u128));
+    assert_eq!(total_voting_power.height, app.block_info().height);
+
+    // Check total power for last block is 2
+    let total_voting_power: TotalPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr,
+            &QueryMsg::TotalPowerAtHeight {
+                height: Some(app.block_info().height - 1),
+            },
+        )
+        .unwrap();
+    assert_eq!(total_voting_power.power, Uint128::new(2u128));
+    assert_eq!(total_voting_power.height, app.block_info().height - 1);
+}