Repository: DA0-DA0/dao-contracts
Pull request #199 (2022-03-15 13:01:06)

Staked balances v1 voting contract
Addresses #194 
Staked balances V1 voting contract

Also allows existing staking contract to be used as per my existing PR.

Full test coverage.

Feedback appreciated


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6f904d2b..6bbc9ae5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -447,6 +447,27 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw20-staked-balance-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.11.1",
+ "cw2 0.12.1",
+ "cw20 0.12.1",
+ "cw20-base 0.12.1",
+ "schemars",
+ "serde",
+ "stake-cw20",
+ "thiserror",
+]
+
 [[package]]
 name = "cw3"
 version = "0.11.1"
diff --git a/contracts/cw20-staked-balance-voting/.cargo/config b/contracts/cw20-staked-balance-voting/.cargo/config
new file mode 100644
index 00000000..336b618a
--- /dev/null
+++ b/contracts/cw20-staked-balance-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/cw20-staked-balance-voting/.gitignore b/contracts/cw20-staked-balance-voting/.gitignore
new file mode 100644
index 00000000..dfdaaa6b
--- /dev/null
+++ b/contracts/cw20-staked-balance-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/cw20-staked-balance-voting/Cargo.lock b/contracts/cw20-staked-balance-voting/Cargo.lock
new file mode 100644
index 00000000..c9737778
--- /dev/null
+++ b/contracts/cw20-staked-balance-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/cw20-staked-balance-voting/Cargo.toml b/contracts/cw20-staked-balance-voting/Cargo.toml
new file mode 100644
index 00000000..3b24d980
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/Cargo.toml
@@ -0,0 +1,48 @@
+[package]
+name = "cw20-staked-balance-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"
+cw20 = "0.12"
+cw-utils = "0.11"
+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" }
+stake-cw20 = { path = "../stake-cw20" }
+cw20-base = {  version = "0.12", features = ["library"] }
+
+[dev-dependencies]
+cosmwasm-schema = { version = "1.0.0-beta5" }
+cw-multi-test = "0.12"
diff --git a/contracts/cw20-staked-balance-voting/README.md b/contracts/cw20-staked-balance-voting/README.md
new file mode 100644
index 00000000..c0910be6
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/README.md
@@ -0,0 +1,4 @@
+# cw staked balance voting
+
+A simple voting power module which determines voting power based on
+the staked token balance of specific addresses.
diff --git a/contracts/cw20-staked-balance-voting/examples/schema.rs b/contracts/cw20-staked-balance-voting/examples/schema.rs
new file mode 100644
index 00000000..a4103874
--- /dev/null
+++ b/contracts/cw20-staked-balance-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 cw20_staked_balance_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/cw20-staked-balance-voting/schema/execute_msg.json b/contracts/cw20-staked-balance-voting/schema/execute_msg.json
new file mode 100644
index 00000000..b3d18b47
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/schema/execute_msg.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ExecuteMsg",
+  "type": "string",
+  "enum": []
+}
diff --git a/contracts/cw20-staked-balance-voting/schema/instantiate_msg.json b/contracts/cw20-staked-balance-voting/schema/instantiate_msg.json
new file mode 100644
index 00000000..c3048d25
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/schema/instantiate_msg.json
@@ -0,0 +1,324 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "InstantiateMsg",
+  "type": "object",
+  "required": [
+    "token_info"
+  ],
+  "properties": {
+    "token_info": {
+      "$ref": "#/definitions/TokenInfo"
+    }
+  },
+  "definitions": {
+    "Binary": {
+      "description": "Binary is a wrapper around Vec<u8> 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<u8>",
+      "type": "string"
+    },
+    "Cw20Coin": {
+      "type": "object",
+      "required": [
+        "address",
+        "amount"
+      ],
+      "properties": {
+        "address": {
+          "type": "string"
+        },
+        "amount": {
+          "$ref": "#/definitions/Uint128"
+        }
+      }
+    },
+    "Duration": {
+      "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined",
+      "oneOf": [
+        {
+          "type": "object",
+          "required": [
+            "height"
+          ],
+          "properties": {
+            "height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Time in seconds",
+          "type": "object",
+          "required": [
+            "time"
+          ],
+          "properties": {
+            "time": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "EmbeddedLogo": {
+      "description": "This is used to store the logo on the blockchain in an accepted format. Enforce maximum size of 5KB on all variants.",
+      "oneOf": [
+        {
+          "description": "Store the Logo as an SVG file. The content must conform to the spec at https://en.wikipedia.org/wiki/Scalable_Vector_Graphics (The contract should do some light-weight sanity-check validation)",
+          "type": "object",
+          "required": [
+            "svg"
+          ],
+          "properties": {
+            "svg": {
+              "$ref": "#/definitions/Binary"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Store the Logo as a PNG file. This will likely only support up to 64x64 or so within the 5KB limit.",
+          "type": "object",
+          "required": [
+            "png"
+          ],
+          "properties": {
+            "png": {
+              "$ref": "#/definitions/Binary"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "InstantiateMarketingInfo": {
+      "type": "object",
+      "properties": {
+        "description": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "logo": {
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Logo"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "marketing": {
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "project": {
+          "type": [
+            "string",
+            "null"
+          ]
+        }
+      }
+    },
+    "Logo": {
+      "description": "This is used for uploading logo data, or setting it in InstantiateData",
+      "oneOf": [
+        {
+          "description": "A reference to an externally hosted logo. Must be a valid HTTP or HTTPS URL.",
+          "type": "object",
+          "required": [
+            "url"
+          ],
+          "properties": {
+            "url": {
+              "type": "string"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Logo content stored on the blockchain. Enforce maximum size of 5KB on all variants",
+          "type": "object",
+          "required": [
+            "embedded"
+          ],
+          "properties": {
+            "embedded": {
+              "$ref": "#/definitions/EmbeddedLogo"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "StakingInfo": {
+      "oneOf": [
+        {
+          "type": "object",
+          "required": [
+            "existing"
+          ],
+          "properties": {
+            "existing": {
+              "type": "object",
+              "required": [
+                "staking_contract_address"
+              ],
+              "properties": {
+                "staking_contract_address": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "new"
+          ],
+          "properties": {
+            "new": {
+              "type": "object",
+              "required": [
+                "staking_code_id"
+              ],
+              "properties": {
+                "staking_code_id": {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                },
+                "unstaking_duration": {
+                  "anyOf": [
+                    {
+                      "$ref": "#/definitions/Duration"
+                    },
+                    {
+                      "type": "null"
+                    }
+                  ]
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "TokenInfo": {
+      "oneOf": [
+        {
+          "type": "object",
+          "required": [
+            "existing"
+          ],
+          "properties": {
+            "existing": {
+              "type": "object",
+              "required": [
+                "address",
+                "staking_contract"
+              ],
+              "properties": {
+                "address": {
+                  "type": "string"
+                },
+                "staking_contract": {
+                  "$ref": "#/definitions/StakingInfo"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "new"
+          ],
+          "properties": {
+            "new": {
+              "type": "object",
+              "required": [
+                "code_id",
+                "decimals",
+                "initial_balances",
+                "label",
+                "name",
+                "staking_code_id",
+                "symbol"
+              ],
+              "properties": {
+                "code_id": {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                },
+                "decimals": {
+                  "type": "integer",
+                  "format": "uint8",
+                  "minimum": 0.0
+                },
+                "initial_balances": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Cw20Coin"
+                  }
+                },
+                "label": {
+                  "type": "string"
+                },
+                "marketing": {
+                  "anyOf": [
+                    {
+                      "$ref": "#/definitions/InstantiateMarketingInfo"
+                    },
+                    {
+                      "type": "null"
+                    }
+                  ]
+                },
+                "name": {
+                  "type": "string"
+                },
+                "staking_code_id": {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                },
+                "symbol": {
+                  "type": "string"
+                },
+                "unstaking_duration": {
+                  "anyOf": [
+                    {
+                      "$ref": "#/definitions/Duration"
+                    },
+                    {
+                      "type": "null"
+                    }
+                  ]
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Uint128": {
+      "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw20-staked-balance-voting/schema/query_msg.json b/contracts/cw20-staked-balance-voting/schema/query_msg.json
new file mode 100644
index 00000000..3b351671
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/schema/query_msg.json
@@ -0,0 +1,92 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "QueryMsg",
+  "oneOf": [
+    {
+      "type": "object",
+      "required": [
+        "token_contract"
+      ],
+      "properties": {
+        "token_contract": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "staking_contract"
+      ],
+      "properties": {
+        "staking_contract": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "voting_power_at_height"
+      ],
+      "properties": {
+        "voting_power_at_height": {
+          "type": "object",
+          "required": [
+            "address"
+          ],
+          "properties": {
+            "address": {
+              "type": "string"
+            },
+            "height": {
+              "type": [
+                "integer",
+                "null"
+              ],
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "total_power_at_height"
+      ],
+      "properties": {
+        "total_power_at_height": {
+          "type": "object",
+          "properties": {
+            "height": {
+              "type": [
+                "integer",
+                "null"
+              ],
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "info"
+      ],
+      "properties": {
+        "info": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    }
+  ]
+}
diff --git a/contracts/cw20-staked-balance-voting/src/contract.rs b/contracts/cw20-staked-balance-voting/src/contract.rs
new file mode 100644
index 00000000..31a71508
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/src/contract.rs
@@ -0,0 +1,278 @@
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cosmwasm_std::{
+    to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, 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, StakingInfo, TokenInfo};
+use crate::state::{
+    DAO, STAKING_CONTRACT, STAKING_CONTRACT_CODE_ID, STAKING_CONTRACT_UNSTAKING_DURATION, TOKEN,
+};
+
+const CONTRACT_NAME: &str = "crates.io:cw20-staked-balance-voting";
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+const INSTANTIATE_TOKEN_REPLY_ID: u64 = 0;
+const INSTANTIATE_STAKING_REPLY_ID: u64 = 1;
+
+#[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)?;
+    DAO.save(deps.storage, &info.sender)?;
+
+    match msg.token_info {
+        TokenInfo::Existing {
+            address,
+            staking_contract,
+        } => {
+            let address = deps.api.addr_validate(&address)?;
+            TOKEN.save(deps.storage, &address)?;
+
+            match staking_contract {
+                StakingInfo::Existing {
+                    staking_contract_address,
+                } => {
+                    let staking_contract_address =
+                        deps.api.addr_validate(&staking_contract_address)?;
+                    let resp: stake_cw20::msg::GetConfigResponse = deps.querier.query_wasm_smart(
+                        &staking_contract_address,
+                        &stake_cw20::msg::QueryMsg::GetConfig {},
+                    )?;
+
+                    if address != resp.token_address {
+                        return Err(ContractError::StakingContractMismatch {});
+                    }
+
+                    STAKING_CONTRACT.save(deps.storage, &staking_contract_address)?;
+                    Ok(Response::default()
+                        .add_attribute("action", "instantiate")
+                        .add_attribute("token", "existing_token")
+                        .add_attribute("token_address", address)
+                        .add_attribute("staking_contract", staking_contract_address))
+                }
+                StakingInfo::New {
+                    staking_code_id,
+                    unstaking_duration,
+                } => {
+                    let msg = WasmMsg::Instantiate {
+                        code_id: staking_code_id,
+                        funds: vec![],
+                        admin: Some(info.sender.to_string()),
+                        label: env.contract.address.to_string(),
+                        msg: to_binary(&stake_cw20::msg::InstantiateMsg {
+                            admin: Some(info.sender.to_string()),
+                            unstaking_duration,
+                            token_address: address.to_string(),
+                        })?,
+                    };
+                    let msg = SubMsg::reply_on_success(msg, INSTANTIATE_STAKING_REPLY_ID);
+                    Ok(Response::default()
+                        .add_attribute("action", "instantiate")
+                        .add_attribute("token", "existing_token")
+                        .add_attribute("token_address", address)
+                        .add_submessage(msg))
+                }
+            }
+        }
+        TokenInfo::New {
+            code_id,
+            label,
+            name,
+            symbol,
+            decimals,
+            initial_balances,
+            marketing,
+            staking_code_id,
+            unstaking_duration,
+        } => {
+            let initial_supply = initial_balances
+                .iter()
+                .fold(Uint128::zero(), |p, n| p + n.amount);
+            if initial_supply.is_zero() {
+                return Err(ContractError::InitialBalancesError {});
+            }
+
+            STAKING_CONTRACT_CODE_ID.save(deps.storage, &staking_code_id)?;
+            STAKING_CONTRACT_UNSTAKING_DURATION.save(deps.storage, &unstaking_duration)?;
+
+            let msg = WasmMsg::Instantiate {
+                admin: Some(info.sender.to_string()),
+                code_id,
+                msg: to_binary(&cw20_base::msg::InstantiateMsg {
+                    name,
+                    symbol,
+                    decimals,
+                    initial_balances,
+                    mint: Some(cw20::MinterResponse {
+                        minter: info.sender.to_string(),
+                        cap: None,
+                    }),
+                    marketing,
+                })?,
+                funds: vec![],
+                label,
+            };
+            let msg = SubMsg::reply_on_success(msg, INSTANTIATE_TOKEN_REPLY_ID);
+
+            Ok(Response::default()
+                .add_attribute("action", "instantiate")
+                .add_attribute("token", "new_token")
+                .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 {}
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::TokenContract {} => query_token_contract(deps),
+        QueryMsg::StakingContract {} => query_staking_contract(deps),
+        QueryMsg::VotingPowerAtHeight { address, height } => {
+            query_voting_power_at_height(deps, env, address, height)
+        }
+        QueryMsg::TotalPowerAtHeight { height } => query_total_power_at_height(deps, env, height),
+        QueryMsg::Info {} => query_info(deps),
+        QueryMsg::Dao {} => query_dao(deps),
+    }
+}
+
+pub fn query_token_contract(deps: Deps) -> StdResult<Binary> {
+    let token = TOKEN.load(deps.storage)?;
+    to_binary(&token)
+}
+
+pub fn query_staking_contract(deps: Deps) -> StdResult<Binary> {
+    let staking_contract = STAKING_CONTRACT.load(deps.storage)?;
+    to_binary(&staking_contract)
+}
+
+pub fn query_voting_power_at_height(
+    deps: Deps,
+    _env: Env,
+    address: String,
+    height: Option<u64>,
+) -> StdResult<Binary> {
+    let staking_contract = STAKING_CONTRACT.load(deps.storage)?;
+    let address = deps.api.addr_validate(&address)?;
+    let res: stake_cw20::msg::StakedBalanceAtHeightResponse = deps.querier.query_wasm_smart(
+        staking_contract,
+        &stake_cw20::msg::QueryMsg::StakedBalanceAtHeight {
+            address: address.to_string(),
+            height,
+        },
+    )?;
+    to_binary(
+        &cw_governance_interface::voting::VotingPowerAtHeightResponse {
+            power: res.balance,
+            height: res.height,
+        },
+    )
+}
+
+pub fn query_total_power_at_height(
+    deps: Deps,
+    _env: Env,
+    height: Option<u64>,
+) -> StdResult<Binary> {
+    let staking_contract = STAKING_CONTRACT.load(deps.storage)?;
+    let res: stake_cw20::msg::TotalStakedAtHeightResponse = deps.querier.query_wasm_smart(
+        staking_contract,
+        &stake_cw20::msg::QueryMsg::TotalStakedAtHeight { height },
+    )?;
+    to_binary(
+        &cw_governance_interface::voting::TotalPowerAtHeightResponse {
+            power: res.total,
+            height: res.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 })
+}
+
+pub fn query_dao(deps: Deps) -> StdResult<Binary> {
+    let dao = DAO.load(deps.storage)?;
+    to_binary(&dao)
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractError> {
+    match msg.id {
+        INSTANTIATE_TOKEN_REPLY_ID => {
+            let res = parse_reply_instantiate_data(msg);
+            match res {
+                Ok(res) => {
+                    let token = TOKEN.may_load(deps.storage)?;
+                    if token.is_some() {
+                        return Err(ContractError::DuplicateToken {});
+                    }
+                    let token = deps.api.addr_validate(&res.contract_address)?;
+                    TOKEN.save(deps.storage, &token)?;
+                    let staking_contract_code_id = STAKING_CONTRACT_CODE_ID.load(deps.storage)?;
+                    let unstaking_duration =
+                        STAKING_CONTRACT_UNSTAKING_DURATION.load(deps.storage)?;
+                    let dao = DAO.load(deps.storage)?;
+                    let msg = WasmMsg::Instantiate {
+                        code_id: staking_contract_code_id,
+                        funds: vec![],
+                        admin: Some(dao.to_string()),
+                        label: env.contract.address.to_string(),
+                        msg: to_binary(&stake_cw20::msg::InstantiateMsg {
+                            admin: Some(dao.to_string()),
+                            unstaking_duration,
+                            token_address: token.to_string(),
+                        })?,
+                    };
+                    let msg = SubMsg::reply_on_success(msg, INSTANTIATE_STAKING_REPLY_ID);
+                    Ok(Response::default()
+                        .add_attribute("token_address", token)
+                        .add_submessage(msg))
+                }
+                Err(_) => Err(ContractError::TokenInstantiateError {}),
+            }
+        }
+        INSTANTIATE_STAKING_REPLY_ID => {
+            let res = parse_reply_instantiate_data(msg);
+            match res {
+                Ok(res) => {
+                    // Validate contract address
+                    let staking_contract_addr = deps.api.addr_validate(&res.contract_address)?;
+
+                    // Check if we have a duplicate
+                    let staking = STAKING_CONTRACT.may_load(deps.storage)?;
+                    if staking.is_some() {
+                        return Err(ContractError::DuplicateStakingContract {});
+                    }
+
+                    // Save staking contract addr
+                    STAKING_CONTRACT.save(deps.storage, &staking_contract_addr)?;
+
+                    Ok(Response::new().add_attribute("staking_contract", staking_contract_addr))
+                }
+                Err(_) => Err(ContractError::TokenInstantiateError {}),
+            }
+        }
+        _ => Err(ContractError::UnknownReplyId { id: msg.id }),
+    }
+}
diff --git a/contracts/cw20-staked-balance-voting/src/error.rs b/contracts/cw20-staked-balance-voting/src/error.rs
new file mode 100644
index 00000000..27c5ac5d
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/src/error.rs
@@ -0,0 +1,29 @@
+use cosmwasm_std::StdError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("Initial governance token balances must not be empty")]
+    InitialBalancesError {},
+
+    #[error("Can not change the contract's token after it has been set")]
+    DuplicateToken {},
+
+    #[error("Error instantiating token")]
+    TokenInstantiateError {},
+
+    #[error("Got a submessage reply with unknown id: {id}")]
+    UnknownReplyId { id: u64 },
+
+    #[error("Staking contract token address does not match provided token address")]
+    StakingContractMismatch {},
+
+    #[error("Can not change the contract's staking contract after it has been set")]
+    DuplicateStakingContract {},
+}
diff --git a/contracts/cw20-staked-balance-voting/src/lib.rs b/contracts/cw20-staked-balance-voting/src/lib.rs
new file mode 100644
index 00000000..8e58a2f1
--- /dev/null
+++ b/contracts/cw20-staked-balance-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/cw20-staked-balance-voting/src/msg.rs b/contracts/cw20-staked-balance-voting/src/msg.rs
new file mode 100644
index 00000000..e7c32def
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/src/msg.rs
@@ -0,0 +1,57 @@
+use cw20::Cw20Coin;
+use cw20_base::msg::InstantiateMarketingInfo;
+use cw_governance_macros::{token_query, voting_query};
+use cw_utils::Duration;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum StakingInfo {
+    Existing {
+        staking_contract_address: String,
+    },
+    New {
+        staking_code_id: u64,
+        unstaking_duration: Option<Duration>,
+    },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum TokenInfo {
+    Existing {
+        address: String,
+        staking_contract: StakingInfo,
+    },
+    New {
+        code_id: u64,
+        label: String,
+
+        name: String,
+        symbol: String,
+        decimals: u8,
+        initial_balances: Vec<Cw20Coin>,
+        marketing: Option<InstantiateMarketingInfo>,
+        staking_code_id: u64,
+        unstaking_duration: Option<Duration>,
+    },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InstantiateMsg {
+    pub token_info: TokenInfo,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ExecuteMsg {}
+
+#[voting_query]
+#[token_query]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum QueryMsg {
+    StakingContract {},
+    Dao {},
+}
diff --git a/contracts/cw20-staked-balance-voting/src/state.rs b/contracts/cw20-staked-balance-voting/src/state.rs
new file mode 100644
index 00000000..d3d043cd
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/src/state.rs
@@ -0,0 +1,10 @@
+use cosmwasm_std::Addr;
+use cw_storage_plus::Item;
+use cw_utils::Duration;
+
+pub const TOKEN: Item<Addr> = Item::new("token");
+pub const DAO: Item<Addr> = Item::new("dao");
+pub const STAKING_CONTRACT: Item<Addr> = Item::new("staking_contract");
+pub const STAKING_CONTRACT_UNSTAKING_DURATION: Item<Option<Duration>> =
+    Item::new("staking_contract_unstaking_duration");
+pub const STAKING_CONTRACT_CODE_ID: Item<u64> = Item::new("staking_contract_code_id");


Test patch:
diff --git a/contracts/cw20-staked-balance-voting/src/tests.rs b/contracts/cw20-staked-balance-voting/src/tests.rs
new file mode 100644
index 00000000..c66c71f7
--- /dev/null
+++ b/contracts/cw20-staked-balance-voting/src/tests.rs
@@ -0,0 +1,853 @@
+use cosmwasm_std::{to_binary, Addr, Empty, Uint128};
+use cw2::ContractVersion;
+use cw20::{Cw20Coin, MinterResponse, TokenInfoResponse};
+use cw_governance_interface::voting::{InfoResponse, VotingPowerAtHeightResponse};
+use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor};
+
+use crate::msg::{InstantiateMsg, QueryMsg, StakingInfo};
+
+const DAO_ADDR: &str = "dao";
+const CREATOR_ADDR: &str = "creator";
+
+fn cw20_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw20_base::contract::execute,
+        cw20_base::contract::instantiate,
+        cw20_base::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn staking_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        stake_cw20::contract::execute,
+        stake_cw20::contract::instantiate,
+        stake_cw20::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn staked_balance_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 stake_tokens(app: &mut App, staking_addr: Addr, cw20_addr: Addr, sender: &str, amount: u128) {
+    let msg = cw20::Cw20ExecuteMsg::Send {
+        contract: staking_addr.to_string(),
+        amount: Uint128::new(amount),
+        msg: to_binary(&stake_cw20::msg::ReceiveMsg::Stake {}).unwrap(),
+    };
+    app.execute_contract(Addr::unchecked(sender), cw20_addr, &msg, &[])
+        .unwrap();
+}
+
+#[test]
+#[should_panic(expected = "Initial governance token balances must not be empty")]
+fn test_instantiate_zero_supply() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+    instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::zero(),
+                }],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+            },
+        },
+    );
+}
+
+#[test]
+#[should_panic(expected = "Initial governance token balances must not be empty")]
+fn test_instantiate_no_balances() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+    instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+            },
+        },
+    );
+}
+
+#[test]
+fn test_contract_info() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(2u64),
+                }],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+            },
+        },
+    );
+
+    let info: InfoResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::Info {})
+        .unwrap();
+    assert_eq!(
+        info,
+        InfoResponse {
+            info: ContractVersion {
+                contract: "crates.io:cw20-staked-balance-voting".to_string(),
+                version: env!("CARGO_PKG_VERSION").to_string()
+            }
+        }
+    );
+
+    let dao: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::Dao {})
+        .unwrap();
+    assert_eq!(dao, Addr::unchecked(DAO_ADDR));
+}
+
+#[test]
+fn test_new_cw20() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_contract_id = app.store_code(staking_contract());
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::New {
+                code_id: cw20_id,
+                label: "DAO DAO voting".to_string(),
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(2u64),
+                }],
+                marketing: None,
+                unstaking_duration: None,
+                staking_code_id: staking_contract_id,
+            },
+        },
+    );
+
+    let token_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::TokenContract {})
+        .unwrap();
+    let staking_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::StakingContract {})
+        .unwrap();
+
+    let token_info: TokenInfoResponse = app
+        .wrap()
+        .query_wasm_smart(token_addr.clone(), &cw20::Cw20QueryMsg::TokenInfo {})
+        .unwrap();
+    assert_eq!(
+        token_info,
+        TokenInfoResponse {
+            name: "DAO DAO".to_string(),
+            symbol: "DAO".to_string(),
+            decimals: 6,
+            total_supply: Uint128::from(2u64)
+        }
+    );
+
+    let minter_info: Option<MinterResponse> = app
+        .wrap()
+        .query_wasm_smart(token_addr.clone(), &cw20::Cw20QueryMsg::Minter {})
+        .unwrap();
+    assert_eq!(
+        minter_info,
+        Some(MinterResponse {
+            minter: DAO_ADDR.to_string(),
+            cap: None,
+        })
+    );
+
+    // Expect 0 as they have not staked
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::zero(),
+            height: app.block_info().height,
+        }
+    );
+
+    // Expect 0 as DAO has not staked
+    let dao_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: DAO_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        dao_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::zero(),
+            height: app.block_info().height,
+        }
+    );
+
+    // Stake 1 token as creator
+    stake_tokens(&mut app, staking_addr, token_addr, CREATOR_ADDR, 1);
+    app.update_block(next_block);
+
+    // Expect 1 as creator has now staked 1
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height,
+        }
+    );
+
+    // Expect 1 as only one token staked to make up whole voting power
+    let total_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::TotalPowerAtHeight { height: None })
+        .unwrap();
+
+    assert_eq!(
+        total_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height,
+        }
+    )
+}
+
+#[test]
+fn test_existing_cw20_new_staking() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_id = app.store_code(staking_contract());
+
+    let token_addr = app
+        .instantiate_contract(
+            cw20_id,
+            Addr::unchecked(CREATOR_ADDR),
+            &cw20_base::msg::InstantiateMsg {
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(2u64),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "voting token",
+            None,
+        )
+        .unwrap();
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::Existing {
+                address: token_addr.to_string(),
+                staking_contract: StakingInfo::New {
+                    staking_code_id: staking_id,
+                    unstaking_duration: None,
+                },
+            },
+        },
+    );
+
+    let token_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::TokenContract {})
+        .unwrap();
+    let staking_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::StakingContract {})
+        .unwrap();
+
+    let token_info: TokenInfoResponse = app
+        .wrap()
+        .query_wasm_smart(token_addr.clone(), &cw20::Cw20QueryMsg::TokenInfo {})
+        .unwrap();
+    assert_eq!(
+        token_info,
+        TokenInfoResponse {
+            name: "DAO DAO".to_string(),
+            symbol: "DAO".to_string(),
+            decimals: 3,
+            total_supply: Uint128::from(2u64)
+        }
+    );
+
+    let minter_info: Option<MinterResponse> = app
+        .wrap()
+        .query_wasm_smart(token_addr.clone(), &cw20::Cw20QueryMsg::Minter {})
+        .unwrap();
+    assert!(minter_info.is_none());
+
+    // Expect 0 as creator has not staked
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::zero(),
+            height: app.block_info().height,
+        }
+    );
+
+    // Expect 0 as DAO has not staked
+    let dao_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: DAO_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        dao_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::zero(),
+            height: app.block_info().height,
+        }
+    );
+
+    // Stake 1 token as creator
+    stake_tokens(&mut app, staking_addr, token_addr, CREATOR_ADDR, 1);
+    app.update_block(next_block);
+
+    // Expect 1 as creator has now staked 1
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height,
+        }
+    );
+
+    // Expect 1 as only one token staked to make up whole voting power
+    let total_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::TotalPowerAtHeight { height: None })
+        .unwrap();
+
+    assert_eq!(
+        total_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height,
+        }
+    )
+}
+
+#[test]
+fn test_existing_cw20_existing_staking() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_id = app.store_code(staking_contract());
+
+    let token_addr = app
+        .instantiate_contract(
+            cw20_id,
+            Addr::unchecked(CREATOR_ADDR),
+            &cw20_base::msg::InstantiateMsg {
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(2u64),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "voting token",
+            None,
+        )
+        .unwrap();
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::Existing {
+                address: token_addr.to_string(),
+                staking_contract: StakingInfo::New {
+                    staking_code_id: staking_id,
+                    unstaking_duration: None,
+                },
+            },
+        },
+    );
+
+    let token_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::TokenContract {})
+        .unwrap();
+    // We'll use this for our valid existing contract
+    let staking_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::StakingContract {})
+        .unwrap();
+
+    let token_info: TokenInfoResponse = app
+        .wrap()
+        .query_wasm_smart(token_addr.clone(), &cw20::Cw20QueryMsg::TokenInfo {})
+        .unwrap();
+    assert_eq!(
+        token_info,
+        TokenInfoResponse {
+            name: "DAO DAO".to_string(),
+            symbol: "DAO".to_string(),
+            decimals: 3,
+            total_supply: Uint128::from(2u64)
+        }
+    );
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::Existing {
+                address: token_addr.to_string(),
+                staking_contract: StakingInfo::Existing {
+                    staking_contract_address: staking_addr.to_string(),
+                },
+            },
+        },
+    );
+
+    // Expect 0 as creator has not staked
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::zero(),
+            height: app.block_info().height,
+        }
+    );
+
+    // Expect 0 as DAO has not staked
+    let dao_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: DAO_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        dao_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::zero(),
+            height: app.block_info().height,
+        }
+    );
+
+    // Stake 1 token as creator
+    stake_tokens(&mut app, staking_addr.clone(), token_addr, CREATOR_ADDR, 1);
+    app.update_block(next_block);
+
+    // Expect 1 as creator has now staked 1
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height,
+        }
+    );
+
+    // Expect 1 as only one token staked to make up whole voting power
+    let total_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(voting_addr, &QueryMsg::TotalPowerAtHeight { height: None })
+        .unwrap();
+
+    assert_eq!(
+        total_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height,
+        }
+    );
+
+    // Now lets test the error case where we use an invalid staking contract
+    let different_token = app
+        .instantiate_contract(
+            cw20_id,
+            Addr::unchecked(CREATOR_ADDR),
+            &cw20_base::msg::InstantiateMsg {
+                name: "DAO DAO MISMATCH".to_string(),
+                symbol: "DAOM".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(2u64),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "voting token",
+            None,
+        )
+        .unwrap();
+
+    // Expect error as the token address does not match the staking address token address
+    app.instantiate_contract(
+        voting_id,
+        Addr::unchecked(DAO_ADDR),
+        &InstantiateMsg {
+            token_info: crate::msg::TokenInfo::Existing {
+                address: different_token.to_string(),
+                staking_contract: StakingInfo::Existing {
+                    staking_contract_address: staking_addr.to_string(),
+                },
+            },
+        },
+        &[],
+        "voting module",
+        None,
+    )
+    .unwrap_err();
+}
+
+#[test]
+fn test_different_heights() {
+    let mut app = App::default();
+    let cw20_id = app.store_code(cw20_contract());
+    let voting_id = app.store_code(staked_balance_voting_contract());
+    let staking_id = app.store_code(staking_contract());
+
+    let token_addr = app
+        .instantiate_contract(
+            cw20_id,
+            Addr::unchecked(CREATOR_ADDR),
+            &cw20_base::msg::InstantiateMsg {
+                name: "DAO DAO".to_string(),
+                symbol: "DAO".to_string(),
+                decimals: 3,
+                initial_balances: vec![Cw20Coin {
+                    address: CREATOR_ADDR.to_string(),
+                    amount: Uint128::from(2u64),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "voting token",
+            None,
+        )
+        .unwrap();
+
+    let voting_addr = instantiate_voting(
+        &mut app,
+        voting_id,
+        InstantiateMsg {
+            token_info: crate::msg::TokenInfo::Existing {
+                address: token_addr.to_string(),
+                staking_contract: StakingInfo::New {
+                    staking_code_id: staking_id,
+                    unstaking_duration: None,
+                },
+            },
+        },
+    );
+
+    let token_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::TokenContract {})
+        .unwrap();
+    let staking_addr: Addr = app
+        .wrap()
+        .query_wasm_smart(voting_addr.clone(), &QueryMsg::StakingContract {})
+        .unwrap();
+
+    // Expect 0 as creator has not staked
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::zero(),
+            height: app.block_info().height,
+        }
+    );
+
+    // Stake 1 token as creator
+    stake_tokens(
+        &mut app,
+        staking_addr.clone(),
+        token_addr.clone(),
+        CREATOR_ADDR,
+        1,
+    );
+    app.update_block(next_block);
+
+    // Expect 1 as creator has now staked 1
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height,
+        }
+    );
+
+    // Expect 1 as only one token staked to make up whole voting power
+    let total_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight { height: None },
+        )
+        .unwrap();
+
+    assert_eq!(
+        total_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height,
+        }
+    );
+
+    // Stake another 1 token as creator
+    stake_tokens(&mut app, staking_addr, token_addr, CREATOR_ADDR, 1);
+    app.update_block(next_block);
+
+    // Expect 2 as creator has now staked 2
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(2u128),
+            height: app.block_info().height,
+        }
+    );
+
+    // Expect 2 as we have now staked 2
+    let total_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::TotalPowerAtHeight { height: None },
+        )
+        .unwrap();
+
+    assert_eq!(
+        total_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(2u128),
+            height: app.block_info().height,
+        }
+    );
+
+    // Check we can query history
+    let creator_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr.clone(),
+            &QueryMsg::VotingPowerAtHeight {
+                address: CREATOR_ADDR.to_string(),
+                height: Some(app.block_info().height - 1),
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        creator_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height - 1,
+        }
+    );
+
+    // Expect 1 at the old height prior to second stake
+    let total_voting_power: VotingPowerAtHeightResponse = app
+        .wrap()
+        .query_wasm_smart(
+            voting_addr,
+            &QueryMsg::TotalPowerAtHeight {
+                height: Some(app.block_info().height - 1),
+            },
+        )
+        .unwrap();
+
+    assert_eq!(
+        total_voting_power,
+        VotingPowerAtHeightResponse {
+            power: Uint128::new(1u128),
+            height: app.block_info().height - 1,
+        }
+    );
+}