Repository: DA0-DA0/dao-contracts
Pull request #383 (2022-07-05 20:27:52)

Basic Native Token Voting Contract
Basic native voting contract which assumes the denom provided is not the staking denom on the network. If we want DAOs to use the native staking token another contract will need to be written. This is for tokens such as ION or IBCed tokens e.g. PageDAO's ERC 20 (via Gravity Bridge)

codecov-commenter: # [Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) Report
> Merging [#383](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) (e8fdf3a) into [main](https://codecov.io/gh/DA0-DA0/dao-contracts/commit/cce4b907194f5f1233f25d81bf6bd8ab2b4ecbbf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) (cce4b90) will **increase** coverage by `0.09%`.
> The diff coverage is `98.97%`.

> :exclamation: Current head e8fdf3a differs from pull request most recent head 812b671. Consider uploading reports for the commit 812b671 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #383      +/-   ##
==========================================
+ Coverage   96.98%   97.07%   +0.09%     
==========================================
  Files          39       41       +2     
  Lines        8847     9337     +490     
==========================================
+ Hits         8580     9064     +484     
- Misses        267      273       +6     
```


| [Impacted Files](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) | Coverage Δ | |
|---|---|---|
| [...ts/cw-native-staked-balance-voting/src/contract.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2N3LW5hdGl2ZS1zdGFrZWQtYmFsYW5jZS12b3Rpbmcvc3JjL2NvbnRyYWN0LnJz) | `96.32% <96.32%> (ø)` | |
| [...racts/cw-native-staked-balance-voting/src/tests.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2N3LW5hdGl2ZS1zdGFrZWQtYmFsYW5jZS12b3Rpbmcvc3JjL3Rlc3RzLnJz) | `100.00% <100.00%> (ø)` | |
| [packages/testing/src/tests.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-cGFja2FnZXMvdGVzdGluZy9zcmMvdGVzdHMucnM=) | `87.79% <0.00%> (-0.30%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0). Last update [cce4b90...812b671](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/383?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2627a8b1..8eb8ed2c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -319,6 +319,26 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw-native-staked-balance-voting"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cosmwasm-storage",
+ "cw-controllers",
+ "cw-core-interface",
+ "cw-core-macros",
+ "cw-multi-test 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cw-storage-plus 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cw-utils 0.13.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cw2",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "cw-paginate"
 version = "0.1.0"
diff --git a/README.md b/README.md
index 806765ee..7f5c22dc 100644
--- a/README.md
+++ b/README.md
@@ -47,12 +47,13 @@ The script will output something like:
 ```sh
 NEXT_PUBLIC_CW20_CODE_ID=1
 NEXT_PUBLIC_CW4GROUP_CODE_ID=2
-NEXT_PUBLIC_CWCORE_CODE_ID=5
-NEXT_PUBLIC_CWPROPOSALSINGLE_CODE_ID=8
-NEXT_PUBLIC_CW4VOTING_CODE_ID=4
-NEXT_PUBLIC_CW20STAKEDBALANCEVOTING_CODE_ID=3
-NEXT_PUBLIC_STAKECW20_CODE_ID=9
-NEXT_PUBLIC_DAO_CONTRACT_ADDRESS=juno1wug8sewp6cedgkmrmvhl3lf3tulagm9hnvy8p0rppz9yjw0g4wtqwrw37d
+NEXT_PUBLIC_CWCORE_CODE_ID=7
+NEXT_PUBLIC_CWPROPOSALSINGLE_CODE_ID=11
+NEXT_PUBLIC_CW4VOTING_CODE_ID=5
+NEXT_PUBLIC_CW20STAKEDBALANCEVOTING_CODE_ID=4
+NEXT_PUBLIC_STAKECW20_CODE_ID=13
+NEXT_PUBLIC_DAO_CONTRACT_ADDRESS=juno1zlmaky7753d2fneyhduwz0rn3u9ns8rse3tudhze8rc2g54w9ysqgjt23l
+NEXT_PUBLIC_V1_FACTORY_CONTRACT_ADDRESS=juno1pvrwmjuusn9wh34j7y520g8gumuy9xtl3gvprlljfdpwju3x7ucssml9ug
 ```
 
 You can then instantiate and interact with contracts.
diff --git a/contracts/cw-admin-factory/Cargo.toml b/contracts/cw-admin-factory/Cargo.toml
index af1ba335..179783cb 100644
--- a/contracts/cw-admin-factory/Cargo.toml
+++ b/contracts/cw-admin-factory/Cargo.toml
@@ -15,17 +15,6 @@ exclude = [
 [lib]
 crate-type = ["cdylib", "rlib"]
 
-[profile.release]
-opt-level = 3
-debug = false
-rpath = false
-lto = true
-debug-assertions = false
-codegen-units = 1
-panic = 'abort'
-incremental = false
-overflow-checks = true
-
 [features]
 # for more explicit tests, cargo test --features=backtraces
 backtraces = ["cosmwasm-std/backtraces"]
diff --git a/contracts/cw-native-staked-balance-voting/.cargo/config b/contracts/cw-native-staked-balance-voting/.cargo/config
new file mode 100644
index 00000000..336b618a
--- /dev/null
+++ b/contracts/cw-native-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/cw-native-staked-balance-voting/.gitignore b/contracts/cw-native-staked-balance-voting/.gitignore
new file mode 100644
index 00000000..dfdaaa6b
--- /dev/null
+++ b/contracts/cw-native-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/cw-native-staked-balance-voting/Cargo.lock b/contracts/cw-native-staked-balance-voting/Cargo.lock
new file mode 100644
index 00000000..5ac6538a
--- /dev/null
+++ b/contracts/cw-native-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"
+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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "832bebef577ecb394603de8e2bf0de429b74aa364e17dec18e15ce37e71b0cae"
+dependencies = [
+ "syn",
+]
+
+[[package]]
+name = "cosmwasm-schema"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e4b3f6933f94acdd3ddb931af4870c2002e3331a4a8b247a4ef070dd31ccb0"
+dependencies = [
+ "schemars",
+ "serde_json",
+]
+
+[[package]]
+name = "cosmwasm-std"
+version = "1.0.0"
+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"
+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/cw-native-staked-balance-voting/Cargo.toml b/contracts/cw-native-staked-balance-voting/Cargo.toml
new file mode 100644
index 00000000..4e225522
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/Cargo.toml
@@ -0,0 +1,40 @@
+[package]
+name = "cw-native-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 = []
+
+[dependencies]
+cosmwasm-std = { version = "1.0.0" }
+cosmwasm-storage = { version = "1.0.0" }
+cw-storage-plus = "0.13"
+cw2 = "0.13"
+cw-utils = "0.13"
+cw-controllers = "0.13"
+schemars = "0.8"
+serde = { version = "1.0", default-features = false, features = ["derive"] }
+thiserror = { version = "1.0" }
+cw-core-macros = { version = "*", path = "../../packages/cw-core-macros" }
+cw-core-interface = { version = "*", path = "../../packages/cw-core-interface" }
+
+[dev-dependencies]
+cosmwasm-schema = { version = "1.0.0" }
+cw-multi-test = "0.13"
+anyhow = "1.0.57"
diff --git a/contracts/cw-native-staked-balance-voting/README.md b/contracts/cw-native-staked-balance-voting/README.md
new file mode 100644
index 00000000..f086a952
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/README.md
@@ -0,0 +1,4 @@
+# CW Native Staked Balance Voting
+
+Simple native token voting contract which assumes the native denom provided is not used for staking for securing the
+network e.g. IBC denoms or secondary tokens (ION).
\ No newline at end of file
diff --git a/contracts/cw-native-staked-balance-voting/examples/schema.rs b/contracts/cw-native-staked-balance-voting/examples/schema.rs
new file mode 100644
index 00000000..08d48788
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/examples/schema.rs
@@ -0,0 +1,34 @@
+use std::env::current_dir;
+use std::fs::create_dir_all;
+
+use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for};
+use cosmwasm_std::Addr;
+use cw_controllers::ClaimsResponse;
+use cw_core_interface::voting::{
+    InfoResponse, IsActiveResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse,
+};
+use cw_native_staked_balance_voting::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
+use cw_native_staked_balance_voting::state::Config;
+
+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);
+    export_schema(&schema_for!(MigrateMsg), &out_dir);
+
+    export_schema(&schema_for!(InfoResponse), &out_dir);
+    export_schema(&schema_for!(TotalPowerAtHeightResponse), &out_dir);
+    export_schema(&schema_for!(VotingPowerAtHeightResponse), &out_dir);
+    export_schema(&schema_for!(IsActiveResponse), &out_dir);
+    export_schema(&schema_for!(ClaimsResponse), &out_dir);
+
+    // Auto TS code generation expects the query return type as QueryNameResponse
+    // Here we map query resonses to the correct name
+    export_schema_with_title(&schema_for!(Addr), &out_dir, "DaoResponse");
+    export_schema_with_title(&schema_for!(Config), &out_dir, "GetConfigResponse");
+}
diff --git a/contracts/cw-native-staked-balance-voting/schema/claims_response.json b/contracts/cw-native-staked-balance-voting/schema/claims_response.json
new file mode 100644
index 00000000..08211a3c
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/claims_response.json
@@ -0,0 +1,95 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ClaimsResponse",
+  "type": "object",
+  "required": [
+    "claims"
+  ],
+  "properties": {
+    "claims": {
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Claim"
+      }
+    }
+  },
+  "definitions": {
+    "Claim": {
+      "type": "object",
+      "required": [
+        "amount",
+        "release_at"
+      ],
+      "properties": {
+        "amount": {
+          "$ref": "#/definitions/Uint128"
+        },
+        "release_at": {
+          "$ref": "#/definitions/Expiration"
+        }
+      }
+    },
+    "Expiration": {
+      "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+      "oneOf": [
+        {
+          "description": "AtHeight will expire when `env.block.height` >= height",
+          "type": "object",
+          "required": [
+            "at_height"
+          ],
+          "properties": {
+            "at_height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "AtTime will expire when `env.block.time` >= time",
+          "type": "object",
+          "required": [
+            "at_time"
+          ],
+          "properties": {
+            "at_time": {
+              "$ref": "#/definitions/Timestamp"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Never will never expire. Used to express the empty variant",
+          "type": "object",
+          "required": [
+            "never"
+          ],
+          "properties": {
+            "never": {
+              "type": "object"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint128": {
+      "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+      "type": "string"
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw-native-staked-balance-voting/schema/dao_response.json b/contracts/cw-native-staked-balance-voting/schema/dao_response.json
new file mode 100644
index 00000000..9518ba3b
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/dao_response.json
@@ -0,0 +1,6 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "DaoResponse",
+  "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/cw-native-staked-balance-voting/schema/execute_msg.json b/contracts/cw-native-staked-balance-voting/schema/execute_msg.json
new file mode 100644
index 00000000..989e07a3
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/execute_msg.json
@@ -0,0 +1,126 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ExecuteMsg",
+  "oneOf": [
+    {
+      "type": "object",
+      "required": [
+        "stake"
+      ],
+      "properties": {
+        "stake": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "unstake"
+      ],
+      "properties": {
+        "unstake": {
+          "type": "object",
+          "required": [
+            "amount"
+          ],
+          "properties": {
+            "amount": {
+              "$ref": "#/definitions/Uint128"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "update_config"
+      ],
+      "properties": {
+        "update_config": {
+          "type": "object",
+          "properties": {
+            "duration": {
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/Duration"
+                },
+                {
+                  "type": "null"
+                }
+              ]
+            },
+            "manager": {
+              "type": [
+                "string",
+                "null"
+              ]
+            },
+            "owner": {
+              "type": [
+                "string",
+                "null"
+              ]
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "claim"
+      ],
+      "properties": {
+        "claim": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    }
+  ],
+  "definitions": {
+    "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
+        }
+      ]
+    },
+    "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/cw-native-staked-balance-voting/schema/get_config_response.json b/contracts/cw-native-staked-balance-voting/schema/get_config_response.json
new file mode 100644
index 00000000..20488cf9
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/get_config_response.json
@@ -0,0 +1,83 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "GetConfigResponse",
+  "type": "object",
+  "required": [
+    "denom"
+  ],
+  "properties": {
+    "denom": {
+      "type": "string"
+    },
+    "manager": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/Addr"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
+    "owner": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/Addr"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    },
+    "unstaking_duration": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/Duration"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    }
+  },
+  "definitions": {
+    "Addr": {
+      "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
+      "type": "string"
+    },
+    "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
+        }
+      ]
+    }
+  }
+}
diff --git a/contracts/cw-native-staked-balance-voting/schema/info_response.json b/contracts/cw-native-staked-balance-voting/schema/info_response.json
new file mode 100644
index 00000000..a0516764
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/info_response.json
@@ -0,0 +1,32 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "InfoResponse",
+  "type": "object",
+  "required": [
+    "info"
+  ],
+  "properties": {
+    "info": {
+      "$ref": "#/definitions/ContractVersion"
+    }
+  },
+  "definitions": {
+    "ContractVersion": {
+      "type": "object",
+      "required": [
+        "contract",
+        "version"
+      ],
+      "properties": {
+        "contract": {
+          "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing",
+          "type": "string"
+        },
+        "version": {
+          "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)",
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git a/contracts/cw-native-staked-balance-voting/schema/instantiate_msg.json b/contracts/cw-native-staked-balance-voting/schema/instantiate_msg.json
new file mode 100644
index 00000000..a7849de9
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/instantiate_msg.json
@@ -0,0 +1,71 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "InstantiateMsg",
+  "type": "object",
+  "required": [
+    "denom"
+  ],
+  "properties": {
+    "denom": {
+      "type": "string"
+    },
+    "manager": {
+      "type": [
+        "string",
+        "null"
+      ]
+    },
+    "owner": {
+      "type": [
+        "string",
+        "null"
+      ]
+    },
+    "unstaking_duration": {
+      "anyOf": [
+        {
+          "$ref": "#/definitions/Duration"
+        },
+        {
+          "type": "null"
+        }
+      ]
+    }
+  },
+  "definitions": {
+    "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
+        }
+      ]
+    }
+  }
+}
diff --git a/contracts/cw-native-staked-balance-voting/schema/is_active_response.json b/contracts/cw-native-staked-balance-voting/schema/is_active_response.json
new file mode 100644
index 00000000..5275d891
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/is_active_response.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "IsActiveResponse",
+  "type": "object",
+  "required": [
+    "active"
+  ],
+  "properties": {
+    "active": {
+      "type": "boolean"
+    }
+  }
+}
diff --git a/contracts/cw-native-staked-balance-voting/schema/migrate_msg.json b/contracts/cw-native-staked-balance-voting/schema/migrate_msg.json
new file mode 100644
index 00000000..87b18ea7
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/migrate_msg.json
@@ -0,0 +1,5 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "MigrateMsg",
+  "type": "object"
+}
diff --git a/contracts/cw-native-staked-balance-voting/schema/query_msg.json b/contracts/cw-native-staked-balance-voting/schema/query_msg.json
new file mode 100644
index 00000000..e2f561a5
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/query_msg.json
@@ -0,0 +1,112 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "QueryMsg",
+  "oneOf": [
+    {
+      "type": "object",
+      "required": [
+        "dao"
+      ],
+      "properties": {
+        "dao": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "get_config"
+      ],
+      "properties": {
+        "get_config": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "claims"
+      ],
+      "properties": {
+        "claims": {
+          "type": "object",
+          "required": [
+            "address"
+          ],
+          "properties": {
+            "address": {
+              "type": "string"
+            }
+          }
+        }
+      },
+      "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/cw-native-staked-balance-voting/schema/total_power_at_height_response.json b/contracts/cw-native-staked-balance-voting/schema/total_power_at_height_response.json
new file mode 100644
index 00000000..8018462b
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/total_power_at_height_response.json
@@ -0,0 +1,25 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "TotalPowerAtHeightResponse",
+  "type": "object",
+  "required": [
+    "height",
+    "power"
+  ],
+  "properties": {
+    "height": {
+      "type": "integer",
+      "format": "uint64",
+      "minimum": 0.0
+    },
+    "power": {
+      "$ref": "#/definitions/Uint128"
+    }
+  },
+  "definitions": {
+    "Uint128": {
+      "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw-native-staked-balance-voting/schema/voting_power_at_height_response.json b/contracts/cw-native-staked-balance-voting/schema/voting_power_at_height_response.json
new file mode 100644
index 00000000..15e986bf
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/schema/voting_power_at_height_response.json
@@ -0,0 +1,25 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "VotingPowerAtHeightResponse",
+  "type": "object",
+  "required": [
+    "height",
+    "power"
+  ],
+  "properties": {
+    "height": {
+      "type": "integer",
+      "format": "uint64",
+      "minimum": 0.0
+    },
+    "power": {
+      "$ref": "#/definitions/Uint128"
+    }
+  },
+  "definitions": {
+    "Uint128": {
+      "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw-native-staked-balance-voting/src/contract.rs b/contracts/cw-native-staked-balance-voting/src/contract.rs
new file mode 100644
index 00000000..613ff5d5
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/src/contract.rs
@@ -0,0 +1,326 @@
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cosmwasm_std::{
+    coins, to_binary, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response,
+    StdResult, Uint128,
+};
+use cw2::set_contract_version;
+use cw_controllers::ClaimsResponse;
+use cw_core_interface::voting::{TotalPowerAtHeightResponse, VotingPowerAtHeightResponse};
+use cw_utils::{must_pay, Duration};
+
+use crate::error::ContractError;
+use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
+use crate::state::{Config, CLAIMS, CONFIG, DAO, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL};
+
+const CONTRACT_NAME: &str = "crates.io:cw-native-staked-balance-voting";
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+fn validate_duration(duration: Option<Duration>) -> Result<(), ContractError> {
+    if let Some(unstaking_duration) = duration {
+        match unstaking_duration {
+            Duration::Height(height) => {
+                if height == 0 {
+                    return Err(ContractError::InvalidUnstakingDuration {});
+                }
+            }
+            Duration::Time(time) => {
+                if time == 0 {
+                    return Err(ContractError::InvalidUnstakingDuration {});
+                }
+            }
+        }
+    }
+    Ok(())
+}
+
+#[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)?;
+
+    let owner = msg
+        .owner
+        .map(|owner| deps.api.addr_validate(&owner))
+        .transpose()?;
+    let manager = msg
+        .manager
+        .map(|manager| deps.api.addr_validate(&manager))
+        .transpose()?;
+
+    validate_duration(msg.unstaking_duration)?;
+
+    let config = Config {
+        owner,
+        manager,
+        denom: msg.denom,
+        unstaking_duration: msg.unstaking_duration,
+    };
+
+    CONFIG.save(deps.storage, &config)?;
+    DAO.save(deps.storage, &info.sender)?;
+
+    Ok(Response::new()
+        .add_attribute("action", "instantiate")
+        .add_attribute(
+            "owner",
+            config
+                .owner
+                .map(|a| a.to_string())
+                .unwrap_or_else(|| "None".to_string()),
+        )
+        .add_attribute(
+            "manager",
+            config
+                .manager
+                .map(|a| a.to_string())
+                .unwrap_or_else(|| "None".to_string()),
+        ))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<Response, ContractError> {
+    match msg {
+        ExecuteMsg::Stake {} => execute_stake(deps, env, info),
+        ExecuteMsg::Unstake { amount } => execute_unstake(deps, env, info, amount),
+        ExecuteMsg::UpdateConfig {
+            owner,
+            manager,
+            duration,
+        } => execute_update_config(deps, info, owner, manager, duration),
+        ExecuteMsg::Claim {} => execute_claim(deps, env, info),
+    }
+}
+
+pub fn execute_stake(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+) -> Result<Response, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+    let amount = must_pay(&info, &config.denom)?;
+
+    STAKED_BALANCES.update(
+        deps.storage,
+        &info.sender,
+        env.block.height,
+        |balance| -> StdResult<Uint128> { Ok(balance.unwrap_or_default().checked_add(amount)?) },
+    )?;
+    STAKED_TOTAL.update(
+        deps.storage,
+        env.block.height,
+        |total| -> StdResult<Uint128> { Ok(total.unwrap_or_default().checked_add(amount)?) },
+    )?;
+
+    Ok(Response::new()
+        .add_attribute("action", "stake")
+        .add_attribute("amount", amount.to_string())
+        .add_attribute("from", info.sender))
+}
+
+pub fn execute_unstake(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    amount: Uint128,
+) -> Result<Response, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+
+    STAKED_BALANCES.update(
+        deps.storage,
+        &info.sender,
+        env.block.height,
+        |balance| -> Result<Uint128, ContractError> {
+            balance
+                .unwrap_or_default()
+                .checked_sub(amount)
+                .map_err(|_e| ContractError::InvalidUnstakeAmount {})
+        },
+    )?;
+    STAKED_TOTAL.update(
+        deps.storage,
+        env.block.height,
+        |total| -> Result<Uint128, ContractError> {
+            total
+                .unwrap_or_default()
+                .checked_sub(amount)
+                .map_err(|_e| ContractError::InvalidUnstakeAmount {})
+        },
+    )?;
+
+    match config.unstaking_duration {
+        None => {
+            let msg = CosmosMsg::Bank(BankMsg::Send {
+                to_address: info.sender.to_string(),
+                amount: coins(amount.u128(), config.denom),
+            });
+            Ok(Response::new()
+                .add_message(msg)
+                .add_attribute("action", "unstake")
+                .add_attribute("from", info.sender)
+                .add_attribute("amount", amount)
+                .add_attribute("claim_duration", "None"))
+        }
+        Some(duration) => {
+            let outstanding_claims = CLAIMS.query_claims(deps.as_ref(), &info.sender)?.claims;
+            if outstanding_claims.len() >= MAX_CLAIMS as usize {
+                return Err(ContractError::TooManyClaims {});
+            }
+
+            CLAIMS.create_claim(
+                deps.storage,
+                &info.sender,
+                amount,
+                duration.after(&env.block),
+            )?;
+            Ok(Response::new()
+                .add_attribute("action", "unstake")
+                .add_attribute("from", info.sender)
+                .add_attribute("amount", amount)
+                .add_attribute("claim_duration", format!("{}", duration)))
+        }
+    }
+}
+
+pub fn execute_update_config(
+    deps: DepsMut,
+    info: MessageInfo,
+    new_owner: Option<String>,
+    new_manager: Option<String>,
+    duration: Option<Duration>,
+) -> Result<Response, ContractError> {
+    let mut config: Config = CONFIG.load(deps.storage)?;
+    if Some(info.sender.clone()) != config.owner && Some(info.sender.clone()) != config.manager {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    let new_owner = new_owner
+        .map(|new_owner| deps.api.addr_validate(&new_owner))
+        .transpose()?;
+    let new_manager = new_manager
+        .map(|new_manager| deps.api.addr_validate(&new_manager))
+        .transpose()?;
+
+    validate_duration(duration)?;
+
+    if Some(info.sender) != config.owner && new_owner != config.owner {
+        return Err(ContractError::OnlyOwnerCanChangeOwner {});
+    };
+
+    config.owner = new_owner;
+    config.manager = new_manager;
+
+    config.unstaking_duration = duration;
+
+    CONFIG.save(deps.storage, &config)?;
+    Ok(Response::new()
+        .add_attribute("action", "update_config")
+        .add_attribute(
+            "owner",
+            config
+                .owner
+                .map(|a| a.to_string())
+                .unwrap_or_else(|| "None".to_string()),
+        )
+        .add_attribute(
+            "manager",
+            config
+                .manager
+                .map(|a| a.to_string())
+                .unwrap_or_else(|| "None".to_string()),
+        ))
+}
+
+pub fn execute_claim(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+) -> Result<Response, ContractError> {
+    let release = CLAIMS.claim_tokens(deps.storage, &info.sender, &env.block, None)?;
+    if release.is_zero() {
+        return Err(ContractError::NothingToClaim {});
+    }
+
+    let config = CONFIG.load(deps.storage)?;
+    let msg = CosmosMsg::Bank(BankMsg::Send {
+        to_address: info.sender.to_string(),
+        amount: coins(release.u128(), config.denom),
+    });
+
+    Ok(Response::new()
+        .add_message(msg)
+        .add_attribute("action", "claim")
+        .add_attribute("from", info.sender)
+        .add_attribute("amount", release))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::VotingPowerAtHeight { address, height } => {
+            to_binary(&query_voting_power_at_height(deps, env, address, height)?)
+        }
+        QueryMsg::TotalPowerAtHeight { height } => {
+            to_binary(&query_total_power_at_height(deps, env, height)?)
+        }
+        QueryMsg::Info {} => query_info(deps),
+        QueryMsg::Dao {} => query_dao(deps),
+        QueryMsg::Claims { address } => to_binary(&query_claims(deps, address)?),
+        QueryMsg::GetConfig {} => to_binary(&CONFIG.load(deps.storage)?),
+    }
+}
+
+pub fn query_voting_power_at_height(
+    deps: Deps,
+    env: Env,
+    address: String,
+    height: Option<u64>,
+) -> StdResult<VotingPowerAtHeightResponse> {
+    let height = height.unwrap_or(env.block.height);
+    let address = deps.api.addr_validate(&address)?;
+    let power = STAKED_BALANCES
+        .may_load_at_height(deps.storage, &address, height)?
+        .unwrap_or_default();
+    Ok(VotingPowerAtHeightResponse { power, height })
+}
+
+pub fn query_total_power_at_height(
+    deps: Deps,
+    env: Env,
+    height: Option<u64>,
+) -> StdResult<TotalPowerAtHeightResponse> {
+    let height = height.unwrap_or(env.block.height);
+    let power = STAKED_TOTAL
+        .may_load_at_height(deps.storage, height)?
+        .unwrap_or_default();
+    Ok(TotalPowerAtHeightResponse { power, height })
+}
+
+pub fn query_info(deps: Deps) -> StdResult<Binary> {
+    let info = cw2::get_contract_version(deps.storage)?;
+    to_binary(&cw_core_interface::voting::InfoResponse { info })
+}
+
+pub fn query_dao(deps: Deps) -> StdResult<Binary> {
+    let dao = DAO.load(deps.storage)?;
+    to_binary(&dao)
+}
+
+pub fn query_claims(deps: Deps, address: String) -> StdResult<ClaimsResponse> {
+    CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
+    // Don't do any state migrations.
+    Ok(Response::default())
+}
diff --git a/contracts/cw-native-staked-balance-voting/src/error.rs b/contracts/cw-native-staked-balance-voting/src/error.rs
new file mode 100644
index 00000000..116c5a8e
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/src/error.rs
@@ -0,0 +1,30 @@
+use cosmwasm_std::StdError;
+use cw_utils::PaymentError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error("{0}")]
+    PaymentError(#[from] PaymentError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("Invalid unstaking duration, unstaking duration cannot be 0")]
+    InvalidUnstakingDuration {},
+
+    #[error("Nothing to claim")]
+    NothingToClaim {},
+
+    #[error("Too many outstanding claims. Claim some tokens before unstaking more.")]
+    TooManyClaims {},
+
+    #[error("Only owner can change owner")]
+    OnlyOwnerCanChangeOwner {},
+
+    #[error("Can only unstake less than or equal to the amount you have staked")]
+    InvalidUnstakeAmount {},
+}
diff --git a/contracts/cw-native-staked-balance-voting/src/lib.rs b/contracts/cw-native-staked-balance-voting/src/lib.rs
new file mode 100644
index 00000000..8e58a2f1
--- /dev/null
+++ b/contracts/cw-native-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/cw-native-staked-balance-voting/src/msg.rs b/contracts/cw-native-staked-balance-voting/src/msg.rs
new file mode 100644
index 00000000..786efcb3
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/src/msg.rs
@@ -0,0 +1,44 @@
+use cosmwasm_std::Uint128;
+use cw_core_macros::voting_query;
+use cw_utils::Duration;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InstantiateMsg {
+    // Owner can update all configs including changing the owner. This will generally be a DAO.
+    pub owner: Option<String>,
+    // Manager can update all configs except changing the owner. This will generally be an operations multisig for a DAO.
+    pub manager: Option<String>,
+    // Token denom e.g. ujuno, or some ibc denom
+    pub denom: String,
+    // How long until the tokens become liquid again
+    pub unstaking_duration: Option<Duration>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ExecuteMsg {
+    Stake {},
+    Unstake {
+        amount: Uint128,
+    },
+    UpdateConfig {
+        owner: Option<String>,
+        manager: Option<String>,
+        duration: Option<Duration>,
+    },
+    Claim {},
+}
+
+#[voting_query]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum QueryMsg {
+    Dao {},
+    GetConfig {},
+    Claims { address: String },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+pub struct MigrateMsg {}
diff --git a/contracts/cw-native-staked-balance-voting/src/state.rs b/contracts/cw-native-staked-balance-voting/src/state.rs
new file mode 100644
index 00000000..4f59cf9c
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/src/state.rs
@@ -0,0 +1,35 @@
+use cosmwasm_std::{Addr, Uint128};
+use cw_controllers::Claims;
+use cw_storage_plus::{Item, SnapshotItem, SnapshotMap, Strategy};
+use cw_utils::Duration;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct Config {
+    pub owner: Option<Addr>,
+    pub manager: Option<Addr>,
+    pub denom: String,
+    pub unstaking_duration: Option<Duration>,
+}
+
+pub const CONFIG: Item<Config> = Item::new("config");
+pub const DAO: Item<Addr> = Item::new("dao");
+pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new(
+    "staked_balances",
+    "staked_balance__checkpoints",
+    "staked_balance__changelog",
+    Strategy::EveryBlock,
+);
+
+pub const STAKED_TOTAL: SnapshotItem<Uint128> = SnapshotItem::new(
+    "total_staked",
+    "total_staked__checkpoints",
+    "total_staked__changelog",
+    Strategy::EveryBlock,
+);
+
+/// The maximum number of claims that may be outstanding.
+pub const MAX_CLAIMS: u64 = 100;
+
+pub const CLAIMS: Claims = Claims::new("claims");
diff --git a/contracts/cw20-stake/examples/schema.rs b/contracts/cw20-stake/examples/schema.rs
index 354bc936..ad360754 100644
--- a/contracts/cw20-stake/examples/schema.rs
+++ b/contracts/cw20-stake/examples/schema.rs
@@ -1,17 +1,18 @@
 use std::env::current_dir;
 use std::fs::create_dir_all;
 
-use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
+use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for};
 
 use cw20::{
     AllAccountsResponse, AllAllowancesResponse, AllowanceResponse, BalanceResponse,
     TokenInfoResponse,
 };
 use cw20_stake::msg::{
-    ClaimsResponse, ExecuteMsg, GetConfigResponse, GetHooksResponse, InstantiateMsg, QueryMsg,
+    ClaimsResponse, ExecuteMsg, GetHooksResponse, InstantiateMsg, QueryMsg,
     StakedBalanceAtHeightResponse, StakedValueResponse, TotalStakedAtHeightResponse,
     TotalValueResponse,
 };
+use cw20_stake::state::Config;
 
 fn main() {
     let mut out_dir = current_dir().unwrap();
@@ -26,7 +27,6 @@ fn main() {
     export_schema(&schema_for!(TotalStakedAtHeightResponse), &out_dir);
     export_schema(&schema_for!(StakedValueResponse), &out_dir);
     export_schema(&schema_for!(TotalValueResponse), &out_dir);
-    export_schema(&schema_for!(GetConfigResponse), &out_dir);
     export_schema(&schema_for!(GetHooksResponse), &out_dir);
     export_schema(&schema_for!(ClaimsResponse), &out_dir);
     export_schema(&schema_for!(AllowanceResponse), &out_dir);
@@ -34,4 +34,7 @@ fn main() {
     export_schema(&schema_for!(TokenInfoResponse), &out_dir);
     export_schema(&schema_for!(AllAllowancesResponse), &out_dir);
     export_schema(&schema_for!(AllAccountsResponse), &out_dir);
+
+    // Need to rename so it matches the TS pattern
+    export_schema_with_title(&schema_for!(Config), &out_dir, "GetConfigResponse");
 }
diff --git a/contracts/cw20-stake/schema/get_config_response.json b/contracts/cw20-stake/schema/get_config_response.json
index c673bbf0..ad6a2497 100644
--- a/contracts/cw20-stake/schema/get_config_response.json
+++ b/contracts/cw20-stake/schema/get_config_response.json
@@ -7,19 +7,27 @@
   ],
   "properties": {
     "manager": {
-      "type": [
-        "string",
-        "null"
+      "anyOf": [
+        {
+          "$ref": "#/definitions/Addr"
+        },
+        {
+          "type": "null"
+        }
       ]
     },
     "owner": {
-      "type": [
-        "string",
-        "null"
+      "anyOf": [
+        {
+          "$ref": "#/definitions/Addr"
+        },
+        {
+          "type": "null"
+        }
       ]
     },
     "token_address": {
-      "type": "string"
+      "$ref": "#/definitions/Addr"
     },
     "unstaking_duration": {
       "anyOf": [
@@ -33,6 +41,10 @@
     }
   },
   "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"
+    },
     "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": [
diff --git a/contracts/cw20-stake/src/contract.rs b/contracts/cw20-stake/src/contract.rs
index d4d2cff3..7efef11b 100644
--- a/contracts/cw20-stake/src/contract.rs
+++ b/contracts/cw20-stake/src/contract.rs
@@ -10,8 +10,8 @@ use cw20::Cw20ReceiveMsg;
 
 use crate::hooks::{stake_hook_msgs, unstake_hook_msgs};
 use crate::msg::{
-    ExecuteMsg, GetConfigResponse, GetHooksResponse, InstantiateMsg, MigrateMsg, QueryMsg,
-    ReceiveMsg, StakedBalanceAtHeightResponse, StakedValueResponse, TotalStakedAtHeightResponse,
+    ExecuteMsg, GetHooksResponse, InstantiateMsg, MigrateMsg, QueryMsg, ReceiveMsg,
+    StakedBalanceAtHeightResponse, StakedValueResponse, TotalStakedAtHeightResponse,
     TotalValueResponse,
 };
 use crate::state::{
@@ -35,6 +35,24 @@ use cw_utils::Duration;
 const CONTRACT_NAME: &str = "crates.io:cw20-stake";
 const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
 
+fn validate_duration(duration: Option<Duration>) -> Result<(), ContractError> {
+    if let Some(unstaking_duration) = duration {
+        match unstaking_duration {
+            Duration::Height(height) => {
+                if height == 0 {
+                    return Err(ContractError::InvalidUnstakingDuration {});
+                }
+            }
+            Duration::Time(time) => {
+                if time == 0 {
+                    return Err(ContractError::InvalidUnstakingDuration {});
+                }
+            }
+        }
+    }
+    Ok(())
+}
+
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn instantiate(
     deps: DepsMut,
@@ -52,6 +70,7 @@ pub fn instantiate(
         None => None,
     };
 
+    validate_duration(msg.unstaking_duration)?;
     let config = Config {
         owner,
         manager,
@@ -106,6 +125,8 @@ pub fn execute_update_config(
         return Err(ContractError::OnlyOwnerCanChangeOwner {});
     };
 
+    validate_duration(duration)?;
+
     config.owner = new_owner;
     config.manager = new_manager;
 
@@ -416,14 +437,9 @@ pub fn query_total_value(deps: Deps, _env: Env) -> StdResult<TotalValueResponse>
     Ok(TotalValueResponse { total: balance })
 }
 
-pub fn query_config(deps: Deps) -> StdResult<GetConfigResponse> {
+pub fn query_config(deps: Deps) -> StdResult<Config> {
     let config = CONFIG.load(deps.storage)?;
-    Ok(GetConfigResponse {
-        owner: config.owner.map(|a| a.to_string()),
-        manager: config.manager.map(|a| a.to_string()),
-        unstaking_duration: config.unstaking_duration,
-        token_address: config.token_address.to_string(),
-    })
+    Ok(config)
 }
 
 pub fn query_claims(deps: Deps, address: String) -> StdResult<ClaimsResponse> {
@@ -468,953 +484,3 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, Co
         MigrateMsg::FromCompatible {} => Ok(Response::default()),
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use std::borrow::BorrowMut;
-
-    use crate::msg::{
-        ExecuteMsg, GetConfigResponse, QueryMsg, ReceiveMsg, StakedBalanceAtHeightResponse,
-        StakedValueResponse, TotalStakedAtHeightResponse, TotalValueResponse,
-    };
-    use crate::state::MAX_CLAIMS;
-    use crate::ContractError;
-    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
-    use cosmwasm_std::{to_binary, Addr, Empty, MessageInfo, Uint128};
-    use cw20::Cw20Coin;
-    use cw_utils::Duration;
-
-    use cw_multi_test::{next_block, App, AppResponse, Contract, ContractWrapper, Executor};
-
-    use anyhow::Result as AnyResult;
-
-    use cw_controllers::{Claim, ClaimsResponse};
-    use cw_utils::Expiration::AtHeight;
-
-    const ADDR1: &str = "addr0001";
-    const ADDR2: &str = "addr0002";
-    const ADDR3: &str = "addr0003";
-    const ADDR4: &str = "addr0004";
-
-    pub fn contract_staking() -> Box<dyn Contract<Empty>> {
-        let contract = ContractWrapper::new(
-            crate::contract::execute,
-            crate::contract::instantiate,
-            crate::contract::query,
-        );
-        Box::new(contract)
-    }
-
-    pub fn contract_cw20() -> Box<dyn Contract<Empty>> {
-        let contract = ContractWrapper::new(
-            cw20_base::contract::execute,
-            cw20_base::contract::instantiate,
-            cw20_base::contract::query,
-        );
-        Box::new(contract)
-    }
-
-    fn mock_app() -> App {
-        App::default()
-    }
-
-    fn get_balance<T: Into<String>, U: Into<String>>(
-        app: &App,
-        contract_addr: T,
-        address: U,
-    ) -> Uint128 {
-        let msg = cw20::Cw20QueryMsg::Balance {
-            address: address.into(),
-        };
-        let result: cw20::BalanceResponse =
-            app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
-        result.balance
-    }
-
-    fn instantiate_cw20(app: &mut App, initial_balances: Vec<Cw20Coin>) -> Addr {
-        let cw20_id = app.store_code(contract_cw20());
-        let msg = cw20_base::msg::InstantiateMsg {
-            name: String::from("Test"),
-            symbol: String::from("TEST"),
-            decimals: 6,
-            initial_balances,
-            mint: None,
-            marketing: None,
-        };
-
-        app.instantiate_contract(cw20_id, Addr::unchecked(ADDR1), &msg, &[], "cw20", None)
-            .unwrap()
-    }
-
-    fn instantiate_staking(
-        app: &mut App,
-        cw20: Addr,
-        unstaking_duration: Option<Duration>,
-    ) -> Addr {
-        let staking_code_id = app.store_code(contract_staking());
-        let msg = crate::msg::InstantiateMsg {
-            owner: Some("owner".to_string()),
-            manager: Some("manager".to_string()),
-            token_address: cw20.to_string(),
-            unstaking_duration,
-        };
-        app.instantiate_contract(
-            staking_code_id,
-            Addr::unchecked(ADDR1),
-            &msg,
-            &[],
-            "staking",
-            None,
-        )
-        .unwrap()
-    }
-
-    fn setup_test_case(
-        app: &mut App,
-        initial_balances: Vec<Cw20Coin>,
-        unstaking_duration: Option<Duration>,
-    ) -> (Addr, Addr) {
-        // Instantiate cw20 contract
-        let cw20_addr = instantiate_cw20(app, initial_balances);
-        app.update_block(next_block);
-        // Instantiate staking contract
-        let staking_addr = instantiate_staking(app, cw20_addr.clone(), unstaking_duration);
-        app.update_block(next_block);
-        (staking_addr, cw20_addr)
-    }
-
-    fn query_staked_balance<T: Into<String>, U: Into<String>>(
-        app: &App,
-        contract_addr: T,
-        address: U,
-    ) -> Uint128 {
-        let msg = QueryMsg::StakedBalanceAtHeight {
-            address: address.into(),
-            height: None,
-        };
-        let result: StakedBalanceAtHeightResponse =
-            app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
-        result.balance
-    }
-
-    fn query_config<T: Into<String>>(app: &App, contract_addr: T) -> GetConfigResponse {
-        let msg = QueryMsg::GetConfig {};
-        app.wrap().query_wasm_smart(contract_addr, &msg).unwrap()
-    }
-
-    fn query_total_staked<T: Into<String>>(app: &App, contract_addr: T) -> Uint128 {
-        let msg = QueryMsg::TotalStakedAtHeight { height: None };
-        let result: TotalStakedAtHeightResponse =
-            app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
-        result.total
-    }
-
-    fn query_staked_value<T: Into<String>, U: Into<String>>(
-        app: &App,
-        contract_addr: T,
-        address: U,
-    ) -> Uint128 {
-        let msg = QueryMsg::StakedValue {
-            address: address.into(),
-        };
-        let result: StakedValueResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
-        result.value
-    }
-
-    fn query_total_value<T: Into<String>>(app: &App, contract_addr: T) -> Uint128 {
-        let msg = QueryMsg::TotalValue {};
-        let result: TotalValueResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
-        result.total
-    }
-
-    fn query_claims<T: Into<String>, U: Into<String>>(
-        app: &App,
-        contract_addr: T,
-        address: U,
-    ) -> Vec<Claim> {
-        let msg = QueryMsg::Claims {
-            address: address.into(),
-        };
-        let result: ClaimsResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
-        result.claims
-    }
-
-    fn stake_tokens(
-        app: &mut App,
-        staking_addr: &Addr,
-        cw20_addr: &Addr,
-        info: MessageInfo,
-        amount: Uint128,
-    ) -> AnyResult<AppResponse> {
-        let msg = cw20::Cw20ExecuteMsg::Send {
-            contract: staking_addr.to_string(),
-            amount,
-            msg: to_binary(&ReceiveMsg::Stake {}).unwrap(),
-        };
-        app.execute_contract(info.sender, cw20_addr.clone(), &msg, &[])
-    }
-
-    fn update_config(
-        app: &mut App,
-        staking_addr: &Addr,
-        info: MessageInfo,
-        owner: Option<Addr>,
-        manager: Option<Addr>,
-        duration: Option<Duration>,
-    ) -> AnyResult<AppResponse> {
-        let msg = ExecuteMsg::UpdateConfig {
-            owner: owner.map(|a| a.to_string()),
-            manager: manager.map(|a| a.to_string()),
-            duration,
-        };
-        app.execute_contract(info.sender, staking_addr.clone(), &msg, &[])
-    }
-
-    fn unstake_tokens(
-        app: &mut App,
-        staking_addr: &Addr,
-        info: MessageInfo,
-        amount: Uint128,
-    ) -> AnyResult<AppResponse> {
-        let msg = ExecuteMsg::Unstake { amount };
-        app.execute_contract(info.sender, staking_addr.clone(), &msg, &[])
-    }
-
-    fn claim_tokens(
-        app: &mut App,
-        staking_addr: &Addr,
-        info: MessageInfo,
-    ) -> AnyResult<AppResponse> {
-        let msg = ExecuteMsg::Claim {};
-        app.execute_contract(info.sender, staking_addr.clone(), &msg, &[])
-    }
-
-    #[test]
-    fn test_update_config() {
-        let _deps = mock_dependencies();
-
-        let mut app = mock_app();
-        let amount1 = Uint128::from(100u128);
-        let _token_address = Addr::unchecked("token_address");
-        let initial_balances = vec![Cw20Coin {
-            address: ADDR1.to_string(),
-            amount: amount1,
-        }];
-        let (staking_addr, _cw20_addr) = setup_test_case(&mut app, initial_balances, None);
-
-        let info = mock_info("owner", &[]);
-        let _env = mock_env();
-        // Test update admin
-        update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            Some(Addr::unchecked("owner2")),
-            None,
-            Some(Duration::Height(100)),
-        )
-        .unwrap();
-
-        let config = query_config(&app, &staking_addr);
-        assert_eq!(config.owner, Some("owner2".to_string()));
-        assert_eq!(config.unstaking_duration, Some(Duration::Height(100)));
-
-        // Try updating owner with original owner, which is now invalid
-        let info = mock_info("owner", &[]);
-        let _err = update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            Some(Addr::unchecked("owner3")),
-            None,
-            Some(Duration::Height(100)),
-        )
-        .unwrap_err();
-
-        // Add manager
-        let info = mock_info("owner2", &[]);
-        let _env = mock_env();
-        update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            Some(Addr::unchecked("owner2")),
-            Some(Addr::unchecked("manager")),
-            Some(Duration::Height(100)),
-        )
-        .unwrap();
-
-        let config = query_config(&app, &staking_addr);
-        assert_eq!(config.owner, Some("owner2".to_string()));
-        assert_eq!(config.manager, Some("manager".to_string()));
-
-        // Manager can update unstaking duration
-        let info = mock_info("manager", &[]);
-        let _env = mock_env();
-        update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            Some(Addr::unchecked("owner2")),
-            Some(Addr::unchecked("manager")),
-            Some(Duration::Height(50)),
-        )
-        .unwrap();
-        let config = query_config(&app, &staking_addr);
-        assert_eq!(config.owner, Some("owner2".to_string()));
-        assert_eq!(config.unstaking_duration, Some(Duration::Height(50)));
-
-        // Manager cannot update owner
-        let info = mock_info("manager", &[]);
-        let _env = mock_env();
-        update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            Some(Addr::unchecked("manager")),
-            Some(Addr::unchecked("manager")),
-            Some(Duration::Height(50)),
-        )
-        .unwrap_err();
-
-        // Manager can update manager
-        let info = mock_info("owner2", &[]);
-        let _env = mock_env();
-        update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            Some(Addr::unchecked("owner2")),
-            None,
-            Some(Duration::Height(50)),
-        )
-        .unwrap();
-
-        let config = query_config(&app, &staking_addr);
-        assert_eq!(config.owner, Some("owner2".to_string()));
-        assert_eq!(config.manager, None);
-
-        // Remove owner
-        let info = mock_info("owner2", &[]);
-        let _env = mock_env();
-        update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            None,
-            None,
-            Some(Duration::Height(100)),
-        )
-        .unwrap();
-
-        // Assert no further updates can be made
-        let info = mock_info("owner2", &[]);
-        let _env = mock_env();
-        let err: ContractError = update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            None,
-            None,
-            Some(Duration::Height(100)),
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-        assert_eq!(err, ContractError::Unauthorized {});
-
-        let info = mock_info("manager", &[]);
-        let _env = mock_env();
-        let err: ContractError = update_config(
-            &mut app,
-            &staking_addr,
-            info,
-            None,
-            None,
-            Some(Duration::Height(100)),
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-        assert_eq!(err, ContractError::Unauthorized {})
-    }
-
-    #[test]
-    fn test_staking() {
-        let _deps = mock_dependencies();
-
-        let mut app = mock_app();
-        let amount1 = Uint128::from(100u128);
-        let _token_address = Addr::unchecked("token_address");
-        let initial_balances = vec![Cw20Coin {
-            address: ADDR1.to_string(),
-            amount: amount1,
-        }];
-        let (staking_addr, cw20_addr) = setup_test_case(&mut app, initial_balances, None);
-
-        let info = mock_info(ADDR1, &[]);
-        let _env = mock_env();
-
-        // Successful bond
-        let amount = Uint128::new(50);
-        stake_tokens(&mut app, &staking_addr, &cw20_addr, info.clone(), amount).unwrap();
-
-        // Very important that this balances is not reflected until
-        // the next block. This protects us from flash loan hostile
-        // takeovers.
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::zero()
-        );
-
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(50u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(50u128)
-        );
-        assert_eq!(
-            get_balance(&app, &cw20_addr, ADDR1.to_string()),
-            Uint128::from(50u128)
-        );
-
-        // Can't transfer bonded amount
-        let msg = cw20::Cw20ExecuteMsg::Transfer {
-            recipient: ADDR2.to_string(),
-            amount: Uint128::from(51u128),
-        };
-        let _err = app
-            .borrow_mut()
-            .execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
-            .unwrap_err();
-
-        // Sucessful transfer of unbonded amount
-        let msg = cw20::Cw20ExecuteMsg::Transfer {
-            recipient: ADDR2.to_string(),
-            amount: Uint128::from(20u128),
-        };
-        let _res = app
-            .borrow_mut()
-            .execute_contract(info.sender, cw20_addr.clone(), &msg, &[])
-            .unwrap();
-
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(30u128));
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(20u128));
-
-        // Addr 2 successful bond
-        let info = mock_info(ADDR2, &[]);
-        stake_tokens(&mut app, &staking_addr, &cw20_addr, info, Uint128::new(20)).unwrap();
-
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR2),
-            Uint128::from(20u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(70u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::zero());
-
-        // Can't unstake more than you have staked
-        let info = mock_info(ADDR2, &[]);
-        let _err = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(100)).unwrap_err();
-
-        // Successful unstake
-        let info = mock_info(ADDR2, &[]);
-        let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(10)).unwrap();
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR2),
-            Uint128::from(10u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(60u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(10u128));
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1),
-            Uint128::from(50u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(30u128));
-    }
-
-    #[test]
-    fn text_max_claims() {
-        let mut app = mock_app();
-        let amount1 = Uint128::from(MAX_CLAIMS + 1);
-        let unstaking_blocks = 1u64;
-        let _token_address = Addr::unchecked("token_address");
-        let initial_balances = vec![Cw20Coin {
-            address: ADDR1.to_string(),
-            amount: amount1,
-        }];
-        let (staking_addr, cw20_addr) = setup_test_case(
-            &mut app,
-            initial_balances,
-            Some(Duration::Height(unstaking_blocks)),
-        );
-
-        let info = mock_info(ADDR1, &[]);
-        stake_tokens(&mut app, &staking_addr, &cw20_addr, info.clone(), amount1).unwrap();
-
-        // Create the max number of claims
-        for _ in 0..MAX_CLAIMS {
-            unstake_tokens(&mut app, &staking_addr, info.clone(), Uint128::new(1)).unwrap();
-        }
-
-        // Additional unstaking attempts ought to fail.
-        unstake_tokens(&mut app, &staking_addr, info.clone(), Uint128::new(1)).unwrap_err();
-
-        // Clear out the claims list.
-        app.update_block(next_block);
-        claim_tokens(&mut app, &staking_addr, info.clone()).unwrap();
-
-        // Unstaking now allowed again.
-        unstake_tokens(&mut app, &staking_addr, info.clone(), Uint128::new(1)).unwrap();
-        app.update_block(next_block);
-        claim_tokens(&mut app, &staking_addr, info).unwrap();
-
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), amount1);
-    }
-
-    #[test]
-    fn test_unstaking_with_claims() {
-        let _deps = mock_dependencies();
-
-        let mut app = mock_app();
-        let amount1 = Uint128::from(100u128);
-        let unstaking_blocks = 10u64;
-        let _token_address = Addr::unchecked("token_address");
-        let initial_balances = vec![Cw20Coin {
-            address: ADDR1.to_string(),
-            amount: amount1,
-        }];
-        let (staking_addr, cw20_addr) = setup_test_case(
-            &mut app,
-            initial_balances,
-            Some(Duration::Height(unstaking_blocks)),
-        );
-
-        let info = mock_info(ADDR1, &[]);
-
-        // Successful bond
-        let _res =
-            stake_tokens(&mut app, &staking_addr, &cw20_addr, info, Uint128::new(50)).unwrap();
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1),
-            Uint128::from(50u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(50u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(50u128));
-
-        // Unstake
-        let info = mock_info(ADDR1, &[]);
-        let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(10)).unwrap();
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1),
-            Uint128::from(40u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(40u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(50u128));
-
-        // Cannot claim when nothing is available
-        let info = mock_info(ADDR1, &[]);
-        let _err: ContractError = claim_tokens(&mut app, &staking_addr, info)
-            .unwrap_err()
-            .downcast()
-            .unwrap();
-        assert_eq!(_err, ContractError::NothingToClaim {});
-
-        // Successful claim
-        app.update_block(|b| b.height += unstaking_blocks);
-        let info = mock_info(ADDR1, &[]);
-        let _res = claim_tokens(&mut app, &staking_addr, info).unwrap();
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1),
-            Uint128::from(40u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(40u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(60u128));
-
-        // Unstake and claim multiple
-        let _info = mock_info(ADDR1, &[]);
-        let info = mock_info(ADDR1, &[]);
-        let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(5)).unwrap();
-        app.update_block(next_block);
-
-        let _info = mock_info(ADDR1, &[]);
-        let info = mock_info(ADDR1, &[]);
-        let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(5)).unwrap();
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1),
-            Uint128::from(30u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(30u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(60u128));
-
-        app.update_block(|b| b.height += unstaking_blocks);
-        let info = mock_info(ADDR1, &[]);
-        let _res = claim_tokens(&mut app, &staking_addr, info).unwrap();
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1),
-            Uint128::from(30u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(30u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(70u128));
-    }
-
-    #[test]
-    fn multiple_address_staking() {
-        let amount1 = Uint128::from(100u128);
-        let initial_balances = vec![
-            Cw20Coin {
-                address: ADDR1.to_string(),
-                amount: amount1,
-            },
-            Cw20Coin {
-                address: ADDR2.to_string(),
-                amount: amount1,
-            },
-            Cw20Coin {
-                address: ADDR3.to_string(),
-                amount: amount1,
-            },
-            Cw20Coin {
-                address: ADDR4.to_string(),
-                amount: amount1,
-            },
-        ];
-        let mut app = mock_app();
-        let amount1 = Uint128::from(100u128);
-        let unstaking_blocks = 10u64;
-        let _token_address = Addr::unchecked("token_address");
-        let (staking_addr, cw20_addr) = setup_test_case(
-            &mut app,
-            initial_balances,
-            Some(Duration::Height(unstaking_blocks)),
-        );
-
-        let info = mock_info(ADDR1, &[]);
-        // Successful bond
-        let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount1).unwrap();
-        app.update_block(next_block);
-
-        let info = mock_info(ADDR2, &[]);
-        // Successful bond
-        let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount1).unwrap();
-        app.update_block(next_block);
-
-        let info = mock_info(ADDR3, &[]);
-        // Successful bond
-        let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount1).unwrap();
-        app.update_block(next_block);
-
-        let info = mock_info(ADDR4, &[]);
-        // Successful bond
-        let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount1).unwrap();
-        app.update_block(next_block);
-
-        assert_eq!(query_staked_balance(&app, &staking_addr, ADDR1), amount1);
-        assert_eq!(query_staked_balance(&app, &staking_addr, ADDR2), amount1);
-        assert_eq!(query_staked_balance(&app, &staking_addr, ADDR3), amount1);
-        assert_eq!(query_staked_balance(&app, &staking_addr, ADDR4), amount1);
-
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            amount1.checked_mul(Uint128::new(4)).unwrap()
-        );
-
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::zero());
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::zero());
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR3), Uint128::zero());
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR4), Uint128::zero());
-    }
-
-    #[test]
-    fn test_auto_compounding_staking() {
-        let _deps = mock_dependencies();
-
-        let mut app = mock_app();
-        let amount1 = Uint128::from(1000u128);
-        let _token_address = Addr::unchecked("token_address");
-        let initial_balances = vec![Cw20Coin {
-            address: ADDR1.to_string(),
-            amount: amount1,
-        }];
-        let (staking_addr, cw20_addr) = setup_test_case(&mut app, initial_balances, None);
-
-        let info = mock_info(ADDR1, &[]);
-        let _env = mock_env();
-
-        // Successful bond
-        let amount = Uint128::new(100);
-        stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount).unwrap();
-        app.update_block(next_block);
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            query_staked_value(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            query_total_value(&app, &staking_addr),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            get_balance(&app, &cw20_addr, ADDR1.to_string()),
-            Uint128::from(900u128)
-        );
-
-        // Add compounding rewards
-        let msg = cw20::Cw20ExecuteMsg::Send {
-            contract: staking_addr.to_string(),
-            amount: Uint128::from(100u128),
-            msg: to_binary(&ReceiveMsg::Fund {}).unwrap(),
-        };
-        let _res = app
-            .borrow_mut()
-            .execute_contract(Addr::unchecked(ADDR1), cw20_addr.clone(), &msg, &[])
-            .unwrap();
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            query_staked_value(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(200u128)
-        );
-        assert_eq!(
-            query_total_value(&app, &staking_addr),
-            Uint128::from(200u128)
-        );
-        assert_eq!(
-            get_balance(&app, &cw20_addr, ADDR1.to_string()),
-            Uint128::from(800u128)
-        );
-
-        // Sucessful transfer of unbonded amount
-        let msg = cw20::Cw20ExecuteMsg::Transfer {
-            recipient: ADDR2.to_string(),
-            amount: Uint128::from(100u128),
-        };
-        let _res = app
-            .borrow_mut()
-            .execute_contract(Addr::unchecked(ADDR1), cw20_addr.clone(), &msg, &[])
-            .unwrap();
-
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(700u128));
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(100u128));
-
-        // Addr 2 successful bond
-        let info = mock_info(ADDR2, &[]);
-        stake_tokens(&mut app, &staking_addr, &cw20_addr, info, Uint128::new(100)).unwrap();
-
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR2),
-            Uint128::from(50u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(150u128)
-        );
-        assert_eq!(
-            query_staked_value(&app, &staking_addr, ADDR2.to_string()),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            query_total_value(&app, &staking_addr),
-            Uint128::from(300u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::zero());
-
-        // Can't unstake more than you have staked
-        let info = mock_info(ADDR2, &[]);
-        let _err = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(51)).unwrap_err();
-
-        // Add compounding rewards
-        let msg = cw20::Cw20ExecuteMsg::Send {
-            contract: staking_addr.to_string(),
-            amount: Uint128::from(90u128),
-            msg: to_binary(&ReceiveMsg::Fund {}).unwrap(),
-        };
-        let _res = app
-            .borrow_mut()
-            .execute_contract(Addr::unchecked(ADDR1), cw20_addr.clone(), &msg, &[])
-            .unwrap();
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR2),
-            Uint128::from(50u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(150u128)
-        );
-        assert_eq!(
-            query_staked_value(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(260u128)
-        );
-        assert_eq!(
-            query_staked_value(&app, &staking_addr, ADDR2.to_string()),
-            Uint128::from(130u128)
-        );
-        assert_eq!(
-            query_total_value(&app, &staking_addr),
-            Uint128::from(390u128)
-        );
-        assert_eq!(
-            get_balance(&app, &cw20_addr, ADDR1.to_string()),
-            Uint128::from(610u128)
-        );
-
-        // Successful unstake
-        let info = mock_info(ADDR2, &[]);
-        let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(25)).unwrap();
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR2),
-            Uint128::from(25u128)
-        );
-        assert_eq!(
-            query_total_staked(&app, &staking_addr),
-            Uint128::from(125u128)
-        );
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(65u128));
-    }
-
-    #[test]
-    fn test_simple_unstaking_with_duration() {
-        let _deps = mock_dependencies();
-
-        let mut app = mock_app();
-        let amount1 = Uint128::from(100u128);
-        let _token_address = Addr::unchecked("token_address");
-        let initial_balances = vec![
-            Cw20Coin {
-                address: ADDR1.to_string(),
-                amount: amount1,
-            },
-            Cw20Coin {
-                address: ADDR2.to_string(),
-                amount: amount1,
-            },
-        ];
-        let (staking_addr, cw20_addr) =
-            setup_test_case(&mut app, initial_balances, Some(Duration::Height(1)));
-
-        // Bond Address 1
-        let info = mock_info(ADDR1, &[]);
-        let _env = mock_env();
-        let amount = Uint128::new(100);
-        stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount).unwrap();
-
-        // Bond Address 2
-        let info = mock_info(ADDR2, &[]);
-        let _env = mock_env();
-        let amount = Uint128::new(100);
-        stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount).unwrap();
-        app.update_block(next_block);
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(100u128)
-        );
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(100u128)
-        );
-
-        // Unstake Addr1
-        let info = mock_info(ADDR1, &[]);
-        let _env = mock_env();
-        let amount = Uint128::new(100);
-        unstake_tokens(&mut app, &staking_addr, info, amount).unwrap();
-
-        // Unstake Addr2
-        let info = mock_info(ADDR2, &[]);
-        let _env = mock_env();
-        let amount = Uint128::new(100);
-        unstake_tokens(&mut app, &staking_addr, info, amount).unwrap();
-
-        app.update_block(next_block);
-
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
-            Uint128::from(0u128)
-        );
-        assert_eq!(
-            query_staked_balance(&app, &staking_addr, ADDR2.to_string()),
-            Uint128::from(0u128)
-        );
-
-        // Claim
-        assert_eq!(
-            query_claims(&app, &staking_addr, ADDR1),
-            vec![Claim {
-                amount: Uint128::new(100),
-                release_at: AtHeight(12349)
-            }]
-        );
-        assert_eq!(
-            query_claims(&app, &staking_addr, ADDR2),
-            vec![Claim {
-                amount: Uint128::new(100),
-                release_at: AtHeight(12349)
-            }]
-        );
-
-        let info = mock_info(ADDR1, &[]);
-        claim_tokens(&mut app, &staking_addr, info).unwrap();
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(100u128));
-
-        let info = mock_info(ADDR2, &[]);
-        claim_tokens(&mut app, &staking_addr, info).unwrap();
-        assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(100u128));
-    }
-}
diff --git a/contracts/cw20-stake/src/error.rs b/contracts/cw20-stake/src/error.rs
index 98094e75..c3e17f5c 100644
--- a/contracts/cw20-stake/src/error.rs
+++ b/contracts/cw20-stake/src/error.rs
@@ -21,4 +21,6 @@ pub enum ContractError {
     HookError(#[from] cw_controllers::HookError),
     #[error("Only owner can change owner")]
     OnlyOwnerCanChangeOwner {},
+    #[error("Invalid unstaking duration, unstaking duration cannot be 0")]
+    InvalidUnstakingDuration {},
 }
diff --git a/contracts/cw20-stake/src/lib.rs b/contracts/cw20-stake/src/lib.rs
index af953d2e..efc1a5ec 100644
--- a/contracts/cw20-stake/src/lib.rs
+++ b/contracts/cw20-stake/src/lib.rs
@@ -4,4 +4,7 @@ pub mod hooks;
 pub mod msg;
 pub mod state;
 
+#[cfg(test)]
+mod tests;
+
 pub use crate::error::ContractError;
diff --git a/contracts/cw20-stake/src/msg.rs b/contracts/cw20-stake/src/msg.rs
index 08ca1f38..64f8f8f8 100644
--- a/contracts/cw20-stake/src/msg.rs
+++ b/contracts/cw20-stake/src/msg.rs
@@ -99,15 +99,6 @@ pub struct TotalValueResponse {
     pub total: Uint128,
 }
 
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub struct GetConfigResponse {
-    pub owner: Option<String>,
-    pub manager: Option<String>,
-    pub token_address: String,
-    pub unstaking_duration: Option<Duration>,
-}
-
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub struct GetHooksResponse {
diff --git a/contracts/cw20-staked-balance-voting/src/contract.rs b/contracts/cw20-staked-balance-voting/src/contract.rs
index 4d55c990..e12815e0 100644
--- a/contracts/cw20-staked-balance-voting/src/contract.rs
+++ b/contracts/cw20-staked-balance-voting/src/contract.rs
@@ -65,7 +65,7 @@ pub fn instantiate(
                 } => {
                     let staking_contract_address =
                         deps.api.addr_validate(&staking_contract_address)?;
-                    let resp: cw20_stake::msg::GetConfigResponse = deps.querier.query_wasm_smart(
+                    let resp: cw20_stake::state::Config = deps.querier.query_wasm_smart(
                         &staking_contract_address,
                         &cw20_stake::msg::QueryMsg::GetConfig {},
                     )?;
diff --git a/scripts/deploy_local.sh b/scripts/deploy_local.sh
index 3abbc855..17e2e4e5 100755
--- a/scripts/deploy_local.sh
+++ b/scripts/deploy_local.sh
@@ -177,6 +177,11 @@ CW_CORE_DAO_CONTRACT=$($BINARY q wasm list-contract-by-code $cw_core_CODE_ID --o
 # actions like executing proposals that require it to pay gas fees.
 $BINARY tx bank send validator $CW_CORE_DAO_CONTRACT 9000000$DENOM --chain-id testing $TXFLAG -y
 
+# Instantiate cw-admin-factory
+echo xxxxxxxxx | $BINARY tx wasm instantiate "$cw_admin_factory_CODE_ID" "{}" --from validator --label "cw admin factory" $TXFLAG --output json --no-admin
+
+CW_ADMIN_FACTORY_CONTRACT=$($BINARY q wasm list-contract-by-code $cw_admin_factory_CODE_ID --output json | jq -r '.contracts[-1]')
+
 
 # Print out config variables
 printf "\n ------------------------ \n"
@@ -190,3 +195,4 @@ echo "NEXT_PUBLIC_CW4VOTING_CODE_ID=$cw4_voting_CODE_ID"
 echo "NEXT_PUBLIC_CW20STAKEDBALANCEVOTING_CODE_ID=$cw20_staked_balance_voting_CODE_ID"
 echo "NEXT_PUBLIC_STAKECW20_CODE_ID=$stake_cw20_CODE_ID"
 echo "NEXT_PUBLIC_DAO_CONTRACT_ADDRESS=$CW_CORE_DAO_CONTRACT"
+echo "NEXT_PUBLIC_V1_FACTORY_CONTRACT_ADDRESS=$CW_ADMIN_FACTORY_CONTRACT"
\ No newline at end of file
diff --git a/types/contracts/cw-native-staked-balance-voting/CwNativeStakedBalanceVotingContract.ts b/types/contracts/cw-native-staked-balance-voting/CwNativeStakedBalanceVotingContract.ts
new file mode 100644
index 00000000..1578900e
--- /dev/null
+++ b/types/contracts/cw-native-staked-balance-voting/CwNativeStakedBalanceVotingContract.ts
@@ -0,0 +1,291 @@
+/**
+* This file was automatically generated by cosmwasm-typescript-gen@0.3.6.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the cosmwasm-typescript-gen generate command to regenerate this file.
+*/
+
+import { CosmWasmClient, ExecuteResult, SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
+import { Coin, StdFee } from "@cosmjs/amino";
+export type Uint128 = string;
+export type Expiration = {
+  at_height: number;
+} | {
+  at_time: Timestamp;
+} | {
+  never: {
+    [k: string]: unknown;
+  };
+};
+export type Timestamp = Uint64;
+export type Uint64 = string;
+export interface ClaimsResponse {
+  claims: Claim[];
+  [k: string]: unknown;
+}
+export interface Claim {
+  amount: Uint128;
+  release_at: Expiration;
+  [k: string]: unknown;
+}
+export type DaoResponse = string;
+export type ExecuteMsg = {
+  stake: {
+    [k: string]: unknown;
+  };
+} | {
+  unstake: {
+    amount: Uint128;
+    [k: string]: unknown;
+  };
+} | {
+  update_config: {
+    duration?: Duration | null;
+    manager?: string | null;
+    owner?: string | null;
+    [k: string]: unknown;
+  };
+} | {
+  claim: {
+    [k: string]: unknown;
+  };
+};
+export type Duration = {
+  height: number;
+} | {
+  time: number;
+};
+export type Addr = string;
+export interface GetConfigResponse {
+  denom: string;
+  manager?: Addr | null;
+  owner?: Addr | null;
+  unstaking_duration?: Duration | null;
+  [k: string]: unknown;
+}
+export interface InfoResponse {
+  info: ContractVersion;
+  [k: string]: unknown;
+}
+export interface ContractVersion {
+  contract: string;
+  version: string;
+  [k: string]: unknown;
+}
+export interface InstantiateMsg {
+  denom: string;
+  manager?: string | null;
+  owner?: string | null;
+  unstaking_duration?: Duration | null;
+  [k: string]: unknown;
+}
+export interface IsActiveResponse {
+  active: boolean;
+  [k: string]: unknown;
+}
+export interface MigrateMsg {
+  [k: string]: unknown;
+}
+export type QueryMsg = {
+  dao: {
+    [k: string]: unknown;
+  };
+} | {
+  get_config: {
+    [k: string]: unknown;
+  };
+} | {
+  claims: {
+    address: string;
+    [k: string]: unknown;
+  };
+} | {
+  voting_power_at_height: {
+    address: string;
+    height?: number | null;
+    [k: string]: unknown;
+  };
+} | {
+  total_power_at_height: {
+    height?: number | null;
+    [k: string]: unknown;
+  };
+} | {
+  info: {
+    [k: string]: unknown;
+  };
+};
+export interface TotalPowerAtHeightResponse {
+  height: number;
+  power: Uint128;
+  [k: string]: unknown;
+}
+export interface VotingPowerAtHeightResponse {
+  height: number;
+  power: Uint128;
+  [k: string]: unknown;
+}
+export interface CwNativeStakedBalanceVotingReadOnlyInterface {
+  contractAddress: string;
+  dao: () => Promise<DaoResponse>;
+  getConfig: () => Promise<GetConfigResponse>;
+  claims: ({
+    address
+  }: {
+    address: string;
+  }) => Promise<ClaimsResponse>;
+  votingPowerAtHeight: ({
+    address,
+    height
+  }: {
+    address: string;
+    height?: number;
+  }) => Promise<VotingPowerAtHeightResponse>;
+  totalPowerAtHeight: ({
+    height
+  }: {
+    height?: number;
+  }) => Promise<TotalPowerAtHeightResponse>;
+  info: () => Promise<InfoResponse>;
+}
+export class CwNativeStakedBalanceVotingQueryClient implements CwNativeStakedBalanceVotingReadOnlyInterface {
+  client: CosmWasmClient;
+  contractAddress: string;
+
+  constructor(client: CosmWasmClient, contractAddress: string) {
+    this.client = client;
+    this.contractAddress = contractAddress;
+    this.dao = this.dao.bind(this);
+    this.getConfig = this.getConfig.bind(this);
+    this.claims = this.claims.bind(this);
+    this.votingPowerAtHeight = this.votingPowerAtHeight.bind(this);
+    this.totalPowerAtHeight = this.totalPowerAtHeight.bind(this);
+    this.info = this.info.bind(this);
+  }
+
+  dao = async (): Promise<DaoResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      dao: {}
+    });
+  };
+  getConfig = async (): Promise<GetConfigResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      get_config: {}
+    });
+  };
+  claims = async ({
+    address
+  }: {
+    address: string;
+  }): Promise<ClaimsResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      claims: {
+        address
+      }
+    });
+  };
+  votingPowerAtHeight = async ({
+    address,
+    height
+  }: {
+    address: string;
+    height?: number;
+  }): Promise<VotingPowerAtHeightResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      voting_power_at_height: {
+        address,
+        height
+      }
+    });
+  };
+  totalPowerAtHeight = async ({
+    height
+  }: {
+    height?: number;
+  }): Promise<TotalPowerAtHeightResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      total_power_at_height: {
+        height
+      }
+    });
+  };
+  info = async (): Promise<InfoResponse> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      info: {}
+    });
+  };
+}
+export interface CwNativeStakedBalanceVotingInterface extends CwNativeStakedBalanceVotingReadOnlyInterface {
+  contractAddress: string;
+  sender: string;
+  stake: (fee?: number | StdFee | "auto", memo?: string, funds?: readonly Coin[]) => Promise<ExecuteResult>;
+  unstake: ({
+    amount
+  }: {
+    amount: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: readonly Coin[]) => Promise<ExecuteResult>;
+  updateConfig: ({
+    duration,
+    manager,
+    owner
+  }: {
+    duration?: Duration;
+    manager?: string;
+    owner?: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: readonly Coin[]) => Promise<ExecuteResult>;
+  claim: (fee?: number | StdFee | "auto", memo?: string, funds?: readonly Coin[]) => Promise<ExecuteResult>;
+}
+export class CwNativeStakedBalanceVotingClient extends CwNativeStakedBalanceVotingQueryClient implements CwNativeStakedBalanceVotingInterface {
+  client: SigningCosmWasmClient;
+  sender: string;
+  contractAddress: string;
+
+  constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) {
+    super(client, contractAddress);
+    this.client = client;
+    this.sender = sender;
+    this.contractAddress = contractAddress;
+    this.stake = this.stake.bind(this);
+    this.unstake = this.unstake.bind(this);
+    this.updateConfig = this.updateConfig.bind(this);
+    this.claim = this.claim.bind(this);
+  }
+
+  stake = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: readonly Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      stake: {}
+    }, fee, memo, funds);
+  };
+  unstake = async ({
+    amount
+  }: {
+    amount: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: readonly Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      unstake: {
+        amount
+      }
+    }, fee, memo, funds);
+  };
+  updateConfig = async ({
+    duration,
+    manager,
+    owner
+  }: {
+    duration?: Duration;
+    manager?: string;
+    owner?: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: readonly Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      update_config: {
+        duration,
+        manager,
+        owner
+      }
+    }, fee, memo, funds);
+  };
+  claim = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: readonly Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      claim: {}
+    }, fee, memo, funds);
+  };
+}
\ No newline at end of file
diff --git a/types/contracts/cw20-stake/Cw20StakeContract.ts b/types/contracts/cw20-stake/Cw20StakeContract.ts
index 72a5febb..db691bd4 100644
--- a/types/contracts/cw20-stake/Cw20StakeContract.ts
+++ b/types/contracts/cw20-stake/Cw20StakeContract.ts
@@ -91,10 +91,11 @@ export interface Cw20ReceiveMsg {
   sender: string;
   [k: string]: unknown;
 }
+export type Addr = string;
 export interface GetConfigResponse {
-  manager?: string | null;
-  owner?: string | null;
-  token_address: string;
+  manager?: Addr | null;
+  owner?: Addr | null;
+  token_address: Addr;
   unstaking_duration?: Duration | null;
   [k: string]: unknown;
 }


Test patch:
diff --git a/contracts/cw-native-staked-balance-voting/src/tests.rs b/contracts/cw-native-staked-balance-voting/src/tests.rs
new file mode 100644
index 00000000..012354f2
--- /dev/null
+++ b/contracts/cw-native-staked-balance-voting/src/tests.rs
@@ -0,0 +1,865 @@
+use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+use crate::state::Config;
+use cosmwasm_std::{coins, Addr, Coin, Empty, Uint128};
+use cw_controllers::ClaimsResponse;
+use cw_core_interface::voting::{
+    InfoResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse,
+};
+use cw_multi_test::{
+    custom_app, next_block, App, AppResponse, Contract, ContractWrapper, Executor,
+};
+use cw_utils::Duration;
+
+const DAO_ADDR: &str = "dao";
+const ADDR1: &str = "addr1";
+const ADDR2: &str = "addr2";
+const DENOM: &str = "ujuno";
+const INVALID_DENOM: &str = "uinvalid";
+
+fn staking_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        crate::contract::execute,
+        crate::contract::instantiate,
+        crate::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn mock_app() -> App {
+    custom_app(|r, _a, s| {
+        r.bank
+            .init_balance(
+                s,
+                &Addr::unchecked(DAO_ADDR),
+                vec![
+                    Coin {
+                        denom: DENOM.to_string(),
+                        amount: Uint128::new(10000),
+                    },
+                    Coin {
+                        denom: INVALID_DENOM.to_string(),
+                        amount: Uint128::new(10000),
+                    },
+                ],
+            )
+            .unwrap();
+        r.bank
+            .init_balance(
+                s,
+                &Addr::unchecked(ADDR1),
+                vec![
+                    Coin {
+                        denom: DENOM.to_string(),
+                        amount: Uint128::new(10000),
+                    },
+                    Coin {
+                        denom: INVALID_DENOM.to_string(),
+                        amount: Uint128::new(10000),
+                    },
+                ],
+            )
+            .unwrap();
+        r.bank
+            .init_balance(
+                s,
+                &Addr::unchecked(ADDR2),
+                vec![
+                    Coin {
+                        denom: DENOM.to_string(),
+                        amount: Uint128::new(10000),
+                    },
+                    Coin {
+                        denom: INVALID_DENOM.to_string(),
+                        amount: Uint128::new(10000),
+                    },
+                ],
+            )
+            .unwrap();
+    })
+}
+
+fn instantiate_staking(app: &mut App, staking_id: u64, msg: InstantiateMsg) -> Addr {
+    app.instantiate_contract(
+        staking_id,
+        Addr::unchecked(DAO_ADDR),
+        &msg,
+        &[],
+        "Staking",
+        None,
+    )
+    .unwrap()
+}
+
+fn stake_tokens(
+    app: &mut App,
+    staking_addr: Addr,
+    sender: &str,
+    amount: u128,
+    denom: &str,
+) -> anyhow::Result<AppResponse> {
+    app.execute_contract(
+        Addr::unchecked(sender),
+        staking_addr,
+        &ExecuteMsg::Stake {},
+        &coins(amount, denom),
+    )
+}
+
+fn unstake_tokens(
+    app: &mut App,
+    staking_addr: Addr,
+    sender: &str,
+    amount: u128,
+) -> anyhow::Result<AppResponse> {
+    app.execute_contract(
+        Addr::unchecked(sender),
+        staking_addr,
+        &ExecuteMsg::Unstake {
+            amount: Uint128::new(amount),
+        },
+        &[],
+    )
+}
+
+fn claim(app: &mut App, staking_addr: Addr, sender: &str) -> anyhow::Result<AppResponse> {
+    app.execute_contract(
+        Addr::unchecked(sender),
+        staking_addr,
+        &ExecuteMsg::Claim {},
+        &[],
+    )
+}
+
+fn update_config(
+    app: &mut App,
+    staking_addr: Addr,
+    sender: &str,
+    owner: Option<String>,
+    manager: Option<String>,
+    duration: Option<Duration>,
+) -> anyhow::Result<AppResponse> {
+    app.execute_contract(
+        Addr::unchecked(sender),
+        staking_addr,
+        &ExecuteMsg::UpdateConfig {
+            owner,
+            manager,
+            duration,
+        },
+        &[],
+    )
+}
+
+fn get_voting_power_at_height(
+    app: &mut App,
+    staking_addr: Addr,
+    address: String,
+    height: Option<u64>,
+) -> VotingPowerAtHeightResponse {
+    app.wrap()
+        .query_wasm_smart(
+            staking_addr,
+            &QueryMsg::VotingPowerAtHeight { address, height },
+        )
+        .unwrap()
+}
+
+fn get_total_power_at_height(
+    app: &mut App,
+    staking_addr: Addr,
+    height: Option<u64>,
+) -> TotalPowerAtHeightResponse {
+    app.wrap()
+        .query_wasm_smart(staking_addr, &QueryMsg::TotalPowerAtHeight { height })
+        .unwrap()
+}
+
+fn get_config(app: &mut App, staking_addr: Addr) -> Config {
+    app.wrap()
+        .query_wasm_smart(staking_addr, &QueryMsg::GetConfig {})
+        .unwrap()
+}
+
+fn get_claims(app: &mut App, staking_addr: Addr, address: String) -> ClaimsResponse {
+    app.wrap()
+        .query_wasm_smart(staking_addr, &QueryMsg::Claims { address })
+        .unwrap()
+}
+
+fn get_balance(app: &mut App, address: &str, denom: &str) -> Uint128 {
+    app.wrap().query_balance(address, denom).unwrap().amount
+}
+
+#[test]
+fn test_instantiate() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    // Populated fields
+    let _addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Non populated fields
+    let _addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: None,
+            manager: None,
+            denom: DENOM.to_string(),
+            unstaking_duration: None,
+        },
+    );
+}
+
+#[test]
+#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")]
+fn test_instantiate_invalid_unstaking_duration() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    // Populated fields
+    let _addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(0)),
+        },
+    );
+
+    // Non populated fields
+    let _addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: None,
+            manager: None,
+            denom: DENOM.to_string(),
+            unstaking_duration: None,
+        },
+    );
+}
+
+#[test]
+#[should_panic(expected = "Must send reserve token 'ujuno'")]
+fn test_stake_invalid_denom() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Try and stake an invalid denom
+    stake_tokens(&mut app, addr, ADDR1, 100, INVALID_DENOM).unwrap();
+}
+
+#[test]
+fn test_stake_valid_denom() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Try and stake an valid denom
+    stake_tokens(&mut app, addr, ADDR1, 100, DENOM).unwrap();
+    app.update_block(next_block);
+}
+
+#[test]
+#[should_panic(expected = "Can only unstake less than or equal to the amount you have staked")]
+fn test_unstake_none_staked() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    unstake_tokens(&mut app, addr, ADDR1, 100).unwrap();
+}
+
+#[test]
+#[should_panic(expected = "Can only unstake less than or equal to the amount you have staked")]
+fn test_unstake_invalid_balance() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Stake some tokens
+    stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap();
+    app.update_block(next_block);
+
+    // Try and unstake too many
+    unstake_tokens(&mut app, addr, ADDR1, 200).unwrap();
+}
+
+#[test]
+fn test_unstake() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Stake some tokens
+    stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap();
+    app.update_block(next_block);
+
+    // Unstake some
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap();
+
+    // Query claims
+    let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string());
+    assert_eq!(claims.claims.len(), 1);
+    app.update_block(next_block);
+
+    // Unstake the rest
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap();
+
+    // Query claims
+    let claims = get_claims(&mut app, addr, ADDR1.to_string());
+    assert_eq!(claims.claims.len(), 2);
+}
+
+#[test]
+fn test_unstake_no_unstaking_duration() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: None,
+        },
+    );
+
+    // Stake some tokens
+    stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap();
+    app.update_block(next_block);
+
+    // Unstake some tokens
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap();
+
+    app.update_block(next_block);
+
+    let balance = get_balance(&mut app, ADDR1, DENOM);
+    // 10000 (initial bal) - 100 (staked) + 75 (unstaked) = 9975
+    assert_eq!(balance, Uint128::new(9975));
+
+    // Unstake the rest
+    unstake_tokens(&mut app, addr, ADDR1, 25).unwrap();
+
+    let balance = get_balance(&mut app, ADDR1, DENOM);
+    // 10000 (initial bal) - 100 (staked) + 75 (unstaked 1) + 25 (unstaked 2) = 10000
+    assert_eq!(balance, Uint128::new(10000))
+}
+
+#[test]
+#[should_panic(expected = "Nothing to claim")]
+fn test_claim_no_claims() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    claim(&mut app, addr, ADDR1).unwrap();
+}
+
+#[test]
+#[should_panic(expected = "Nothing to claim")]
+fn test_claim_claim_not_reached() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Stake some tokens
+    stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap();
+    app.update_block(next_block);
+
+    // Unstake them to create the claims
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 100).unwrap();
+    app.update_block(next_block);
+
+    // We have a claim but it isnt reached yet so this will still fail
+    claim(&mut app, addr, ADDR1).unwrap();
+}
+
+#[test]
+fn test_claim() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Stake some tokens
+    stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap();
+    app.update_block(next_block);
+
+    // Unstake some to create the claims
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 75).unwrap();
+    app.update_block(|b| {
+        b.height += 5;
+        b.time = b.time.plus_seconds(25);
+    });
+
+    // Claim
+    claim(&mut app, addr.clone(), ADDR1).unwrap();
+
+    // Query balance
+    let balance = get_balance(&mut app, ADDR1, DENOM);
+    // 10000 (initial bal) - 100 (staked) + 75 (unstaked) = 9975
+    assert_eq!(balance, Uint128::new(9975));
+
+    // Unstake the rest
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap();
+    app.update_block(|b| {
+        b.height += 10;
+        b.time = b.time.plus_seconds(50);
+    });
+
+    // Claim
+    claim(&mut app, addr, ADDR1).unwrap();
+
+    // Query balance
+    let balance = get_balance(&mut app, ADDR1, DENOM);
+    // 10000 (initial bal) - 100 (staked) + 75 (unstaked 1) + 25 (unstaked 2) = 10000
+    assert_eq!(balance, Uint128::new(10000));
+}
+
+#[test]
+#[should_panic(expected = "Unauthorized")]
+fn test_update_config_invalid_sender() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // From ADDR2, so not owner or manager
+    update_config(
+        &mut app,
+        addr,
+        ADDR2,
+        Some(ADDR1.to_string()),
+        Some(DAO_ADDR.to_string()),
+        Some(Duration::Height(10)),
+    )
+    .unwrap();
+}
+
+#[test]
+#[should_panic(expected = "Only owner can change owner")]
+fn test_update_config_non_owner_changes_owner() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // ADDR1 is the manager so cannot change the owner
+    update_config(&mut app, addr, ADDR1, Some(ADDR2.to_string()), None, None).unwrap();
+}
+
+#[test]
+fn test_update_config_as_owner() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Swap owner and manager, change duration
+    update_config(
+        &mut app,
+        addr.clone(),
+        DAO_ADDR,
+        Some(ADDR1.to_string()),
+        Some(DAO_ADDR.to_string()),
+        Some(Duration::Height(10)),
+    )
+    .unwrap();
+
+    let config = get_config(&mut app, addr);
+    assert_eq!(
+        Config {
+            owner: Some(Addr::unchecked(ADDR1)),
+            manager: Some(Addr::unchecked(DAO_ADDR)),
+            unstaking_duration: Some(Duration::Height(10)),
+            denom: DENOM.to_string(),
+        },
+        config
+    );
+}
+
+#[test]
+fn test_update_config_as_manager() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Change duration and manager as manager cannot change owner
+    update_config(
+        &mut app,
+        addr.clone(),
+        ADDR1,
+        Some(DAO_ADDR.to_string()),
+        Some(ADDR2.to_string()),
+        Some(Duration::Height(10)),
+    )
+    .unwrap();
+
+    let config = get_config(&mut app, addr);
+    assert_eq!(
+        Config {
+            owner: Some(Addr::unchecked(DAO_ADDR)),
+            manager: Some(Addr::unchecked(ADDR2)),
+            unstaking_duration: Some(Duration::Height(10)),
+            denom: DENOM.to_string(),
+        },
+        config
+    );
+}
+
+#[test]
+#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")]
+fn test_update_config_invalid_duration() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Change duration and manager as manager cannot change owner
+    update_config(
+        &mut app,
+        addr,
+        ADDR1,
+        Some(DAO_ADDR.to_string()),
+        Some(ADDR2.to_string()),
+        Some(Duration::Height(0)),
+    )
+    .unwrap();
+}
+
+#[test]
+fn test_query_dao() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    let msg = QueryMsg::Dao {};
+    let dao: Addr = app.wrap().query_wasm_smart(addr, &msg).unwrap();
+    assert_eq!(dao, Addr::unchecked(DAO_ADDR));
+}
+
+#[test]
+fn test_query_info() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    let msg = QueryMsg::Info {};
+    let resp: InfoResponse = app.wrap().query_wasm_smart(addr, &msg).unwrap();
+    assert_eq!(
+        resp.info.contract,
+        "crates.io:cw-native-staked-balance-voting"
+    );
+}
+
+#[test]
+fn test_query_claims() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string());
+    assert_eq!(claims.claims.len(), 0);
+
+    // Stake some tokens
+    stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap();
+    app.update_block(next_block);
+
+    // Unstake some tokens
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap();
+    app.update_block(next_block);
+
+    let claims = get_claims(&mut app, addr.clone(), ADDR1.to_string());
+    assert_eq!(claims.claims.len(), 1);
+
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 25).unwrap();
+    app.update_block(next_block);
+
+    let claims = get_claims(&mut app, addr, ADDR1.to_string());
+    assert_eq!(claims.claims.len(), 2);
+}
+
+#[test]
+fn test_query_get_config() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    let config = get_config(&mut app, addr);
+    assert_eq!(
+        config,
+        Config {
+            owner: Some(Addr::unchecked(DAO_ADDR)),
+            manager: Some(Addr::unchecked(ADDR1)),
+            unstaking_duration: Some(Duration::Height(5)),
+            denom: DENOM.to_string(),
+        }
+    )
+}
+
+#[test]
+fn test_voting_power_queries() {
+    let mut app = mock_app();
+    let staking_id = app.store_code(staking_contract());
+    let addr = instantiate_staking(
+        &mut app,
+        staking_id,
+        InstantiateMsg {
+            owner: Some(DAO_ADDR.to_string()),
+            manager: Some(ADDR1.to_string()),
+            denom: DENOM.to_string(),
+            unstaking_duration: Some(Duration::Height(5)),
+        },
+    );
+
+    // Total power is 0
+    let resp = get_total_power_at_height(&mut app, addr.clone(), None);
+    assert!(resp.power.is_zero());
+
+    // ADDR1 has no power, none staked
+    let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None);
+    assert!(resp.power.is_zero());
+
+    // ADDR1 stakes
+    stake_tokens(&mut app, addr.clone(), ADDR1, 100, DENOM).unwrap();
+    app.update_block(next_block);
+
+    // Total power is 100
+    let resp = get_total_power_at_height(&mut app, addr.clone(), None);
+    assert_eq!(resp.power, Uint128::new(100));
+
+    // ADDR1 has 100 power
+    let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None);
+    assert_eq!(resp.power, Uint128::new(100));
+
+    // ADDR2 still has 0 power
+    let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), None);
+    assert!(resp.power.is_zero());
+
+    // ADDR2 stakes
+    stake_tokens(&mut app, addr.clone(), ADDR2, 50, DENOM).unwrap();
+    app.update_block(next_block);
+    let prev_height = app.block_info().height - 1;
+
+    // Query the previous height, total 100, ADDR1 100, ADDR2 0
+    // Total power is 100
+    let resp = get_total_power_at_height(&mut app, addr.clone(), Some(prev_height));
+    assert_eq!(resp.power, Uint128::new(100));
+
+    // ADDR1 has 100 power
+    let resp =
+        get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), Some(prev_height));
+    assert_eq!(resp.power, Uint128::new(100));
+
+    // ADDR2 still has 0 power
+    let resp =
+        get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), Some(prev_height));
+    assert!(resp.power.is_zero());
+
+    // For current height, total 150, ADDR1 100, ADDR2 50
+    // Total power is 150
+    let resp = get_total_power_at_height(&mut app, addr.clone(), None);
+    assert_eq!(resp.power, Uint128::new(150));
+
+    // ADDR1 has 100 power
+    let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None);
+    assert_eq!(resp.power, Uint128::new(100));
+
+    // ADDR2 now has 50 power
+    let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), None);
+    assert_eq!(resp.power, Uint128::new(50));
+
+    // ADDR1 unstakes half
+    unstake_tokens(&mut app, addr.clone(), ADDR1, 50).unwrap();
+    app.update_block(next_block);
+    let prev_height = app.block_info().height - 1;
+
+    // Query the previous height, total 150, ADDR1 100, ADDR2 50
+    // Total power is 100
+    let resp = get_total_power_at_height(&mut app, addr.clone(), Some(prev_height));
+    assert_eq!(resp.power, Uint128::new(150));
+
+    // ADDR1 has 100 power
+    let resp =
+        get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), Some(prev_height));
+    assert_eq!(resp.power, Uint128::new(100));
+
+    // ADDR2 still has 0 power
+    let resp =
+        get_voting_power_at_height(&mut app, addr.clone(), ADDR2.to_string(), Some(prev_height));
+    assert_eq!(resp.power, Uint128::new(50));
+
+    // For current height, total 100, ADDR1 50, ADDR2 50
+    // Total power is 100
+    let resp = get_total_power_at_height(&mut app, addr.clone(), None);
+    assert_eq!(resp.power, Uint128::new(100));
+
+    // ADDR1 has 50 power
+    let resp = get_voting_power_at_height(&mut app, addr.clone(), ADDR1.to_string(), None);
+    assert_eq!(resp.power, Uint128::new(50));
+
+    // ADDR2 now has 50 power
+    let resp = get_voting_power_at_height(&mut app, addr, ADDR2.to_string(), None);
+    assert_eq!(resp.power, Uint128::new(50));
+}
diff --git a/contracts/cw20-stake/src/tests.rs b/contracts/cw20-stake/src/tests.rs
new file mode 100644
index 00000000..9ea61ca8
--- /dev/null
+++ b/contracts/cw20-stake/src/tests.rs
@@ -0,0 +1,966 @@
+use std::borrow::BorrowMut;
+
+use crate::msg::{
+    ExecuteMsg, QueryMsg, ReceiveMsg, StakedBalanceAtHeightResponse, StakedValueResponse,
+    TotalStakedAtHeightResponse, TotalValueResponse,
+};
+use crate::state::{Config, MAX_CLAIMS};
+use crate::ContractError;
+use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
+use cosmwasm_std::{to_binary, Addr, Empty, MessageInfo, Uint128};
+use cw20::Cw20Coin;
+use cw_utils::Duration;
+
+use cw_multi_test::{next_block, App, AppResponse, Contract, ContractWrapper, Executor};
+
+use anyhow::Result as AnyResult;
+
+use cw_controllers::{Claim, ClaimsResponse};
+use cw_utils::Expiration::AtHeight;
+
+const ADDR1: &str = "addr0001";
+const ADDR2: &str = "addr0002";
+const ADDR3: &str = "addr0003";
+const ADDR4: &str = "addr0004";
+
+pub fn contract_staking() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        crate::contract::execute,
+        crate::contract::instantiate,
+        crate::contract::query,
+    );
+    Box::new(contract)
+}
+
+pub fn contract_cw20() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw20_base::contract::execute,
+        cw20_base::contract::instantiate,
+        cw20_base::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn mock_app() -> App {
+    App::default()
+}
+
+fn get_balance<T: Into<String>, U: Into<String>>(
+    app: &App,
+    contract_addr: T,
+    address: U,
+) -> Uint128 {
+    let msg = cw20::Cw20QueryMsg::Balance {
+        address: address.into(),
+    };
+    let result: cw20::BalanceResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
+    result.balance
+}
+
+fn instantiate_cw20(app: &mut App, initial_balances: Vec<Cw20Coin>) -> Addr {
+    let cw20_id = app.store_code(contract_cw20());
+    let msg = cw20_base::msg::InstantiateMsg {
+        name: String::from("Test"),
+        symbol: String::from("TEST"),
+        decimals: 6,
+        initial_balances,
+        mint: None,
+        marketing: None,
+    };
+
+    app.instantiate_contract(cw20_id, Addr::unchecked(ADDR1), &msg, &[], "cw20", None)
+        .unwrap()
+}
+
+fn instantiate_staking(app: &mut App, cw20: Addr, unstaking_duration: Option<Duration>) -> Addr {
+    let staking_code_id = app.store_code(contract_staking());
+    let msg = crate::msg::InstantiateMsg {
+        owner: Some("owner".to_string()),
+        manager: Some("manager".to_string()),
+        token_address: cw20.to_string(),
+        unstaking_duration,
+    };
+    app.instantiate_contract(
+        staking_code_id,
+        Addr::unchecked(ADDR1),
+        &msg,
+        &[],
+        "staking",
+        None,
+    )
+    .unwrap()
+}
+
+fn setup_test_case(
+    app: &mut App,
+    initial_balances: Vec<Cw20Coin>,
+    unstaking_duration: Option<Duration>,
+) -> (Addr, Addr) {
+    // Instantiate cw20 contract
+    let cw20_addr = instantiate_cw20(app, initial_balances);
+    app.update_block(next_block);
+    // Instantiate staking contract
+    let staking_addr = instantiate_staking(app, cw20_addr.clone(), unstaking_duration);
+    app.update_block(next_block);
+    (staking_addr, cw20_addr)
+}
+
+fn query_staked_balance<T: Into<String>, U: Into<String>>(
+    app: &App,
+    contract_addr: T,
+    address: U,
+) -> Uint128 {
+    let msg = QueryMsg::StakedBalanceAtHeight {
+        address: address.into(),
+        height: None,
+    };
+    let result: StakedBalanceAtHeightResponse =
+        app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
+    result.balance
+}
+
+fn query_config<T: Into<String>>(app: &App, contract_addr: T) -> Config {
+    let msg = QueryMsg::GetConfig {};
+    app.wrap().query_wasm_smart(contract_addr, &msg).unwrap()
+}
+
+fn query_total_staked<T: Into<String>>(app: &App, contract_addr: T) -> Uint128 {
+    let msg = QueryMsg::TotalStakedAtHeight { height: None };
+    let result: TotalStakedAtHeightResponse =
+        app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
+    result.total
+}
+
+fn query_staked_value<T: Into<String>, U: Into<String>>(
+    app: &App,
+    contract_addr: T,
+    address: U,
+) -> Uint128 {
+    let msg = QueryMsg::StakedValue {
+        address: address.into(),
+    };
+    let result: StakedValueResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
+    result.value
+}
+
+fn query_total_value<T: Into<String>>(app: &App, contract_addr: T) -> Uint128 {
+    let msg = QueryMsg::TotalValue {};
+    let result: TotalValueResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
+    result.total
+}
+
+fn query_claims<T: Into<String>, U: Into<String>>(
+    app: &App,
+    contract_addr: T,
+    address: U,
+) -> Vec<Claim> {
+    let msg = QueryMsg::Claims {
+        address: address.into(),
+    };
+    let result: ClaimsResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
+    result.claims
+}
+
+fn stake_tokens(
+    app: &mut App,
+    staking_addr: &Addr,
+    cw20_addr: &Addr,
+    info: MessageInfo,
+    amount: Uint128,
+) -> AnyResult<AppResponse> {
+    let msg = cw20::Cw20ExecuteMsg::Send {
+        contract: staking_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::Stake {}).unwrap(),
+    };
+    app.execute_contract(info.sender, cw20_addr.clone(), &msg, &[])
+}
+
+fn update_config(
+    app: &mut App,
+    staking_addr: &Addr,
+    info: MessageInfo,
+    owner: Option<Addr>,
+    manager: Option<Addr>,
+    duration: Option<Duration>,
+) -> AnyResult<AppResponse> {
+    let msg = ExecuteMsg::UpdateConfig {
+        owner: owner.map(|a| a.to_string()),
+        manager: manager.map(|a| a.to_string()),
+        duration,
+    };
+    app.execute_contract(info.sender, staking_addr.clone(), &msg, &[])
+}
+
+fn unstake_tokens(
+    app: &mut App,
+    staking_addr: &Addr,
+    info: MessageInfo,
+    amount: Uint128,
+) -> AnyResult<AppResponse> {
+    let msg = ExecuteMsg::Unstake { amount };
+    app.execute_contract(info.sender, staking_addr.clone(), &msg, &[])
+}
+
+fn claim_tokens(app: &mut App, staking_addr: &Addr, info: MessageInfo) -> AnyResult<AppResponse> {
+    let msg = ExecuteMsg::Claim {};
+    app.execute_contract(info.sender, staking_addr.clone(), &msg, &[])
+}
+
+#[test]
+#[should_panic(expected = "Invalid unstaking duration, unstaking duration cannot be 0")]
+fn test_instantiate_invalid_unstaking_duration() {
+    let mut app = mock_app();
+    let amount1 = Uint128::from(100u128);
+    let _token_address = Addr::unchecked("token_address");
+    let initial_balances = vec![Cw20Coin {
+        address: ADDR1.to_string(),
+        amount: amount1,
+    }];
+    let (_staking_addr, _cw20_addr) =
+        setup_test_case(&mut app, initial_balances, Some(Duration::Height(0)));
+}
+
+#[test]
+fn test_update_config() {
+    let _deps = mock_dependencies();
+
+    let mut app = mock_app();
+    let amount1 = Uint128::from(100u128);
+    let _token_address = Addr::unchecked("token_address");
+    let initial_balances = vec![Cw20Coin {
+        address: ADDR1.to_string(),
+        amount: amount1,
+    }];
+    let (staking_addr, _cw20_addr) = setup_test_case(&mut app, initial_balances, None);
+
+    let info = mock_info("owner", &[]);
+    let _env = mock_env();
+    // Test update admin
+    update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        Some(Addr::unchecked("owner2")),
+        None,
+        Some(Duration::Height(100)),
+    )
+    .unwrap();
+
+    let config = query_config(&app, &staking_addr);
+    assert_eq!(config.owner, Some(Addr::unchecked("owner2")));
+    assert_eq!(config.unstaking_duration, Some(Duration::Height(100)));
+
+    // Try updating owner with original owner, which is now invalid
+    let info = mock_info("owner", &[]);
+    let _err = update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        Some(Addr::unchecked("owner3")),
+        None,
+        Some(Duration::Height(100)),
+    )
+    .unwrap_err();
+
+    // Add manager
+    let info = mock_info("owner2", &[]);
+    let _env = mock_env();
+    update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        Some(Addr::unchecked("owner2")),
+        Some(Addr::unchecked("manager")),
+        Some(Duration::Height(100)),
+    )
+    .unwrap();
+
+    let config = query_config(&app, &staking_addr);
+    assert_eq!(config.owner, Some(Addr::unchecked("owner2")));
+    assert_eq!(config.manager, Some(Addr::unchecked("manager")));
+
+    // Manager can update unstaking duration
+    let info = mock_info("manager", &[]);
+    let _env = mock_env();
+    update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        Some(Addr::unchecked("owner2")),
+        Some(Addr::unchecked("manager")),
+        Some(Duration::Height(50)),
+    )
+    .unwrap();
+    let config = query_config(&app, &staking_addr);
+    assert_eq!(config.owner, Some(Addr::unchecked("owner2")));
+    assert_eq!(config.unstaking_duration, Some(Duration::Height(50)));
+
+    // Manager cannot update owner
+    let info = mock_info("manager", &[]);
+    let _env = mock_env();
+    update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        Some(Addr::unchecked("manager")),
+        Some(Addr::unchecked("manager")),
+        Some(Duration::Height(50)),
+    )
+    .unwrap_err();
+
+    // Manager can update manager
+    let info = mock_info("owner2", &[]);
+    let _env = mock_env();
+    update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        Some(Addr::unchecked("owner2")),
+        None,
+        Some(Duration::Height(50)),
+    )
+    .unwrap();
+
+    let config = query_config(&app, &staking_addr);
+    assert_eq!(config.owner, Some(Addr::unchecked("owner2")));
+    assert_eq!(config.manager, None);
+
+    // Invalid duration
+    let info = mock_info("owner2", &[]);
+    let _env = mock_env();
+    let err: ContractError = update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        Some(Addr::unchecked("owner2")),
+        None,
+        Some(Duration::Height(0)),
+    )
+    .unwrap_err()
+    .downcast()
+    .unwrap();
+    assert_eq!(err, ContractError::InvalidUnstakingDuration {});
+
+    // Remove owner
+    let info = mock_info("owner2", &[]);
+    let _env = mock_env();
+    update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        None,
+        None,
+        Some(Duration::Height(100)),
+    )
+    .unwrap();
+
+    // Assert no further updates can be made
+    let info = mock_info("owner2", &[]);
+    let _env = mock_env();
+    let err: ContractError = update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        None,
+        None,
+        Some(Duration::Height(100)),
+    )
+    .unwrap_err()
+    .downcast()
+    .unwrap();
+    assert_eq!(err, ContractError::Unauthorized {});
+
+    let info = mock_info("manager", &[]);
+    let _env = mock_env();
+    let err: ContractError = update_config(
+        &mut app,
+        &staking_addr,
+        info,
+        None,
+        None,
+        Some(Duration::Height(100)),
+    )
+    .unwrap_err()
+    .downcast()
+    .unwrap();
+    assert_eq!(err, ContractError::Unauthorized {})
+}
+
+#[test]
+fn test_staking() {
+    let _deps = mock_dependencies();
+
+    let mut app = mock_app();
+    let amount1 = Uint128::from(100u128);
+    let _token_address = Addr::unchecked("token_address");
+    let initial_balances = vec![Cw20Coin {
+        address: ADDR1.to_string(),
+        amount: amount1,
+    }];
+    let (staking_addr, cw20_addr) = setup_test_case(&mut app, initial_balances, None);
+
+    let info = mock_info(ADDR1, &[]);
+    let _env = mock_env();
+
+    // Successful bond
+    let amount = Uint128::new(50);
+    stake_tokens(&mut app, &staking_addr, &cw20_addr, info.clone(), amount).unwrap();
+
+    // Very important that this balances is not reflected until
+    // the next block. This protects us from flash loan hostile
+    // takeovers.
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::zero()
+    );
+
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(50u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(50u128)
+    );
+    assert_eq!(
+        get_balance(&app, &cw20_addr, ADDR1.to_string()),
+        Uint128::from(50u128)
+    );
+
+    // Can't transfer bonded amount
+    let msg = cw20::Cw20ExecuteMsg::Transfer {
+        recipient: ADDR2.to_string(),
+        amount: Uint128::from(51u128),
+    };
+    let _err = app
+        .borrow_mut()
+        .execute_contract(info.sender.clone(), cw20_addr.clone(), &msg, &[])
+        .unwrap_err();
+
+    // Sucessful transfer of unbonded amount
+    let msg = cw20::Cw20ExecuteMsg::Transfer {
+        recipient: ADDR2.to_string(),
+        amount: Uint128::from(20u128),
+    };
+    let _res = app
+        .borrow_mut()
+        .execute_contract(info.sender, cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(30u128));
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(20u128));
+
+    // Addr 2 successful bond
+    let info = mock_info(ADDR2, &[]);
+    stake_tokens(&mut app, &staking_addr, &cw20_addr, info, Uint128::new(20)).unwrap();
+
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR2),
+        Uint128::from(20u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(70u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::zero());
+
+    // Can't unstake more than you have staked
+    let info = mock_info(ADDR2, &[]);
+    let _err = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(100)).unwrap_err();
+
+    // Successful unstake
+    let info = mock_info(ADDR2, &[]);
+    let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(10)).unwrap();
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR2),
+        Uint128::from(10u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(60u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(10u128));
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1),
+        Uint128::from(50u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(30u128));
+}
+
+#[test]
+fn text_max_claims() {
+    let mut app = mock_app();
+    let amount1 = Uint128::from(MAX_CLAIMS + 1);
+    let unstaking_blocks = 1u64;
+    let _token_address = Addr::unchecked("token_address");
+    let initial_balances = vec![Cw20Coin {
+        address: ADDR1.to_string(),
+        amount: amount1,
+    }];
+    let (staking_addr, cw20_addr) = setup_test_case(
+        &mut app,
+        initial_balances,
+        Some(Duration::Height(unstaking_blocks)),
+    );
+
+    let info = mock_info(ADDR1, &[]);
+    stake_tokens(&mut app, &staking_addr, &cw20_addr, info.clone(), amount1).unwrap();
+
+    // Create the max number of claims
+    for _ in 0..MAX_CLAIMS {
+        unstake_tokens(&mut app, &staking_addr, info.clone(), Uint128::new(1)).unwrap();
+    }
+
+    // Additional unstaking attempts ought to fail.
+    unstake_tokens(&mut app, &staking_addr, info.clone(), Uint128::new(1)).unwrap_err();
+
+    // Clear out the claims list.
+    app.update_block(next_block);
+    claim_tokens(&mut app, &staking_addr, info.clone()).unwrap();
+
+    // Unstaking now allowed again.
+    unstake_tokens(&mut app, &staking_addr, info.clone(), Uint128::new(1)).unwrap();
+    app.update_block(next_block);
+    claim_tokens(&mut app, &staking_addr, info).unwrap();
+
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), amount1);
+}
+
+#[test]
+fn test_unstaking_with_claims() {
+    let _deps = mock_dependencies();
+
+    let mut app = mock_app();
+    let amount1 = Uint128::from(100u128);
+    let unstaking_blocks = 10u64;
+    let _token_address = Addr::unchecked("token_address");
+    let initial_balances = vec![Cw20Coin {
+        address: ADDR1.to_string(),
+        amount: amount1,
+    }];
+    let (staking_addr, cw20_addr) = setup_test_case(
+        &mut app,
+        initial_balances,
+        Some(Duration::Height(unstaking_blocks)),
+    );
+
+    let info = mock_info(ADDR1, &[]);
+
+    // Successful bond
+    let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, Uint128::new(50)).unwrap();
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1),
+        Uint128::from(50u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(50u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(50u128));
+
+    // Unstake
+    let info = mock_info(ADDR1, &[]);
+    let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(10)).unwrap();
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1),
+        Uint128::from(40u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(40u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(50u128));
+
+    // Cannot claim when nothing is available
+    let info = mock_info(ADDR1, &[]);
+    let _err: ContractError = claim_tokens(&mut app, &staking_addr, info)
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(_err, ContractError::NothingToClaim {});
+
+    // Successful claim
+    app.update_block(|b| b.height += unstaking_blocks);
+    let info = mock_info(ADDR1, &[]);
+    let _res = claim_tokens(&mut app, &staking_addr, info).unwrap();
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1),
+        Uint128::from(40u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(40u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(60u128));
+
+    // Unstake and claim multiple
+    let _info = mock_info(ADDR1, &[]);
+    let info = mock_info(ADDR1, &[]);
+    let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(5)).unwrap();
+    app.update_block(next_block);
+
+    let _info = mock_info(ADDR1, &[]);
+    let info = mock_info(ADDR1, &[]);
+    let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(5)).unwrap();
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1),
+        Uint128::from(30u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(30u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(60u128));
+
+    app.update_block(|b| b.height += unstaking_blocks);
+    let info = mock_info(ADDR1, &[]);
+    let _res = claim_tokens(&mut app, &staking_addr, info).unwrap();
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1),
+        Uint128::from(30u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(30u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(70u128));
+}
+
+#[test]
+fn multiple_address_staking() {
+    let amount1 = Uint128::from(100u128);
+    let initial_balances = vec![
+        Cw20Coin {
+            address: ADDR1.to_string(),
+            amount: amount1,
+        },
+        Cw20Coin {
+            address: ADDR2.to_string(),
+            amount: amount1,
+        },
+        Cw20Coin {
+            address: ADDR3.to_string(),
+            amount: amount1,
+        },
+        Cw20Coin {
+            address: ADDR4.to_string(),
+            amount: amount1,
+        },
+    ];
+    let mut app = mock_app();
+    let amount1 = Uint128::from(100u128);
+    let unstaking_blocks = 10u64;
+    let _token_address = Addr::unchecked("token_address");
+    let (staking_addr, cw20_addr) = setup_test_case(
+        &mut app,
+        initial_balances,
+        Some(Duration::Height(unstaking_blocks)),
+    );
+
+    let info = mock_info(ADDR1, &[]);
+    // Successful bond
+    let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount1).unwrap();
+    app.update_block(next_block);
+
+    let info = mock_info(ADDR2, &[]);
+    // Successful bond
+    let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount1).unwrap();
+    app.update_block(next_block);
+
+    let info = mock_info(ADDR3, &[]);
+    // Successful bond
+    let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount1).unwrap();
+    app.update_block(next_block);
+
+    let info = mock_info(ADDR4, &[]);
+    // Successful bond
+    let _res = stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount1).unwrap();
+    app.update_block(next_block);
+
+    assert_eq!(query_staked_balance(&app, &staking_addr, ADDR1), amount1);
+    assert_eq!(query_staked_balance(&app, &staking_addr, ADDR2), amount1);
+    assert_eq!(query_staked_balance(&app, &staking_addr, ADDR3), amount1);
+    assert_eq!(query_staked_balance(&app, &staking_addr, ADDR4), amount1);
+
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        amount1.checked_mul(Uint128::new(4)).unwrap()
+    );
+
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::zero());
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::zero());
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR3), Uint128::zero());
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR4), Uint128::zero());
+}
+
+#[test]
+fn test_auto_compounding_staking() {
+    let _deps = mock_dependencies();
+
+    let mut app = mock_app();
+    let amount1 = Uint128::from(1000u128);
+    let _token_address = Addr::unchecked("token_address");
+    let initial_balances = vec![Cw20Coin {
+        address: ADDR1.to_string(),
+        amount: amount1,
+    }];
+    let (staking_addr, cw20_addr) = setup_test_case(&mut app, initial_balances, None);
+
+    let info = mock_info(ADDR1, &[]);
+    let _env = mock_env();
+
+    // Successful bond
+    let amount = Uint128::new(100);
+    stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount).unwrap();
+    app.update_block(next_block);
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        query_staked_value(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        query_total_value(&app, &staking_addr),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        get_balance(&app, &cw20_addr, ADDR1.to_string()),
+        Uint128::from(900u128)
+    );
+
+    // Add compounding rewards
+    let msg = cw20::Cw20ExecuteMsg::Send {
+        contract: staking_addr.to_string(),
+        amount: Uint128::from(100u128),
+        msg: to_binary(&ReceiveMsg::Fund {}).unwrap(),
+    };
+    let _res = app
+        .borrow_mut()
+        .execute_contract(Addr::unchecked(ADDR1), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        query_staked_value(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(200u128)
+    );
+    assert_eq!(
+        query_total_value(&app, &staking_addr),
+        Uint128::from(200u128)
+    );
+    assert_eq!(
+        get_balance(&app, &cw20_addr, ADDR1.to_string()),
+        Uint128::from(800u128)
+    );
+
+    // Sucessful transfer of unbonded amount
+    let msg = cw20::Cw20ExecuteMsg::Transfer {
+        recipient: ADDR2.to_string(),
+        amount: Uint128::from(100u128),
+    };
+    let _res = app
+        .borrow_mut()
+        .execute_contract(Addr::unchecked(ADDR1), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(700u128));
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(100u128));
+
+    // Addr 2 successful bond
+    let info = mock_info(ADDR2, &[]);
+    stake_tokens(&mut app, &staking_addr, &cw20_addr, info, Uint128::new(100)).unwrap();
+
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR2),
+        Uint128::from(50u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(150u128)
+    );
+    assert_eq!(
+        query_staked_value(&app, &staking_addr, ADDR2.to_string()),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        query_total_value(&app, &staking_addr),
+        Uint128::from(300u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::zero());
+
+    // Can't unstake more than you have staked
+    let info = mock_info(ADDR2, &[]);
+    let _err = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(51)).unwrap_err();
+
+    // Add compounding rewards
+    let msg = cw20::Cw20ExecuteMsg::Send {
+        contract: staking_addr.to_string(),
+        amount: Uint128::from(90u128),
+        msg: to_binary(&ReceiveMsg::Fund {}).unwrap(),
+    };
+    let _res = app
+        .borrow_mut()
+        .execute_contract(Addr::unchecked(ADDR1), cw20_addr.clone(), &msg, &[])
+        .unwrap();
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR2),
+        Uint128::from(50u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(150u128)
+    );
+    assert_eq!(
+        query_staked_value(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(260u128)
+    );
+    assert_eq!(
+        query_staked_value(&app, &staking_addr, ADDR2.to_string()),
+        Uint128::from(130u128)
+    );
+    assert_eq!(
+        query_total_value(&app, &staking_addr),
+        Uint128::from(390u128)
+    );
+    assert_eq!(
+        get_balance(&app, &cw20_addr, ADDR1.to_string()),
+        Uint128::from(610u128)
+    );
+
+    // Successful unstake
+    let info = mock_info(ADDR2, &[]);
+    let _res = unstake_tokens(&mut app, &staking_addr, info, Uint128::new(25)).unwrap();
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR2),
+        Uint128::from(25u128)
+    );
+    assert_eq!(
+        query_total_staked(&app, &staking_addr),
+        Uint128::from(125u128)
+    );
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(65u128));
+}
+
+#[test]
+fn test_simple_unstaking_with_duration() {
+    let _deps = mock_dependencies();
+
+    let mut app = mock_app();
+    let amount1 = Uint128::from(100u128);
+    let _token_address = Addr::unchecked("token_address");
+    let initial_balances = vec![
+        Cw20Coin {
+            address: ADDR1.to_string(),
+            amount: amount1,
+        },
+        Cw20Coin {
+            address: ADDR2.to_string(),
+            amount: amount1,
+        },
+    ];
+    let (staking_addr, cw20_addr) =
+        setup_test_case(&mut app, initial_balances, Some(Duration::Height(1)));
+
+    // Bond Address 1
+    let info = mock_info(ADDR1, &[]);
+    let _env = mock_env();
+    let amount = Uint128::new(100);
+    stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount).unwrap();
+
+    // Bond Address 2
+    let info = mock_info(ADDR2, &[]);
+    let _env = mock_env();
+    let amount = Uint128::new(100);
+    stake_tokens(&mut app, &staking_addr, &cw20_addr, info, amount).unwrap();
+    app.update_block(next_block);
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(100u128)
+    );
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(100u128)
+    );
+
+    // Unstake Addr1
+    let info = mock_info(ADDR1, &[]);
+    let _env = mock_env();
+    let amount = Uint128::new(100);
+    unstake_tokens(&mut app, &staking_addr, info, amount).unwrap();
+
+    // Unstake Addr2
+    let info = mock_info(ADDR2, &[]);
+    let _env = mock_env();
+    let amount = Uint128::new(100);
+    unstake_tokens(&mut app, &staking_addr, info, amount).unwrap();
+
+    app.update_block(next_block);
+
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR1.to_string()),
+        Uint128::from(0u128)
+    );
+    assert_eq!(
+        query_staked_balance(&app, &staking_addr, ADDR2.to_string()),
+        Uint128::from(0u128)
+    );
+
+    // Claim
+    assert_eq!(
+        query_claims(&app, &staking_addr, ADDR1),
+        vec![Claim {
+            amount: Uint128::new(100),
+            release_at: AtHeight(12349)
+        }]
+    );
+    assert_eq!(
+        query_claims(&app, &staking_addr, ADDR2),
+        vec![Claim {
+            amount: Uint128::new(100),
+            release_at: AtHeight(12349)
+        }]
+    );
+
+    let info = mock_info(ADDR1, &[]);
+    claim_tokens(&mut app, &staking_addr, info).unwrap();
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR1), Uint128::from(100u128));
+
+    let info = mock_info(ADDR2, &[]);
+    claim_tokens(&mut app, &staking_addr, info).unwrap();
+    assert_eq!(get_balance(&app, &cw20_addr, ADDR2), Uint128::from(100u128));
+}