Repository: DA0-DA0/dao-contracts
Pull request #193 (2022-03-14 21:42:26)

Contracts v1 - single choice voting module.
This is an implementation of single choice voting for the v1
contracts. It is largely based on the existing single choice voting
implementation and thus inherits some of its issues (more discussion
of this below).

This implementation also fixes an overflow bug and an issue with
rejected proposals not being closable.

### Inherited issues

#### Vote ordering matters.

Our present voting system (and the ones used by the cw-plus voting
contracts) determine if a vote passes or fails by which option (yes or
no) reaches the threshold first. For example, with a 50% passing
threshold and 100 tokens there are two possible outcomes for the same
set of votes depending on ordering:

1. Wallet A votes no first with 50 tokens - proposal fails.
2. Wallet B votes yes first with 50 tokens - proposal passes.

This also has interesting effects when dealing with `ThresholdQuorum`
passing thresholds. These behaviors are currenly demonstrated in the
`test_close_cotes_quorum` and `test_close_votes` tests.

I am unsure what the appropriate solution to this is, but think we
should put our heads together a little.

#### Early proposal expiration reduces hostile takeover cost.

Proposals currently have a `latest` field which the proposer may
set. This field overrules the governance module's default max voting
period if it is smaller than the default. The result of this is that
in governance systems with a `ThresholdQuorum` the cost of a hostile
takeover becomes `quorum * total_supply * token_price` as a hostile
actor may do the following:

1. Purchase `quorum * total_supply` tokens.
2. Create a proposal that expires the block after it is created.
3. Vote yes on that proposal in the same block that it is created.

To mitigate this I think we should remove the `latest` field from
proposals.



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2a2f6d24..6f904d2b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -221,6 +221,30 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "cw-govmod-single"
+version = "0.1.0"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cosmwasm-storage",
+ "cw-governance",
+ "cw-governance-interface",
+ "cw-governance-macros",
+ "cw-multi-test 0.12.1",
+ "cw-storage-plus 0.12.1",
+ "cw-utils 0.12.1",
+ "cw2 0.12.1",
+ "cw20 0.12.1",
+ "cw20-balance-voting",
+ "cw20-base 0.12.1",
+ "cw3 0.12.1",
+ "rand",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "cw-govmod-sudo"
 version = "0.1.0"
@@ -435,6 +459,18 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "cw3"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "017b6263414c58081cea97626f4d8da2dc8f7267337bba0eb5770260d26251e0"
+dependencies = [
+ "cosmwasm-std",
+ "cw-utils 0.12.1",
+ "schemars",
+ "serde",
+]
+
 [[package]]
 name = "cw3-dao"
 version = "0.2.6"
@@ -447,7 +483,7 @@ dependencies = [
  "cw2 0.11.1",
  "cw20 0.11.1",
  "cw20-base 0.11.1",
- "cw3",
+ "cw3 0.11.1",
  "schemars",
  "serde",
  "stake-cw20",
@@ -466,7 +502,7 @@ dependencies = [
  "cw2 0.11.1",
  "cw20 0.11.1",
  "cw20-base 0.11.1",
- "cw3",
+ "cw3 0.11.1",
  "cw4",
  "cw4-group",
  "schemars",
@@ -724,6 +760,12 @@ dependencies = [
  "spki",
 ]
 
+[[package]]
+name = "ppv-lite86"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.36"
@@ -765,6 +807,27 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.3",
+]
+
 [[package]]
 name = "rand_core"
 version = "0.5.1"
diff --git a/contracts/cw-governance-interface/src/voting.rs b/contracts/cw-governance-interface/src/voting.rs
index 27cbdd87..720b564b 100644
--- a/contracts/cw-governance-interface/src/voting.rs
+++ b/contracts/cw-governance-interface/src/voting.rs
@@ -1,9 +1,10 @@
 use cosmwasm_std::Uint128;
 use cw2::ContractVersion;
-use cw_governance_macros::voting_query;
+use cw_governance_macros::{token_query, voting_query};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
+#[token_query]
 #[voting_query]
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
diff --git a/contracts/cw-governance-macros/src/lib.rs b/contracts/cw-governance-macros/src/lib.rs
index e5301ebe..0602a452 100644
--- a/contracts/cw-governance-macros/src/lib.rs
+++ b/contracts/cw-governance-macros/src/lib.rs
@@ -92,6 +92,76 @@ pub fn voting_query(metadata: TokenStream, input: TokenStream) -> TokenStream {
     .into()
 }
 
+/// Adds the nesecary fields to an enum such that it implements the
+/// interface needed to be a cw-governance voting module with a token.
+///
+/// For example:
+///
+/// ```
+/// use cw_governance_macros::token_query;
+///
+/// #[token_query]
+/// enum QueryMsg {}
+/// ```
+///
+/// Will transform the enum to:
+///
+/// ```
+/// enum QueryMsg {
+///     TokenContract {},
+/// }
+/// ```
+///
+/// Note that other derive macro invocations must occur after this
+/// procedural macro as they may depend on the new fields. For
+/// example, the following will fail becase the `Clone` derivation
+/// occurs before the addition of the field.
+///
+/// ```compile_fail
+/// use cw_governance_macros::govmod_query;
+///
+/// #[derive(Clone)]
+/// #[token_query]
+/// #[allow(dead_code)]
+/// enum Test {
+///     Foo,
+///     Bar(u64),
+///     Baz { foo: u64 },
+/// }
+/// ```
+#[proc_macro_attribute]
+pub fn token_query(metadata: TokenStream, input: TokenStream) -> TokenStream {
+    // Make sure that no arguments were passed in.
+    let args = parse_macro_input!(metadata as AttributeArgs);
+    if let Some(first_arg) = args.first() {
+        return syn::Error::new_spanned(first_arg, "token query macro takes no arguments")
+            .to_compile_error()
+            .into();
+    }
+
+    let mut ast: DeriveInput = parse_macro_input!(input);
+    match &mut ast.data {
+        syn::Data::Enum(DataEnum { variants, .. }) => {
+            let info: Variant = syn::parse2(quote! { TokenContract {} }).unwrap();
+
+            variants.push(info);
+        }
+        _ => {
+            return syn::Error::new(
+                ast.ident.span(),
+                "token query types can not be only be derived for enums",
+            )
+            .to_compile_error()
+            .into()
+        }
+    };
+
+    quote! {
+    #ast
+    }
+    .into()
+}
+
 /// Adds the nesecary fields to an enum such that it implements the
 /// interface needed to be a cw-governance governance module.
 ///
diff --git a/contracts/cw-governance/src/contract.rs b/contracts/cw-governance/src/contract.rs
index 6945ee57..16bd3f73 100644
--- a/contracts/cw-governance/src/contract.rs
+++ b/contracts/cw-governance/src/contract.rs
@@ -1,5 +1,3 @@
-use std::usize;
-
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
 use cosmwasm_std::{
diff --git a/contracts/cw-govmod-single/Cargo.lock b/contracts/cw-govmod-single/Cargo.lock
new file mode 100644
index 00000000..c9737778
--- /dev/null
+++ b/contracts/cw-govmod-single/Cargo.lock
@@ -0,0 +1,596 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "byteorder"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "const-oid"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22"
+
+[[package]]
+name = "cosmwasm-crypto"
+version = "1.0.0-beta4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f903ebbabc0d4880dbc76148efb8be8fc29fa4bf294c440c3d70da1c8bcafff7"
+dependencies = [
+ "digest",
+ "ed25519-zebra",
+ "k256",
+ "rand_core 0.5.1",
+ "thiserror",
+]
+
+[[package]]
+name = "cosmwasm-derive"
+version = "1.0.0-beta4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "832bebef577ecb394603de8e2bf0de429b74aa364e17dec18e15ce37e71b0cae"
+dependencies = [
+ "syn",
+]
+
+[[package]]
+name = "cosmwasm-schema"
+version = "1.0.0-beta"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e4b3f6933f94acdd3ddb931af4870c2002e3331a4a8b247a4ef070dd31ccb0"
+dependencies = [
+ "schemars",
+ "serde_json",
+]
+
+[[package]]
+name = "cosmwasm-std"
+version = "1.0.0-beta4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6238c45840cc9de5a39f0f619e3a4f7c38c5d2c6ac9e3e4d72751ee045e6d7da"
+dependencies = [
+ "base64",
+ "cosmwasm-crypto",
+ "cosmwasm-derive",
+ "schemars",
+ "serde",
+ "serde-json-wasm",
+ "thiserror",
+ "uint",
+]
+
+[[package]]
+name = "cosmwasm-storage"
+version = "1.0.0-beta"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "665cf97ad42be46936f6e6739711824a9bf21c440a6c98e185a3404aef0c3b81"
+dependencies = [
+ "cosmwasm-std",
+ "serde",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4"
+dependencies = [
+ "generic-array",
+ "rand_core 0.6.3",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-mac"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
+dependencies = [
+ "generic-array",
+ "subtle",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
+dependencies = [
+ "byteorder",
+ "digest",
+ "rand_core 0.5.1",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "cw-storage-plus"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261"
+dependencies = [
+ "cosmwasm-std",
+ "schemars",
+ "serde",
+]
+
+[[package]]
+name = "cw2"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1d81d7c359d6c1fba3aa83dad7ec6f999e512571380ae62f81257c3db569743"
+dependencies = [
+ "cosmwasm-std",
+ "cw-storage-plus",
+ "schemars",
+ "serde",
+]
+
+[[package]]
+name = "der"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e21d2d0f22cde6e88694108429775c0219760a07779bf96503b434a03d7412"
+dependencies = [
+ "const-oid",
+]
+
+[[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
+
+[[package]]
+name = "ecdsa"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "713c32426287891008edb98f8b5c6abb2130aa043c93a818728fcda78606f274"
+dependencies = [
+ "der",
+ "elliptic-curve",
+ "hmac",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-zebra"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409"
+dependencies = [
+ "curve25519-dalek",
+ "hex",
+ "rand_core 0.5.1",
+ "serde",
+ "sha2",
+ "thiserror",
+]
+
+[[package]]
+name = "elliptic-curve"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "069397e10739989e400628cbc0556a817a8a64119d7a2315767f4456e1332c23"
+dependencies = [
+ "crypto-bigint",
+ "ff",
+ "generic-array",
+ "group",
+ "pkcs8",
+ "rand_core 0.6.3",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ff"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63eec06c61e487eecf0f7e6e6372e596a81922c28d33e645d6983ca6493a1af0"
+dependencies = [
+ "rand_core 0.6.3",
+ "subtle",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "group"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912"
+dependencies = [
+ "ff",
+ "rand_core 0.6.3",
+ "subtle",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hmac"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
+dependencies = [
+ "crypto-mac",
+ "digest",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
+
+[[package]]
+name = "k256"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea"
+dependencies = [
+ "cfg-if",
+ "ecdsa",
+ "elliptic-curve",
+ "sha2",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
+name = "pkcs8"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbee84ed13e44dd82689fa18348a49934fa79cc774a344c42fc9b301c71b140a"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
+dependencies = [
+ "getrandom 0.2.3",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+
+[[package]]
+name = "schemars"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc6ab463ae35acccb5cba66c0084c985257b797d288b6050cc2f6ac1b266cb78"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "902fdfbcf871ae8f653bddf4b2c05905ddaabc08f69d32a915787e3be0d31356"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-json-wasm"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "042ac496d97e5885149d34139bad1d617192770d7eb8f1866da2317ff4501853"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.127"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha2"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12"
+dependencies = [
+ "block-buffer",
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+ "opaque-debug",
+]
+
+[[package]]
+name = "signature"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c19772be3c4dd2ceaacf03cb41d5885f2a02c4d8804884918e3a258480803335"
+dependencies = [
+ "digest",
+ "rand_core 0.6.3",
+]
+
+[[package]]
+name = "spki"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "987637c5ae6b3121aba9d513f869bd2bff11c4cc086c22473befd6649c0bd521"
+dependencies = [
+ "der",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "subtle"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
+
+[[package]]
+name = "syn"
+version = "1.0.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
+[[package]]
+name = "testgen-local"
+version = "0.1.0"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cosmwasm-storage",
+ "cw-storage-plus",
+ "cw2",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "typenum"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
+
+[[package]]
+name = "uint"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f"
+dependencies = [
+ "byteorder",
+ "crunchy",
+ "hex",
+ "static_assertions",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
+name = "zeroize"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd"
diff --git a/contracts/cw-govmod-single/Cargo.toml b/contracts/cw-govmod-single/Cargo.toml
new file mode 100644
index 00000000..4b04124d
--- /dev/null
+++ b/contracts/cw-govmod-single/Cargo.toml
@@ -0,0 +1,51 @@
+[package]
+name = "cw-govmod-single"
+version = "0.1.0"
+authors = ["Zeke Medley <zekemedley@gmail.com>"]
+edition = "2018"
+
+exclude = [
+  # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
+  "contract.wasm",
+  "hash.txt",
+]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+# for more explicit tests, cargo test --features=backtraces
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all instantiate/execute/query exports
+library = []
+
+[package.metadata.scripts]
+optimize = """docker run --rm -v "$(pwd)":/code \
+  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
+  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
+  cosmwasm/rust-optimizer:0.12.5
+"""
+
+[dependencies]
+cosmwasm-std = { version = "1.0.0-beta5" }
+cosmwasm-storage = { version = "1.0.0-beta5" }
+cw-storage-plus = "0.12"
+cw-utils = "0.12"
+cw2 = "0.12"
+cw20 = "0.12"
+cw3 = "0.12"
+schemars = "0.8"
+serde = { version = "1.0", default-features = false, features = ["derive"] }
+thiserror = { version = "1.0" }
+cw-governance = { path = "../cw-governance", version = "0.1.0", features = ["library"] }
+cw-governance-macros = { path = "../cw-governance-macros", version = "0.1.0" }
+cw-governance-interface = { version = "0.1.0", path = "../cw-governance-interface" }
+
+[dev-dependencies]
+cosmwasm-schema = { version = "1.0.0-beta5" }
+cw-multi-test = "0.12"
+cw20-balance-voting = { path = "../cw20-balance-voting", version = "0.1.0" }
+cw20-base = "0.12"
+rand = "0.8"
\ No newline at end of file
diff --git a/contracts/cw-govmod-single/README b/contracts/cw-govmod-single/README
new file mode 100644
index 00000000..055dd00c
--- /dev/null
+++ b/contracts/cw-govmod-single/README
@@ -0,0 +1,4 @@
+# cw20-govmod-sudo
+
+A governance module for the cw-governance contract. Does single choice
+voting.
\ No newline at end of file
diff --git a/contracts/cw-govmod-single/examples/schema.rs b/contracts/cw-govmod-single/examples/schema.rs
new file mode 100644
index 00000000..df503662
--- /dev/null
+++ b/contracts/cw-govmod-single/examples/schema.rs
@@ -0,0 +1,17 @@
+use std::env::current_dir;
+use std::fs::create_dir_all;
+
+use cosmwasm_schema::{export_schema, remove_schemas, schema_for};
+
+use cw_govmod_single::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+
+fn main() {
+    let mut out_dir = current_dir().unwrap();
+    out_dir.push("schema");
+    create_dir_all(&out_dir).unwrap();
+    remove_schemas(&out_dir).unwrap();
+
+    export_schema(&schema_for!(InstantiateMsg), &out_dir);
+    export_schema(&schema_for!(ExecuteMsg), &out_dir);
+    export_schema(&schema_for!(QueryMsg), &out_dir);
+}
diff --git a/contracts/cw-govmod-single/schema/execute_msg.json b/contracts/cw-govmod-single/schema/execute_msg.json
new file mode 100644
index 00000000..302a22ad
--- /dev/null
+++ b/contracts/cw-govmod-single/schema/execute_msg.json
@@ -0,0 +1,487 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "ExecuteMsg",
+  "oneOf": [
+    {
+      "type": "object",
+      "required": [
+        "execute"
+      ],
+      "properties": {
+        "execute": {
+          "type": "object",
+          "required": [
+            "msgs"
+          ],
+          "properties": {
+            "msgs": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/CosmosMsg_for_Empty"
+              }
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    }
+  ],
+  "definitions": {
+    "BankMsg": {
+      "description": "The message types of the bank module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto",
+      "oneOf": [
+        {
+          "description": "Sends native tokens from the contract to the given address.\n\nThis is translated to a [MsgSend](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/bank/v1beta1/tx.proto#L19-L28). `from_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "send"
+          ],
+          "properties": {
+            "send": {
+              "type": "object",
+              "required": [
+                "amount",
+                "to_address"
+              ],
+              "properties": {
+                "amount": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Coin"
+                  }
+                },
+                "to_address": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "This will burn the given coins from the contract's account. There is no Cosmos SDK message that performs this, but it can be done by calling the bank keeper. Important if a contract controls significant token supply that must be retired.",
+          "type": "object",
+          "required": [
+            "burn"
+          ],
+          "properties": {
+            "burn": {
+              "type": "object",
+              "required": [
+                "amount"
+              ],
+              "properties": {
+                "amount": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Coin"
+                  }
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Binary": {
+      "description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>",
+      "type": "string"
+    },
+    "Coin": {
+      "type": "object",
+      "required": [
+        "amount",
+        "denom"
+      ],
+      "properties": {
+        "amount": {
+          "$ref": "#/definitions/Uint128"
+        },
+        "denom": {
+          "type": "string"
+        }
+      }
+    },
+    "CosmosMsg_for_Empty": {
+      "oneOf": [
+        {
+          "type": "object",
+          "required": [
+            "bank"
+          ],
+          "properties": {
+            "bank": {
+              "$ref": "#/definitions/BankMsg"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "custom"
+          ],
+          "properties": {
+            "custom": {
+              "$ref": "#/definitions/Empty"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "staking"
+          ],
+          "properties": {
+            "staking": {
+              "$ref": "#/definitions/StakingMsg"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "distribution"
+          ],
+          "properties": {
+            "distribution": {
+              "$ref": "#/definitions/DistributionMsg"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "wasm"
+          ],
+          "properties": {
+            "wasm": {
+              "$ref": "#/definitions/WasmMsg"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "DistributionMsg": {
+      "description": "The message types of the distribution module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto",
+      "oneOf": [
+        {
+          "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "set_withdraw_address"
+          ],
+          "properties": {
+            "set_withdraw_address": {
+              "type": "object",
+              "required": [
+                "address"
+              ],
+              "properties": {
+                "address": {
+                  "description": "The `withdraw_address`",
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "This is translated to a [[MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "withdraw_delegator_reward"
+          ],
+          "properties": {
+            "withdraw_delegator_reward": {
+              "type": "object",
+              "required": [
+                "validator"
+              ],
+              "properties": {
+                "validator": {
+                  "description": "The `validator_address`",
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Empty": {
+      "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
+      "type": "object"
+    },
+    "StakingMsg": {
+      "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto",
+      "oneOf": [
+        {
+          "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "delegate"
+          ],
+          "properties": {
+            "delegate": {
+              "type": "object",
+              "required": [
+                "amount",
+                "validator"
+              ],
+              "properties": {
+                "amount": {
+                  "$ref": "#/definitions/Coin"
+                },
+                "validator": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "undelegate"
+          ],
+          "properties": {
+            "undelegate": {
+              "type": "object",
+              "required": [
+                "amount",
+                "validator"
+              ],
+              "properties": {
+                "amount": {
+                  "$ref": "#/definitions/Coin"
+                },
+                "validator": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L95-L105). `delegator_address` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "redelegate"
+          ],
+          "properties": {
+            "redelegate": {
+              "type": "object",
+              "required": [
+                "amount",
+                "dst_validator",
+                "src_validator"
+              ],
+              "properties": {
+                "amount": {
+                  "$ref": "#/definitions/Coin"
+                },
+                "dst_validator": {
+                  "type": "string"
+                },
+                "src_validator": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "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"
+    },
+    "WasmMsg": {
+      "description": "The message types of the wasm module.\n\nSee https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto",
+      "oneOf": [
+        {
+          "description": "Dispatches a call to another contract at a known address (with known ABI).\n\nThis is translated to a [MsgExecuteContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L68-L78). `sender` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "execute"
+          ],
+          "properties": {
+            "execute": {
+              "type": "object",
+              "required": [
+                "contract_addr",
+                "funds",
+                "msg"
+              ],
+              "properties": {
+                "contract_addr": {
+                  "type": "string"
+                },
+                "funds": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Coin"
+                  }
+                },
+                "msg": {
+                  "description": "msg is the json-encoded ExecuteMsg struct (as raw Binary)",
+                  "allOf": [
+                    {
+                      "$ref": "#/definitions/Binary"
+                    }
+                  ]
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Instantiates a new contracts from previously uploaded Wasm code.\n\nThis is translated to a [MsgInstantiateContract](https://github.com/CosmWasm/wasmd/blob/v0.16.0-alpha1/x/wasm/internal/types/tx.proto#L47-L61). `sender` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "instantiate"
+          ],
+          "properties": {
+            "instantiate": {
+              "type": "object",
+              "required": [
+                "code_id",
+                "funds",
+                "label",
+                "msg"
+              ],
+              "properties": {
+                "admin": {
+                  "type": [
+                    "string",
+                    "null"
+                  ]
+                },
+                "code_id": {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                },
+                "funds": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/definitions/Coin"
+                  }
+                },
+                "label": {
+                  "description": "A human-readbale label for the contract",
+                  "type": "string"
+                },
+                "msg": {
+                  "description": "msg is the JSON-encoded InstantiateMsg struct (as raw Binary)",
+                  "allOf": [
+                    {
+                      "$ref": "#/definitions/Binary"
+                    }
+                  ]
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Migrates a given contracts to use new wasm code. Passes a MigrateMsg to allow us to customize behavior.\n\nOnly the contract admin (as defined in wasmd), if any, is able to make this call.\n\nThis is translated to a [MsgMigrateContract](https://github.com/CosmWasm/wasmd/blob/v0.14.0/x/wasm/internal/types/tx.proto#L86-L96). `sender` is automatically filled with the current contract's address.",
+          "type": "object",
+          "required": [
+            "migrate"
+          ],
+          "properties": {
+            "migrate": {
+              "type": "object",
+              "required": [
+                "contract_addr",
+                "msg",
+                "new_code_id"
+              ],
+              "properties": {
+                "contract_addr": {
+                  "type": "string"
+                },
+                "msg": {
+                  "description": "msg is the json-encoded MigrateMsg struct that will be passed to the new code",
+                  "allOf": [
+                    {
+                      "$ref": "#/definitions/Binary"
+                    }
+                  ]
+                },
+                "new_code_id": {
+                  "description": "the code_id of the new logic to place in the given contract",
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Sets a new admin (for migrate) on the given contract. Fails if this contract is not currently admin of the target contract.",
+          "type": "object",
+          "required": [
+            "update_admin"
+          ],
+          "properties": {
+            "update_admin": {
+              "type": "object",
+              "required": [
+                "admin",
+                "contract_addr"
+              ],
+              "properties": {
+                "admin": {
+                  "type": "string"
+                },
+                "contract_addr": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Clears the admin on the given contract, so no more migration possible. Fails if this contract is not currently admin of the target contract.",
+          "type": "object",
+          "required": [
+            "clear_admin"
+          ],
+          "properties": {
+            "clear_admin": {
+              "type": "object",
+              "required": [
+                "contract_addr"
+              ],
+              "properties": {
+                "contract_addr": {
+                  "type": "string"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    }
+  }
+}
diff --git a/contracts/cw-govmod-single/schema/instantiate_msg.json b/contracts/cw-govmod-single/schema/instantiate_msg.json
new file mode 100644
index 00000000..8e3416dc
--- /dev/null
+++ b/contracts/cw-govmod-single/schema/instantiate_msg.json
@@ -0,0 +1,13 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "InstantiateMsg",
+  "type": "object",
+  "required": [
+    "root"
+  ],
+  "properties": {
+    "root": {
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw-govmod-single/schema/query_msg.json b/contracts/cw-govmod-single/schema/query_msg.json
new file mode 100644
index 00000000..1936c076
--- /dev/null
+++ b/contracts/cw-govmod-single/schema/query_msg.json
@@ -0,0 +1,30 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "QueryMsg",
+  "oneOf": [
+    {
+      "type": "object",
+      "required": [
+        "admin"
+      ],
+      "properties": {
+        "admin": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "dao"
+      ],
+      "properties": {
+        "dao": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    }
+  ]
+}
diff --git a/contracts/cw-govmod-single/src/contract.rs b/contracts/cw-govmod-single/src/contract.rs
new file mode 100644
index 00000000..096f828e
--- /dev/null
+++ b/contracts/cw-govmod-single/src/contract.rs
@@ -0,0 +1,436 @@
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cosmwasm_std::{
+    to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response,
+    StdResult, WasmMsg,
+};
+use cw2::set_contract_version;
+use cw_storage_plus::Bound;
+use cw_utils::{Duration, Expiration};
+
+use crate::{
+    error::ContractError,
+    msg::{DepositInfo, ExecuteMsg, InstantiateMsg, QueryMsg},
+    proposal::{advance_proposal_id, Proposal, Status, Vote, Votes},
+    query::{ProposalResponse, VoteInfo, VoteListResponse, VoteResponse},
+    state::{
+        get_deposit_msg, get_return_deposit_msg, Ballot, Config, BALLOTS, CONFIG, PROPOSALS,
+        PROPOSAL_COUNT,
+    },
+    threshold::Threshold,
+    utils::{get_total_power, get_voting_power},
+};
+
+const CONTRACT_NAME: &str = "crates.io:cw-govmod-single";
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+/// Default limit for proposal pagination.
+const DEFAULT_LIMIT: u64 = 30;
+
+#[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)?;
+
+    msg.threshold.validate()?;
+
+    let dao = info.sender;
+    let deposit_info = msg
+        .deposit_info
+        .map(|info| info.into_checked(deps.as_ref(), dao.clone()))
+        .transpose()?;
+
+    let config = Config {
+        threshold: msg.threshold,
+        max_voting_period: msg.max_voting_period,
+        only_members_execute: msg.only_members_execute,
+        dao: dao.clone(),
+        deposit_info,
+    };
+
+    CONFIG.save(deps.storage, &config)?;
+
+    Ok(Response::default()
+        .add_attribute("action", "instantiate")
+        .add_attribute("dao", dao))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<Response, ContractError> {
+    match msg {
+        ExecuteMsg::Propose {
+            title,
+            description,
+            msgs,
+            latest,
+        } => execute_propose(deps, env, info.sender, title, description, msgs, latest),
+        ExecuteMsg::Vote { proposal_id, vote } => execute_vote(deps, env, info, proposal_id, vote),
+        ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id),
+        ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id),
+        ExecuteMsg::UpdateConfig {
+            threshold,
+            max_voting_period,
+            only_members_execute,
+            dao,
+            deposit_info,
+        } => execute_update_config(
+            deps,
+            info,
+            threshold,
+            max_voting_period,
+            only_members_execute,
+            dao,
+            deposit_info,
+        ),
+    }
+}
+
+pub fn execute_propose(
+    deps: DepsMut,
+    env: Env,
+    sender: Addr,
+    title: String,
+    description: String,
+    msgs: Vec<CosmosMsg<Empty>>,
+    latest: Option<Expiration>,
+) -> Result<Response, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+
+    // Check that the sender is a member of the governance contract.
+    let sender_power = get_voting_power(
+        deps.as_ref(),
+        sender.clone(),
+        config.dao.clone(),
+        Some(env.block.height),
+    )?;
+    if sender_power.is_zero() {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    // Set the expiration to the minimum of the proposal's `latest`
+    // argument and the configured max voting period.
+    let max_voting_expiration = config.max_voting_period.after(&env.block);
+    let expiration = if let Some(latest) = latest {
+        if latest <= max_voting_expiration {
+            latest
+        } else {
+            return Err(ContractError::InvalidExpiration {});
+        }
+    } else {
+        max_voting_expiration
+    };
+
+    let total_power = get_total_power(deps.as_ref(), config.dao, Some(env.block.height))?;
+
+    let proposal = {
+        // Limit mutability to this block.
+        let mut proposal = Proposal {
+            title,
+            description,
+            proposer: sender.clone(),
+            start_height: env.block.height,
+            expiration,
+            threshold: config.threshold,
+            total_power,
+            msgs,
+            status: Status::Open,
+            votes: Votes::zero(),
+            deposit_info: config.deposit_info.clone(),
+        };
+        // Update the proposal's status. Addresses case where proposal
+        // expires on the same block as it is created.
+        proposal.update_status(&env.block);
+        proposal
+    };
+    let id = advance_proposal_id(deps.storage)?;
+    PROPOSALS.save(deps.storage, id, &proposal)?;
+
+    let deposit_msg = get_deposit_msg(&config.deposit_info, &env.contract.address, &sender)?;
+
+    Ok(Response::default()
+        .add_messages(deposit_msg)
+        .add_attribute("action", "propose")
+        .add_attribute("sender", sender)
+        .add_attribute("proposal_id", id.to_string())
+        .add_attribute("status", proposal.status.to_string()))
+}
+
+pub fn execute_execute(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    proposal_id: u64,
+) -> Result<Response, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+    if config.only_members_execute {
+        let power = get_voting_power(
+            deps.as_ref(),
+            info.sender.clone(),
+            config.dao.clone(),
+            Some(env.block.height),
+        )?;
+        if power.is_zero() {
+            return Err(ContractError::Unauthorized {});
+        }
+    }
+
+    let mut prop = PROPOSALS
+        .may_load(deps.storage, proposal_id)?
+        .ok_or(ContractError::NoSuchProposal { id: proposal_id })?;
+
+    // Check here that the proposal is passed. Allow it to be
+    // executed even if it is expired so long as it passed during its
+    // voting period.
+    if !prop.is_passed(&env.block) {
+        return Err(ContractError::NotPassed {});
+    }
+    prop.status = Status::Executed;
+    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
+
+    let return_deposit = get_return_deposit_msg(&prop)?;
+
+    let response = if !prop.msgs.is_empty() {
+        let execute_message = WasmMsg::Execute {
+            contract_addr: config.dao.to_string(),
+            msg: to_binary(&cw_governance::msg::ExecuteMsg::ExecuteProposalHook {
+                msgs: prop.msgs,
+            })?,
+            funds: vec![],
+        };
+        Response::<Empty>::default().add_message(execute_message)
+    } else {
+        Response::default()
+    };
+
+    Ok(response
+        .add_messages(return_deposit)
+        .add_attribute("action", "execute")
+        .add_attribute("sender", info.sender)
+        .add_attribute("proposal_id", proposal_id.to_string())
+        .add_attribute("dao", config.dao))
+}
+
+pub fn execute_vote(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    proposal_id: u64,
+    vote: Vote,
+) -> Result<Response, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+    let mut prop = PROPOSALS
+        .may_load(deps.storage, proposal_id)?
+        .ok_or(ContractError::NoSuchProposal { id: proposal_id })?;
+    if prop.current_status(&env.block) != Status::Open {
+        return Err(ContractError::NotOpen { id: proposal_id });
+    }
+
+    let vote_power = get_voting_power(
+        deps.as_ref(),
+        info.sender.clone(),
+        config.dao,
+        Some(prop.start_height),
+    )?;
+    if vote_power.is_zero() {
+        return Err(ContractError::NotRegistered {});
+    }
+
+    BALLOTS.update(
+        deps.storage,
+        (proposal_id, info.sender.clone()),
+        |bal| match bal {
+            Some(_) => Err(ContractError::AlreadyVoted {}),
+            None => Ok(Ballot {
+                power: vote_power,
+                vote,
+            }),
+        },
+    )?;
+
+    prop.votes.add_vote(vote, vote_power);
+    prop.update_status(&env.block);
+    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
+
+    Ok(Response::default()
+        .add_attribute("action", "vote")
+        .add_attribute("sender", info.sender)
+        .add_attribute("proposal_id", proposal_id.to_string())
+        .add_attribute("position", vote.to_string())
+        .add_attribute("status", prop.status.to_string()))
+}
+
+pub fn execute_close(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    proposal_id: u64,
+) -> Result<Response, ContractError> {
+    let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
+
+    // Only open and expired or rejected proposals may be closed.
+    match prop.status {
+        Status::Rejected => (),
+        Status::Open => {
+            if !prop.expiration.is_expired(&env.block) {
+                return Err(ContractError::NotExpired {});
+            }
+        }
+        _ => return Err(ContractError::WrongCloseStatus {}),
+    }
+
+    let refund_message = get_return_deposit_msg(&prop)?;
+
+    prop.status = Status::Closed;
+    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
+
+    Ok(Response::default()
+        .add_attribute("action", "close")
+        .add_attribute("sender", info.sender)
+        .add_messages(refund_message)
+        .add_attribute("proposal_id", proposal_id.to_string()))
+}
+
+pub fn execute_update_config(
+    deps: DepsMut,
+    info: MessageInfo,
+    threshold: Threshold,
+    max_voting_period: Duration,
+    only_members_execute: bool,
+    dao: String,
+    deposit_info: Option<DepositInfo>,
+) -> Result<Response, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+
+    // Only the DAO may call this method.
+    if info.sender != config.dao {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    threshold.validate()?;
+    let dao = deps.api.addr_validate(&dao)?;
+    let deposit_info = deposit_info
+        .map(|info| info.into_checked(deps.as_ref(), dao.clone()))
+        .transpose()?;
+
+    CONFIG.save(
+        deps.storage,
+        &Config {
+            threshold,
+            max_voting_period,
+            only_members_execute,
+            dao,
+            deposit_info,
+        },
+    )?;
+
+    Ok(Response::default()
+        .add_attribute("action", "update_config")
+        .add_attribute("sender", info.sender))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::Config {} => query_config(deps),
+        QueryMsg::Proposal { proposal_id } => query_proposal(deps, env, proposal_id),
+        QueryMsg::ListProposals { start_after, limit } => {
+            query_list_proposals(deps, env, start_after, limit)
+        }
+        QueryMsg::ProposalCount {} => query_proposal_count(deps),
+        QueryMsg::Vote { proposal_id, voter } => query_vote(deps, proposal_id, voter),
+        QueryMsg::ListVotes {
+            proposal_id,
+            start_after,
+            limit,
+        } => query_list_votes(deps, proposal_id, start_after, limit),
+        QueryMsg::Info {} => query_info(deps),
+    }
+}
+
+pub fn query_config(deps: Deps) -> StdResult<Binary> {
+    let config = CONFIG.load(deps.storage)?;
+    to_binary(&config)
+}
+
+pub fn query_proposal(deps: Deps, env: Env, id: u64) -> StdResult<Binary> {
+    let proposal = PROPOSALS.load(deps.storage, id)?;
+    to_binary(&proposal.into_response(&env.block, id))
+}
+
+pub fn query_list_proposals(
+    deps: Deps,
+    env: Env,
+    start_after: Option<u64>,
+    limit: Option<u64>,
+) -> StdResult<Binary> {
+    let min = start_after.map(Bound::exclusive);
+    let limit = limit.unwrap_or(DEFAULT_LIMIT);
+    let props: Vec<ProposalResponse> = PROPOSALS
+        .range(deps.storage, min, None, cosmwasm_std::Order::Ascending)
+        .take(limit as usize)
+        .collect::<Result<Vec<(u64, Proposal)>, _>>()?
+        .into_iter()
+        .map(|(id, proposal)| proposal.into_response(&env.block, id))
+        .collect();
+
+    to_binary(&props)
+}
+
+pub fn query_proposal_count(deps: Deps) -> StdResult<Binary> {
+    let proposal_count = PROPOSAL_COUNT.load(deps.storage)?;
+    to_binary(&proposal_count)
+}
+
+pub fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult<Binary> {
+    let voter = deps.api.addr_validate(&voter)?;
+    let ballot = BALLOTS.may_load(deps.storage, (proposal_id, voter.clone()))?;
+    let vote = ballot.map(|ballot| VoteInfo {
+        voter,
+        vote: ballot.vote,
+        power: ballot.power,
+    });
+    to_binary(&VoteResponse { vote })
+}
+
+pub fn query_list_votes(
+    deps: Deps,
+    proposal_id: u64,
+    start_after: Option<String>,
+    limit: Option<u64>,
+) -> StdResult<Binary> {
+    let limit = limit.unwrap_or(DEFAULT_LIMIT);
+    let start_after = start_after
+        .map(|addr| deps.api.addr_validate(&addr))
+        .transpose()?;
+    let min = start_after.map(Bound::<Addr>::exclusive);
+
+    let votes = BALLOTS
+        .prefix(proposal_id)
+        .range(deps.storage, min, None, cosmwasm_std::Order::Ascending)
+        .take(limit as usize)
+        .map(|item| {
+            let (voter, ballot) = item?;
+            Ok(VoteInfo {
+                voter,
+                vote: ballot.vote,
+                power: ballot.power,
+            })
+        })
+        .collect::<StdResult<Vec<_>>>()?;
+
+    to_binary(&VoteListResponse { votes })
+}
+
+pub fn query_info(deps: Deps) -> StdResult<Binary> {
+    let info = cw2::get_contract_version(deps.storage)?;
+    to_binary(&cw_governance_interface::voting::InfoResponse { info })
+}
diff --git a/contracts/cw-govmod-single/src/error.rs b/contracts/cw-govmod-single/src/error.rs
new file mode 100644
index 00000000..61e857c9
--- /dev/null
+++ b/contracts/cw-govmod-single/src/error.rs
@@ -0,0 +1,46 @@
+use std::u64;
+
+use cosmwasm_std::StdError;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("Required threshold cannot be zero")]
+    ZeroThreshold {},
+
+    #[error("Not possible to reach required (passing) threshold")]
+    UnreachableThreshold {},
+
+    #[error("Suggested proposal expiration is larger than the maximum proposal duration")]
+    InvalidExpiration {},
+
+    #[error("No such proposal ({id})")]
+    NoSuchProposal { id: u64 },
+
+    #[error("Proposal is not open ({id})")]
+    NotOpen { id: u64 },
+
+    #[error("Proposal is expired ({id})")]
+    Expired { id: u64 },
+
+    #[error("Not registered to vote (no voting power) at time of proposal creation.")]
+    NotRegistered {},
+
+    #[error("Already voted")]
+    AlreadyVoted {},
+
+    #[error("Proposal is not passed.")]
+    NotPassed {},
+
+    #[error("Proposal is not expired.")]
+    NotExpired {},
+
+    #[error("Only rejected or expired proposals may be closed.")]
+    WrongCloseStatus {},
+}
diff --git a/contracts/cw-govmod-single/src/lib.rs b/contracts/cw-govmod-single/src/lib.rs
new file mode 100644
index 00000000..80747ebd
--- /dev/null
+++ b/contracts/cw-govmod-single/src/lib.rs
@@ -0,0 +1,12 @@
+pub mod contract;
+mod error;
+pub mod msg;
+pub mod proposal;
+pub mod query;
+pub mod state;
+#[cfg(test)]
+mod tests;
+pub mod threshold;
+pub mod utils;
+
+pub use crate::error::ContractError;
diff --git a/contracts/cw-govmod-single/src/msg.rs b/contracts/cw-govmod-single/src/msg.rs
new file mode 100644
index 00000000..03284f65
--- /dev/null
+++ b/contracts/cw-govmod-single/src/msg.rs
@@ -0,0 +1,139 @@
+use cosmwasm_std::{CosmosMsg, Empty, Uint128};
+use cw_utils::{Duration, Expiration};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::{proposal::Vote, threshold::Threshold};
+use cw_governance_macros::govmod_query;
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct InstantiateMsg {
+    /// The threshold a proposal must reach to complete.
+    pub threshold: Threshold,
+    /// The default maximum amount of time a proposal may be voted on
+    /// before expiring.
+    pub max_voting_period: Duration,
+    /// If set to true only members may execute passed
+    /// proposals. Otherwise, any address may execute a passed
+    /// proposal.
+    pub only_members_execute: bool,
+    /// Information about the deposit required to create a
+    /// proposal. None if there is no deposit requirement, Some
+    /// otherwise.
+    pub deposit_info: Option<DepositInfo>,
+}
+
+/// Information about the token to use for proposal deposits.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum DepositToken {
+    /// Use a specific token address as the deposit token.
+    Token(String),
+    /// Use the token address of the associated DAO's voting
+    /// module. NOTE: in order to use the token address of the voting
+    /// module the voting module must (1) use a cw20 token and (2)
+    /// implement the `TokenContract {}` query type defined by
+    /// `cw_governance_macros::token_query`. Failing to implement that
+    /// and using this option will cause instantiation to fail.
+    VotingModuleToken,
+}
+
+/// Information about the deposit required to create a proposal.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct DepositInfo {
+    /// The address of the cw20 token to be used for proposal
+    /// deposits.
+    pub token: DepositToken,
+    /// The number of tokens that must be deposited to create a
+    /// proposal.
+    pub deposit: Uint128,
+    /// If failed proposals should have their deposits refunded.
+    pub refund_failed_proposals: bool,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum ExecuteMsg {
+    /// Creates a proposal in the governance module.
+    Propose {
+        /// The title of the proposal.
+        title: String,
+        /// A description of the proposal.
+        description: String,
+        /// The messages that should be executed in response to this
+        /// proposal passing.
+        msgs: Vec<CosmosMsg<Empty>>,
+        /// Optionally, a proposal may have a different expiration
+        /// than the one that would be set by the `max_voting_period`
+        /// in the governance module's config.
+        latest: Option<Expiration>,
+    },
+    /// Votes on a proposal. Voting power is determined by the DAO's
+    /// voting power module.
+    Vote {
+        /// The ID of the proposal to vote on.
+        proposal_id: u64,
+        /// The senders position on the proposal.
+        vote: Vote,
+    },
+    /// Causes the messages associated with a passed proposal to be
+    /// executed by the DAO.
+    Execute {
+        /// The ID of the proposal to execute.
+        proposal_id: u64,
+    },
+    /// Closes a proposal that has failed (either not passed or timed
+    /// out). If applicable this will cause the proposal deposit
+    /// associated wth said proposal to be returned.
+    Close {
+        /// The ID of the proposal to close.
+        proposal_id: u64,
+    },
+    /// Updates the governance module's config.
+    UpdateConfig {
+        /// The new proposal passing threshold. This will only apply
+        /// to proposals created after the config update.
+        threshold: Threshold,
+        /// The default maximum amount of time a proposal may be voted
+        /// on before expiring. This will only apply to proposals
+        /// created after the config update.
+        max_voting_period: Duration,
+        /// If set to true only members may execute passed
+        /// proposals. Otherwise, any address may execute a passed
+        /// proposal. Applies to all outstanding and future proposals.
+        only_members_execute: bool,
+        /// The address if tge DAO that this governance module is
+        /// associated with.
+        dao: String,
+        /// Information about the deposit required to make a
+        /// proposal. None if no deposit, Some otherwise.
+        deposit_info: Option<DepositInfo>,
+    },
+}
+
+#[govmod_query]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum QueryMsg {
+    /// Gets the governance module's config. Returns `state::Config`.
+    Config {},
+    /// Gets information about a proposal. Returns
+    /// `proposals::Proposal`.
+    Proposal {
+        proposal_id: u64,
+    },
+    ListProposals {
+        start_after: Option<u64>,
+        limit: Option<u64>,
+    },
+    ProposalCount {},
+    Vote {
+        proposal_id: u64,
+        voter: String,
+    },
+    ListVotes {
+        proposal_id: u64,
+        start_after: Option<String>,
+        limit: Option<u64>,
+    },
+}
diff --git a/contracts/cw-govmod-single/src/proposal.rs b/contracts/cw-govmod-single/src/proposal.rs
new file mode 100644
index 00000000..c572aa82
--- /dev/null
+++ b/contracts/cw-govmod-single/src/proposal.rs
@@ -0,0 +1,770 @@
+use std::convert::TryInto;
+
+use cosmwasm_std::{
+    Addr, BlockInfo, CosmosMsg, Decimal, Empty, StdResult, Storage, Uint128, Uint256,
+};
+use cw_utils::Expiration;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+    query::ProposalResponse,
+    state::{CheckedDepositInfo, PROPOSAL_COUNT},
+    threshold::Threshold,
+};
+
+// We multiply by this when calculating needed_votes in order to round
+// up properly.
+const PRECISION_FACTOR: u128 = 10u128.pow(9);
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Copy)]
+#[serde(rename_all = "snake_case")]
+#[repr(u8)]
+pub enum Status {
+    /// The proposal is open for voting.
+    Open,
+    /// The proposal has been rejected.
+    Rejected,
+    /// The proposal has been passed but has not been executed.
+    Passed,
+    /// The proposal has been passed and executed.
+    Executed,
+    /// The proposal has failed or expired and has been closed. A
+    /// proposal deposit refund has been issued if applicable.
+    Closed,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct Votes {
+    pub yes: Uint128,
+    pub no: Uint128,
+    pub abstain: Uint128,
+}
+
+#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, JsonSchema, Debug)]
+#[serde(rename_all = "lowercase")]
+#[repr(u8)]
+pub enum Vote {
+    /// Marks support for the proposal.
+    Yes,
+    /// Marks opposition to the proposal.
+    No,
+    /// Marks participation but does not count towards the ratio of
+    /// support / opposed.
+    Abstain,
+}
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct Proposal {
+    pub title: String,
+    pub description: String,
+    pub proposer: Addr,
+
+    pub start_height: u64,
+    pub expiration: Expiration,
+
+    pub threshold: Threshold,
+    pub total_power: Uint128,
+
+    pub msgs: Vec<CosmosMsg<Empty>>,
+
+    pub status: Status,
+    pub votes: Votes,
+
+    /// Information about the deposit that was sent as part of this
+    /// proposal. None if no deposit.
+    pub deposit_info: Option<CheckedDepositInfo>,
+}
+
+/// Information about the number of votes needed to pass a proposal.
+#[derive(PartialEq, Clone, Debug)]
+enum VotesNeeded {
+    /// A finite number of votes.
+    Finite(Uint128),
+    /// An unrechable number of votes. Caused by zero total voting
+    /// power.
+    Unreachable,
+}
+/// Computes the number of votes needed for a proposal to pass. This
+/// must round up. For example, with a 50% passing percentage and 15
+/// total votes 8 votes are required, not 7.
+///
+/// Note that it is possible, though unlikely, that no number of votes
+/// will ever meet the threshold. This happens if the total power is
+/// zero. For example, this may happen if all votes are abstain.
+fn votes_needed(total_power: Uint128, passing_percentage: Decimal) -> VotesNeeded {
+    // If there are no votes possible then no threshold will ever be
+    // reachable.
+    if total_power.is_zero() {
+        return VotesNeeded::Unreachable;
+    }
+    // Voting power is counted with a Uint128. In order to avoid an
+    // overflow while multiplying by the percision factor we need to
+    // do a full mul which results in a Uint256.
+    //
+    // Percision factor here ensures that any rounding down here that
+    // happens in the VM happens in the 9th decimal place. This makes
+    // it reasonably likely that we successfully round up.
+    let total_power = total_power.full_mul(PRECISION_FACTOR);
+    // Multiplication of a Uint256 and a Decimal is not implemented so
+    // we first multiply by the decimal's raw form and then divide by
+    // the number of decimal places in the decimal. This is the same
+    // thing that happens under the hood when a Uint128 is multiplied
+    // by Decimal.
+    //
+    // This multiplication will round down but because we have
+    // multiplied by `PERCISION_FACTOR` above that rounding ought to
+    // occur in the 9th decimal place.
+    let applied = total_power.multiply_ratio(
+        passing_percentage.atomics(),
+        Uint256::from(10u64).pow(passing_percentage.decimal_places()),
+    );
+    // The maximum possible value for applied occurs if `total_power`
+    // is 2^128. Given a percision factor of 10^9 we confirm that the
+    // numerator value will not overflow as:
+    //
+    // 2^128 * 10^9 + 10^9 < 2^256
+    //
+    // In the interest of being percise, this will not overflow so
+    // long as our percision factor is less than `3.4*10^38`. We don't
+    // have to worry about this because that value won't even fit into
+    // a u128 (the type of PERCISION_FACTOR).
+    let rounded = (applied + Uint256::from(PRECISION_FACTOR) - Uint256::from(1u128))
+        / Uint256::from(PRECISION_FACTOR);
+    // Truncated should be strictly <= than the largest possible
+    // Uint128. Imagine the pathalogical case where the passing
+    // threshold is 100% and there are 2^128 total votes. In this case
+    // rounded is:
+    //
+    // Floor[(2^128 * 10^9 - 1) / 10^9 + 1]
+    //
+    // This number is 2^128 which will fit into a 128 bit
+    // integer. Note: if we didn't floor here this would not be the
+    // case, happily unsigned integer division does indeed do that.
+    let truncated: Uint128 = rounded.try_into().unwrap();
+    VotesNeeded::Finite(truncated)
+}
+
+fn votes_meet_threshold(votes: Uint128, total_power: Uint128, passing_percentage: Decimal) -> bool {
+    match votes_needed(total_power, passing_percentage) {
+        VotesNeeded::Finite(needed) => votes >= needed,
+        VotesNeeded::Unreachable => false,
+    }
+}
+
+pub fn advance_proposal_id(store: &mut dyn Storage) -> StdResult<u64> {
+    let id: u64 = PROPOSAL_COUNT.may_load(store)?.unwrap_or_default() + 1;
+    PROPOSAL_COUNT.save(store, &id)?;
+    Ok(id)
+}
+
+impl Votes {
+    /// Constructs an zero'd out votes struct.
+    pub fn zero() -> Self {
+        Self {
+            yes: Uint128::zero(),
+            no: Uint128::zero(),
+            abstain: Uint128::zero(),
+        }
+    }
+
+    /// Constructs a vote with a specified number of yes votes. Used
+    /// for testing.
+    #[cfg(test)]
+    pub fn with_yes(yes: Uint128) -> Self {
+        Self {
+            yes,
+            no: Uint128::zero(),
+            abstain: Uint128::zero(),
+        }
+    }
+
+    /// Adds a vote to the votes.
+    pub fn add_vote(&mut self, vote: Vote, power: Uint128) {
+        match vote {
+            Vote::Yes => self.yes += power,
+            Vote::No => self.no += power,
+            Vote::Abstain => self.abstain += power,
+        }
+    }
+
+    /// Computes the total number of votes cast.
+    ///
+    /// NOTE: The total number of votes avaliable from a voting module
+    /// is a `Uint128`. As it is not possible to vote twice we know
+    /// that the sum of votes must be <= 2^128 and can safely return a
+    /// `Uint128` from this function. A missbehaving voting power
+    /// module may break this invariant.
+    pub fn total(&self) -> Uint128 {
+        self.yes + self.no + self.abstain
+    }
+}
+
+impl Proposal {
+    /// Consumes the proposal and returns a version which may be used
+    /// in a query response. The difference being that proposal
+    /// statuses are only updated on vote, execute, and close
+    /// events. It is possible though that since a vote has occured
+    /// the proposal expiring has changed its status. This method
+    /// recomputes the status so that queries get accurate
+    /// information.
+    pub fn into_response(mut self, block: &BlockInfo, id: u64) -> ProposalResponse {
+        self.update_status(block);
+        ProposalResponse { id, proposal: self }
+    }
+
+    /// Gets the current status of the proposal.
+    pub fn current_status(&self, block: &BlockInfo) -> Status {
+        if self.status == Status::Open && self.is_passed(block) {
+            Status::Passed
+        } else if self.status == Status::Open
+            && (self.expiration.is_expired(block) || self.is_rejected(block))
+        {
+            Status::Rejected
+        } else {
+            self.status
+        }
+    }
+
+    /// Sets a proposals status to its current status.
+    pub fn update_status(&mut self, block: &BlockInfo) {
+        self.status = self.current_status(block)
+    }
+
+    /// Returns true iff this proposal is sure to pass (even before
+    /// expiration if no future sequence of possible votes can cause
+    /// it to fail)
+    pub fn is_passed(&self, block: &BlockInfo) -> bool {
+        self.does_vote_count_reach_threshold(self.votes.yes, block)
+    }
+
+    /// As above for the passed check, used to check if a proposal is
+    /// already rejected.
+    pub fn is_rejected(&self, block: &BlockInfo) -> bool {
+        self.does_vote_count_reach_threshold(self.votes.no, block)
+    }
+
+    /// Helper function to determine if a vote count has reached the
+    /// proposal's threshold. Useful for determining if a particular
+    /// outcome has been reached, for example
+    /// `does_vote_count_meet_threshold(self.votes.yes, block)` will
+    /// return true if there are a sufficent number of yes votes to
+    /// pass the proposal.
+    fn does_vote_count_reach_threshold(&self, vote_count: Uint128, block: &BlockInfo) -> bool {
+        match self.threshold {
+            Threshold::AbsolutePercentage { percentage } => votes_meet_threshold(
+                vote_count,
+                self.total_power - self.votes.abstain,
+                percentage,
+            ),
+            Threshold::ThresholdQuorum { threshold, quorum } => {
+                if !votes_meet_threshold(self.votes.total(), self.total_power, quorum) {
+                    return false;
+                }
+                if self.expiration.is_expired(block) {
+                    // If the quorum is met and the proposal is
+                    // expired the number of votes needed to pass a
+                    // proposal is compared to the number of votes on
+                    // the proposal.
+                    //
+                    // NOTE(zeke): I do not like this
+                    // behavior. Strongly recomend that we either
+                    // remove custom end dates from proposals or
+                    // remove this logic. These together make the cost
+                    // of doing a hostile takeover of the DAO
+                    // `token_price * quorum` as opposed to the
+                    // 'desired' `token_price * threshold`.
+                    let options = self.votes.total() - self.votes.abstain;
+                    votes_meet_threshold(vote_count, options, threshold)
+                } else {
+                    let options = self.total_power - self.votes.abstain;
+                    votes_meet_threshold(vote_count, options, threshold)
+                }
+            }
+        }
+    }
+}
+
+impl std::fmt::Display for Status {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Status::Open => write!(f, "open"),
+            Status::Rejected => write!(f, "rejected"),
+            Status::Passed => write!(f, "passed"),
+            Status::Executed => write!(f, "executed"),
+            Status::Closed => write!(f, "closed"),
+        }
+    }
+}
+
+impl std::fmt::Display for Vote {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Vote::Yes => write!(f, "yes"),
+            Vote::No => write!(f, "no"),
+            Vote::Abstain => write!(f, "abstain"),
+        }
+    }
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use cosmwasm_std::testing::mock_env;
+
+    #[test]
+    fn count_votes() {
+        let mut votes = Votes::with_yes(Uint128::new(5));
+        votes.add_vote(Vote::No, Uint128::new(10));
+        votes.add_vote(Vote::Yes, Uint128::new(30));
+        votes.add_vote(Vote::Abstain, Uint128::new(40));
+
+        assert_eq!(votes.total(), Uint128::new(5 + 10 + 30 + 40));
+        assert_eq!(votes.yes, Uint128::new(35));
+        assert_eq!(votes.no, Uint128::new(10));
+        assert_eq!(votes.abstain, Uint128::new(40));
+    }
+
+    #[test]
+    fn votes_needed_rounds_properly() {
+        // round up right below 1
+        assert_eq!(
+            VotesNeeded::Finite(Uint128::new(1)),
+            votes_needed(Uint128::new(3), Decimal::permille(333))
+        );
+        // round up right over 1
+        assert_eq!(
+            VotesNeeded::Finite(Uint128::new(2)),
+            votes_needed(Uint128::new(3), Decimal::permille(334))
+        );
+        assert_eq!(
+            VotesNeeded::Finite(Uint128::new(11)),
+            votes_needed(Uint128::new(30), Decimal::permille(334))
+        );
+
+        // exact matches don't round
+        assert_eq!(
+            VotesNeeded::Finite(Uint128::new(17)),
+            votes_needed(Uint128::new(34), Decimal::percent(50))
+        );
+        assert_eq!(
+            VotesNeeded::Finite(Uint128::new(12)),
+            votes_needed(Uint128::new(48), Decimal::percent(25))
+        );
+
+        assert_eq!(
+            VotesNeeded::Finite(Uint128::new(7)),
+            votes_needed(Uint128::new(13), Decimal::percent(50))
+        );
+
+        assert_eq!(
+            VotesNeeded::Unreachable,
+            votes_needed(Uint128::zero(), Decimal::percent(50))
+        );
+
+        assert_eq!(
+            VotesNeeded::Finite(Uint128::new(1)),
+            votes_needed(Uint128::new(1), Decimal::percent(1))
+        );
+
+        assert_eq!(
+            VotesNeeded::Finite(Uint128::new(0)),
+            votes_needed(Uint128::new(1), Decimal::percent(0))
+        );
+    }
+
+    fn setup_prop(
+        threshold: Threshold,
+        votes: Votes,
+        total_power: Uint128,
+        is_expired: bool,
+    ) -> (Proposal, BlockInfo) {
+        let block = mock_env().block;
+        let expiration = match is_expired {
+            true => Expiration::AtHeight(block.height - 5),
+            false => Expiration::AtHeight(block.height + 100),
+        };
+        let prop = Proposal {
+            title: "Demo".to_string(),
+            description: "Info".to_string(),
+            proposer: Addr::unchecked("test"),
+            start_height: 100,
+            expiration,
+            msgs: vec![],
+            status: Status::Open,
+            threshold,
+            total_power,
+            votes,
+            deposit_info: None,
+        };
+        (prop, block)
+    }
+
+    fn check_is_passed(
+        threshold: Threshold,
+        votes: Votes,
+        total_power: Uint128,
+        is_expired: bool,
+    ) -> bool {
+        let (prop, block) = setup_prop(threshold, votes, total_power, is_expired);
+        prop.is_passed(&block)
+    }
+
+    fn check_is_rejected(
+        threshold: Threshold,
+        votes: Votes,
+        total_power: Uint128,
+        is_expired: bool,
+    ) -> bool {
+        let (prop, block) = setup_prop(threshold, votes, total_power, is_expired);
+        prop.is_rejected(&block)
+    }
+
+    #[test]
+    fn test_pass_absolute_percentage() {
+        let threshold = Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(50),
+        };
+        let votes = Votes {
+            yes: Uint128::new(7),
+            no: Uint128::new(4),
+            abstain: Uint128::new(2),
+        };
+
+        // 15 total votes. 7 yes and 2 abstain. 50% threshold. Should
+        // be passed.
+        assert!(check_is_passed(
+            threshold.clone(),
+            votes.clone(),
+            Uint128::new(15),
+            false
+        ));
+        // Proposal being expired should not effect those results.
+        assert!(check_is_passed(
+            threshold.clone(),
+            votes.clone(),
+            Uint128::new(15),
+            true
+        ));
+
+        // More votes == higher threshold => not passed.
+        assert!(!check_is_passed(threshold, votes, Uint128::new(17), false));
+    }
+
+    #[test]
+    fn test_reject_absolute_percentage() {
+        let percent = Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(50),
+        };
+
+        // 4 YES, 7 NO, 2 ABSTAIN
+        let votes = Votes {
+            yes: Uint128::new(4),
+            no: Uint128::new(7),
+            abstain: Uint128::new(2),
+        };
+
+        // 15 total voting power
+        // 7 / (15 - 2) > 50%
+        // Expiry does not matter
+        assert!(check_is_rejected(
+            percent.clone(),
+            votes.clone(),
+            Uint128::new(15),
+            false
+        ));
+        assert!(check_is_rejected(
+            percent.clone(),
+            votes.clone(),
+            Uint128::new(15),
+            true
+        ));
+
+        // 17 total voting power
+        // 7 / (17 - 2) < 50%
+        assert!(!check_is_rejected(
+            percent.clone(),
+            votes.clone(),
+            Uint128::new(17),
+            false
+        ));
+        assert!(!check_is_rejected(
+            percent.clone(),
+            votes.clone(),
+            Uint128::new(17),
+            true
+        ));
+
+        // Rejected if total was lower
+        assert!(check_is_rejected(
+            percent.clone(),
+            votes.clone(),
+            Uint128::new(14),
+            false
+        ));
+        assert!(check_is_rejected(percent, votes, Uint128::new(14), true));
+    }
+
+    #[test]
+    fn proposal_passed_quorum() {
+        let quorum = Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(50),
+            quorum: Decimal::percent(40),
+        };
+        // all non-yes votes are counted for quorum
+        let passing = Votes {
+            yes: Uint128::new(7),
+            no: Uint128::new(3),
+            abstain: Uint128::new(2),
+        };
+        // abstain votes are not counted for threshold => yes / (yes + no + veto)
+        let passes_ignoring_abstain = Votes {
+            yes: Uint128::new(6),
+            no: Uint128::new(6),
+            abstain: Uint128::new(5),
+        };
+        // fails any way you look at it
+        let failing = Votes {
+            yes: Uint128::new(6),
+            no: Uint128::new(7),
+            abstain: Uint128::new(2),
+        };
+
+        // first, expired (voting period over)
+        // over quorum (40% of 30 = 12), over threshold (7/12 > 50%)
+        assert!(check_is_passed(
+            quorum.clone(),
+            passing.clone(),
+            Uint128::new(30),
+            true
+        ));
+        // under quorum it is not passing (40% of 33 = 13.2 > 13)
+        assert!(!check_is_passed(
+            quorum.clone(),
+            passing.clone(),
+            Uint128::new(33),
+            true
+        ));
+        // over quorum, threshold passes if we ignore abstain
+        // 17 total votes w/ abstain => 40% quorum of 40 total
+        // 6 yes / (6 yes + 4 no + 2 votes) => 50% threshold
+        assert!(check_is_passed(
+            quorum.clone(),
+            passes_ignoring_abstain.clone(),
+            Uint128::new(40),
+            true
+        ));
+        // over quorum, but under threshold fails also
+        assert!(!check_is_passed(
+            quorum.clone(),
+            failing,
+            Uint128::new(20),
+            true
+        ));
+
+        // now, check with open voting period
+        // would pass if closed, but fail here, as remaining votes no -> fail
+        assert!(!check_is_passed(
+            quorum.clone(),
+            passing.clone(),
+            Uint128::new(30),
+            false
+        ));
+        assert!(!check_is_passed(
+            quorum.clone(),
+            passes_ignoring_abstain.clone(),
+            Uint128::new(40),
+            false
+        ));
+        // if we have threshold * total_weight as yes votes this must pass
+        assert!(check_is_passed(
+            quorum.clone(),
+            passing.clone(),
+            Uint128::new(14),
+            false
+        ));
+        // all votes have been cast, some abstain
+        assert!(check_is_passed(
+            quorum.clone(),
+            passes_ignoring_abstain,
+            Uint128::new(17),
+            false
+        ));
+        // 3 votes uncast, if they all vote no, we have 7 yes, 7 no+veto, 2 abstain (out of 16)
+        assert!(check_is_passed(quorum, passing, Uint128::new(16), false));
+    }
+
+    #[test]
+    fn proposal_rejected_quorum() {
+        let quorum = Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(50),
+            quorum: Decimal::percent(40),
+        };
+        // all non-yes votes are counted for quorum
+        let rejecting = Votes {
+            yes: Uint128::new(3),
+            no: Uint128::new(8),
+            abstain: Uint128::new(2),
+        };
+        // abstain votes are not counted for threshold => yes / (yes + no)
+        let rejected_ignoring_abstain = Votes {
+            yes: Uint128::new(4),
+            no: Uint128::new(8),
+            abstain: Uint128::new(5),
+        };
+        // fails any way you look at it
+        let failing = Votes {
+            yes: Uint128::new(5),
+            no: Uint128::new(8),
+            abstain: Uint128::new(2),
+        };
+
+        // first, expired (voting period over)
+        // over quorum (40% of 30 = 12), over threshold (7/11 > 50%)
+        assert!(check_is_rejected(
+            quorum.clone(),
+            rejecting.clone(),
+            Uint128::new(30),
+            true
+        ));
+        // Under quorum means it cannot be rejected
+        assert!(!check_is_rejected(
+            quorum.clone(),
+            rejecting.clone(),
+            Uint128::new(33),
+            true
+        ));
+
+        // over quorum, threshold passes if we ignore abstain
+        // 17 total votes w/ abstain => 40% quorum of 40 total
+        // 6 no / (6 no + 4 yes + 2 votes) => 50% threshold
+        assert!(check_is_rejected(
+            quorum.clone(),
+            rejected_ignoring_abstain.clone(),
+            Uint128::new(40),
+            true
+        ));
+
+        // Over quorum, but under threshold fails if the proposal is
+        // not expired. If the proposal is expired though it passes as
+        // the total vote count used is the number of votes, and not
+        // the total number of votes avaliable.
+        assert!(check_is_rejected(
+            quorum.clone(),
+            failing.clone(),
+            Uint128::new(20),
+            true
+        ));
+        assert!(!check_is_rejected(
+            quorum.clone(),
+            failing,
+            Uint128::new(20),
+            false
+        ));
+
+        // Voting is still open so assume rest of votes are yes
+        // threshold not reached
+        assert!(!check_is_rejected(
+            quorum.clone(),
+            rejecting.clone(),
+            Uint128::new(30),
+            false
+        ));
+        assert!(!check_is_rejected(
+            quorum.clone(),
+            rejected_ignoring_abstain.clone(),
+            Uint128::new(40),
+            false
+        ));
+        // if we have threshold * total_weight as no votes this must reject
+        assert!(check_is_rejected(
+            quorum.clone(),
+            rejecting.clone(),
+            Uint128::new(14),
+            false
+        ));
+        // all votes have been cast, some abstain
+        assert!(check_is_rejected(
+            quorum.clone(),
+            rejected_ignoring_abstain,
+            Uint128::new(17),
+            false
+        ));
+        // 3 votes uncast, if they all vote yes, we have 7 no, 7 yes+veto, 2 abstain (out of 16)
+        assert!(check_is_rejected(
+            quorum,
+            rejecting,
+            Uint128::new(16),
+            false
+        ));
+    }
+
+    #[test]
+    fn quorum_edge_cases() {
+        // When we pass absolute threshold (everyone else voting no,
+        // we pass), but still don't hit quorum.
+        let quorum = Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(60),
+            quorum: Decimal::percent(80),
+        };
+
+        // Try 9 yes, 1 no (out of 15) -> 90% voter threshold, 60%
+        // absolute threshold, still no quorum doesn't matter if
+        // expired or not.
+        let missing_voters = Votes {
+            yes: Uint128::new(9),
+            no: Uint128::new(1),
+            abstain: Uint128::new(0),
+        };
+        assert!(!check_is_passed(
+            quorum.clone(),
+            missing_voters.clone(),
+            Uint128::new(15),
+            false
+        ));
+        assert!(!check_is_passed(
+            quorum.clone(),
+            missing_voters,
+            Uint128::new(15),
+            true
+        ));
+
+        // 1 less yes, 3 vetos and this passes only when expired.
+        let wait_til_expired = Votes {
+            yes: Uint128::new(8),
+            no: Uint128::new(4),
+            abstain: Uint128::new(0),
+        };
+        assert!(!check_is_passed(
+            quorum.clone(),
+            wait_til_expired.clone(),
+            Uint128::new(15),
+            false
+        ));
+        assert!(check_is_passed(
+            quorum.clone(),
+            wait_til_expired,
+            Uint128::new(15),
+            true
+        ));
+
+        // 9 yes and 3 nos passes early
+        let passes_early = Votes {
+            yes: Uint128::new(9),
+            no: Uint128::new(3),
+            abstain: Uint128::new(0),
+        };
+        assert!(check_is_passed(
+            quorum.clone(),
+            passes_early.clone(),
+            Uint128::new(15),
+            false
+        ));
+        assert!(check_is_passed(
+            quorum,
+            passes_early,
+            Uint128::new(15),
+            true
+        ));
+    }
+}
diff --git a/contracts/cw-govmod-single/src/query.rs b/contracts/cw-govmod-single/src/query.rs
new file mode 100644
index 00000000..57f48ca4
--- /dev/null
+++ b/contracts/cw-govmod-single/src/query.rs
@@ -0,0 +1,36 @@
+use cosmwasm_std::{Addr, Uint128};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::proposal::{Proposal, Vote};
+
+/// Information about a proposal returned by proposal queries.
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct ProposalResponse {
+    pub id: u64,
+    pub proposal: Proposal,
+}
+
+/// Information about a vote that was cast.
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct VoteInfo {
+    /// The address that voted.
+    pub voter: Addr,
+    /// Position on the vote.
+    pub vote: Vote,
+    /// The voting power behind the vote.
+    pub power: Uint128,
+}
+
+/// Information about a vote.
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct VoteResponse {
+    /// None if no such vote, Some otherwise.
+    pub vote: Option<VoteInfo>,
+}
+
+/// Information about the votes for a proposal.
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+pub struct VoteListResponse {
+    pub votes: Vec<VoteInfo>,
+}
diff --git a/contracts/cw-govmod-single/src/state.rs b/contracts/cw-govmod-single/src/state.rs
new file mode 100644
index 00000000..19d5ce7a
--- /dev/null
+++ b/contracts/cw-govmod-single/src/state.rs
@@ -0,0 +1,146 @@
+use cosmwasm_std::{to_binary, Addr, CosmosMsg, Deps, StdResult, Uint128, WasmMsg};
+use cw_storage_plus::{Item, Map};
+use cw_utils::Duration;
+
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::{
+    msg::{DepositInfo, DepositToken},
+    proposal::{Proposal, Status, Vote},
+    threshold::Threshold,
+};
+
+/// Counterpart to the `DepositInfo` struct which has been processed.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct CheckedDepositInfo {
+    /// The address of the cw20 token to be used for proposal
+    /// deposits.
+    pub token: Addr,
+    /// The number of tokens that must be deposited to create a
+    /// proposal.
+    pub deposit: Uint128,
+    /// If failed proposals should have their deposits refunded.
+    pub refund_failed_proposals: bool,
+}
+
+/// The governance module's configuration.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct Config {
+    /// The threshold a proposal must reach to complete.
+    pub threshold: Threshold,
+    /// The default maximum amount of time a proposal may be voted on
+    /// before expiring.
+    pub max_voting_period: Duration,
+    /// If set to true only members may execute passed
+    /// proposals. Otherwise, any address may execute a passed
+    /// proposal.
+    pub only_members_execute: bool,
+    /// The address of the DAO that this governance module is
+    /// associated with.
+    pub dao: Addr,
+    /// Information about the depost required to create a
+    /// proposal. None if no deposit is required, Some otherwise.
+    pub deposit_info: Option<CheckedDepositInfo>,
+}
+
+/// A vote cast for a proposal.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct Ballot {
+    /// The amount of voting power behind the vote.
+    pub power: Uint128,
+    /// The position.
+    pub vote: Vote,
+}
+
+pub const CONFIG: Item<Config> = Item::new("config");
+pub const PROPOSAL_COUNT: Item<u64> = Item::new("proposal_count");
+pub const PROPOSALS: Map<u64, Proposal> = Map::new("proposals");
+pub const BALLOTS: Map<(u64, Addr), Ballot> = Map::new("ballots");
+
+impl DepositInfo {
+    /// Converts deposit info into checked deposit info.
+    pub fn into_checked(self, deps: Deps, dao: Addr) -> StdResult<CheckedDepositInfo> {
+        let Self {
+            token,
+            deposit,
+            refund_failed_proposals,
+        } = self;
+        let token = match token {
+            DepositToken::Token(token) => deps.api.addr_validate(&token)?,
+            DepositToken::VotingModuleToken => {
+                let voting_module: Addr = deps
+                    .querier
+                    .query_wasm_smart(dao, &cw_governance::msg::QueryMsg::VotingModule {})?;
+                let token_addr: Addr = deps.querier.query_wasm_smart(
+                    voting_module,
+                    &cw_governance_interface::voting::Query::TokenContract {},
+                )?;
+                token_addr
+            }
+        };
+        // Make an info query as a smoke test that we are indeed
+        // working with a token here. We can't turbofish this
+        // type. See <https://github.com/rust-lang/rust/issues/83701>
+        let _info: cw20::TokenInfoResponse = deps
+            .querier
+            .query_wasm_smart(token.clone(), &cw20::Cw20QueryMsg::TokenInfo {})?;
+        Ok(CheckedDepositInfo {
+            token,
+            deposit,
+            refund_failed_proposals,
+        })
+    }
+}
+
+pub fn get_deposit_msg(
+    info: &Option<CheckedDepositInfo>,
+    contract: &Addr,
+    sender: &Addr,
+) -> StdResult<Vec<CosmosMsg>> {
+    match info {
+        Some(info) => {
+            if info.deposit.is_zero() {
+                Ok(vec![])
+            } else {
+                let transfer_msg = WasmMsg::Execute {
+                    contract_addr: info.token.to_string(),
+                    funds: vec![],
+                    msg: to_binary(&cw20::Cw20ExecuteMsg::TransferFrom {
+                        owner: sender.to_string(),
+                        recipient: contract.to_string(),
+                        amount: info.deposit,
+                    })?,
+                };
+                let transfer_msg: CosmosMsg = transfer_msg.into();
+                Ok(vec![transfer_msg])
+            }
+        }
+        None => Ok(vec![]),
+    }
+}
+
+pub fn get_return_deposit_msg(proposal: &Proposal) -> StdResult<Vec<CosmosMsg>> {
+    match &proposal.deposit_info {
+        Some(info) => {
+            // If it is closed only issue a refund if we are refunding
+            // failed proposals. Otherwise, the proposal is open and
+            // expired.
+            if proposal.status == Status::Rejected && !info.refund_failed_proposals {
+                return Ok(vec![]);
+            }
+
+            let transfer_msg = WasmMsg::Execute {
+                contract_addr: info.token.to_string(),
+                funds: vec![],
+                msg: to_binary(&cw20::Cw20ExecuteMsg::Transfer {
+                    recipient: proposal.proposer.to_string(),
+                    amount: info.deposit,
+                })?,
+            };
+            let transfer_msg: CosmosMsg = transfer_msg.into();
+            Ok(vec![transfer_msg])
+        }
+        None => Ok(vec![]),
+    }
+}
diff --git a/contracts/cw-govmod-single/src/threshold.rs b/contracts/cw-govmod-single/src/threshold.rs
new file mode 100644
index 00000000..21e8b12b
--- /dev/null
+++ b/contracts/cw-govmod-single/src/threshold.rs
@@ -0,0 +1,49 @@
+use cosmwasm_std::Decimal;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+use crate::ContractError;
+
+/// The ways a proposal may reach its passing / failing threshold.
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum Threshold {
+    /// Declares a percentage of the total weight that must cast Yes
+    /// votes in order for a proposal to pass.  See
+    /// `ThresholdResponse::AbsolutePercentage` in the cw3 spec for
+    /// details.
+    AbsolutePercentage { percentage: Decimal },
+
+    /// Declares a `quorum` of the total votes that must participate
+    /// in the election in order for the vote to be considered at all.
+    /// See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for
+    /// details.
+    ThresholdQuorum { threshold: Decimal, quorum: Decimal },
+}
+
+/// Asserts that the 0.0 < percent <= 1.0
+fn validate_percentage(percent: &Decimal) -> Result<(), ContractError> {
+    if percent.is_zero() {
+        Err(ContractError::ZeroThreshold {})
+    } else if *percent > Decimal::one() {
+        Err(ContractError::UnreachableThreshold {})
+    } else {
+        Ok(())
+    }
+}
+
+impl Threshold {
+    /// returns error if this is an unreachable value,
+    /// given a total weight of all members in the group
+    pub fn validate(&self) -> Result<(), ContractError> {
+        match self {
+            Threshold::AbsolutePercentage {
+                percentage: percentage_needed,
+            } => validate_percentage(percentage_needed),
+            Threshold::ThresholdQuorum { threshold, quorum } => {
+                validate_percentage(threshold)?;
+                validate_percentage(quorum)
+            }
+        }
+    }
+}
diff --git a/contracts/cw-govmod-single/src/utils.rs b/contracts/cw-govmod-single/src/utils.rs
new file mode 100644
index 00000000..1e9f1221
--- /dev/null
+++ b/contracts/cw-govmod-single/src/utils.rs
@@ -0,0 +1,26 @@
+use cosmwasm_std::{Addr, Deps, StdResult, Uint128};
+
+use cw_governance_interface::voting;
+
+pub fn get_voting_power(
+    deps: Deps,
+    address: Addr,
+    dao: Addr,
+    height: Option<u64>,
+) -> StdResult<Uint128> {
+    let response: voting::VotingPowerAtHeightResponse = deps.querier.query_wasm_smart(
+        dao,
+        &voting::Query::VotingPowerAtHeight {
+            address: address.to_string(),
+            height,
+        },
+    )?;
+    Ok(response.power)
+}
+
+pub fn get_total_power(deps: Deps, dao: Addr, height: Option<u64>) -> StdResult<Uint128> {
+    let response: voting::TotalPowerAtHeightResponse = deps
+        .querier
+        .query_wasm_smart(dao, &voting::Query::TotalPowerAtHeight { height })?;
+    Ok(response.power)
+}
diff --git a/contracts/cw20-balance-voting/src/msg.rs b/contracts/cw20-balance-voting/src/msg.rs
index 7162af43..b448f503 100644
--- a/contracts/cw20-balance-voting/src/msg.rs
+++ b/contracts/cw20-balance-voting/src/msg.rs
@@ -1,6 +1,6 @@
 use cw20::Cw20Coin;
 use cw20_base::msg::InstantiateMarketingInfo;
-use cw_governance_macros::voting_query;
+use cw_governance_macros::{token_query, voting_query};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
@@ -31,9 +31,8 @@ pub struct InstantiateMsg {
 #[serde(rename_all = "snake_case")]
 pub enum ExecuteMsg {}
 
+#[token_query]
 #[voting_query]
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
-pub enum QueryMsg {
-    TokenContract {},
-}
+pub enum QueryMsg {}


Test patch:
diff --git a/contracts/cw-govmod-single/src/tests.rs b/contracts/cw-govmod-single/src/tests.rs
new file mode 100644
index 00000000..3afd4f25
--- /dev/null
+++ b/contracts/cw-govmod-single/src/tests.rs
@@ -0,0 +1,1277 @@
+use rand::{prelude::SliceRandom, Rng};
+
+use cosmwasm_std::{to_binary, Addr, Decimal, Empty, Uint128};
+use cw20::Cw20Coin;
+use cw_multi_test::{App, Contract, ContractWrapper, Executor};
+
+use crate::{
+    msg::{DepositInfo, DepositToken, ExecuteMsg, InstantiateMsg, QueryMsg},
+    proposal::{Proposal, Status, Vote, Votes},
+    query::{ProposalResponse, VoteInfo, VoteResponse},
+    state::{CheckedDepositInfo, Config},
+    threshold::Threshold,
+};
+
+const CREATOR_ADDR: &str = "creator";
+
+fn cw20_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw20_base::contract::execute,
+        cw20_base::contract::instantiate,
+        cw20_base::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn single_govmod_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        crate::contract::execute,
+        crate::contract::instantiate,
+        crate::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn cw20_balances_voting() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw20_balance_voting::contract::execute,
+        cw20_balance_voting::contract::instantiate,
+        cw20_balance_voting::contract::query,
+    )
+    .with_reply(cw20_balance_voting::contract::reply);
+    Box::new(contract)
+}
+
+fn cw_gov_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw_governance::contract::execute,
+        cw_governance::contract::instantiate,
+        cw_governance::contract::query,
+    )
+    .with_reply(cw_governance::contract::reply);
+    Box::new(contract)
+}
+
+fn instantiate_governance(
+    app: &mut App,
+    code_id: u64,
+    msg: cw_governance::msg::InstantiateMsg,
+) -> Addr {
+    app.instantiate_contract(
+        code_id,
+        Addr::unchecked(CREATOR_ADDR),
+        &msg,
+        &[],
+        "cw-governance",
+        None,
+    )
+    .unwrap()
+}
+
+fn instantiate_with_default_governance(
+    app: &mut App,
+    code_id: u64,
+    msg: InstantiateMsg,
+    initial_balances: Option<Vec<Cw20Coin>>,
+) -> Addr {
+    let cw20_id = app.store_code(cw20_contract());
+    let governance_id = app.store_code(cw_gov_contract());
+    let votemod_id = app.store_code(cw20_balances_voting());
+
+    let initial_balances = initial_balances.unwrap_or_else(|| {
+        vec![Cw20Coin {
+            address: CREATOR_ADDR.to_string(),
+            amount: Uint128::new(100_000_000),
+        }]
+    });
+
+    let governance_instantiate = cw_governance::msg::InstantiateMsg {
+        name: "DAO DAO".to_string(),
+        description: "A DAO that builds DAOs".to_string(),
+        image_url: None,
+        voting_module_instantiate_info: cw_governance::msg::ModuleInstantiateInfo {
+            code_id: votemod_id,
+            msg: to_binary(&cw20_balance_voting::msg::InstantiateMsg {
+                token_info: cw20_balance_voting::msg::TokenInfo::New {
+                    code_id: cw20_id,
+                    label: "DAO DAO governance token".to_string(),
+                    name: "DAO".to_string(),
+                    symbol: "DAO".to_string(),
+                    decimals: 6,
+                    initial_balances,
+                    marketing: None,
+                },
+            })
+            .unwrap(),
+            admin: cw_governance::msg::Admin::GovernanceContract {},
+            label: "DAO DAO voting module".to_string(),
+        },
+        governance_modules_instantiate_info: vec![cw_governance::msg::ModuleInstantiateInfo {
+            code_id,
+            msg: to_binary(&msg).unwrap(),
+            admin: cw_governance::msg::Admin::GovernanceContract {},
+            label: "DAO DAO governance module".to_string(),
+        }],
+    };
+
+    instantiate_governance(app, governance_id, governance_instantiate)
+}
+
+#[test]
+fn test_propose() {
+    let mut app = App::default();
+    let govmod_id = app.store_code(single_govmod_contract());
+
+    let threshold = Threshold::AbsolutePercentage {
+        percentage: Decimal::percent(50),
+    };
+    let max_voting_period = cw_utils::Duration::Height(6);
+    let instantiate = InstantiateMsg {
+        threshold: threshold.clone(),
+        max_voting_period,
+        only_members_execute: false,
+        deposit_info: None,
+    };
+
+    let governance_addr =
+        instantiate_with_default_governance(&mut app, govmod_id, instantiate, None);
+    let governance_modules: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            governance_addr.clone(),
+            &cw_governance::msg::QueryMsg::GovernanceModules {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    // Check that the governance module has been configured correctly.
+    let config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single.clone(), &QueryMsg::Config {})
+        .unwrap();
+    let expected = Config {
+        threshold: threshold.clone(),
+        max_voting_period,
+        only_members_execute: false,
+        dao: governance_addr,
+        deposit_info: None,
+    };
+    assert_eq!(config, expected);
+
+    // Create a new proposal.
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        govmod_single.clone(),
+        &ExecuteMsg::Propose {
+            title: "A simple text proposal".to_string(),
+            description: "This is a simple text proposal".to_string(),
+            msgs: vec![],
+            latest: None,
+        },
+        &[],
+    )
+    .unwrap();
+
+    let created: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(govmod_single, &QueryMsg::Proposal { proposal_id: 1 })
+        .unwrap();
+    let current_block = app.block_info();
+    let expected = Proposal {
+        title: "A simple text proposal".to_string(),
+        description: "This is a simple text proposal".to_string(),
+        proposer: Addr::unchecked(CREATOR_ADDR),
+        start_height: current_block.height,
+        expiration: max_voting_period.after(&current_block),
+        threshold,
+        total_power: Uint128::new(100_000_000),
+        msgs: vec![],
+        status: crate::proposal::Status::Open,
+        votes: Votes::zero(),
+        deposit_info: None,
+    };
+
+    assert_eq!(created.proposal, expected);
+    assert_eq!(created.id, 1u64);
+}
+
+/// If a test vote should execute. Used for fuzzing and checking that
+/// votes after a proposal has completed aren't allowed.
+pub enum ShouldExecute {
+    /// This should execute.
+    Yes,
+    /// This should not execute.
+    No,
+    /// Doesn't matter.
+    Meh,
+}
+
+struct TestVote {
+    /// The address casting the vote.
+    voter: String,
+    /// Position on the vote.
+    position: Vote,
+    /// Voting power of the address.
+    weight: Uint128,
+    /// If this vote is expected to execute.
+    should_execute: ShouldExecute,
+}
+
+// Creates a proposal and then executes a series of votes on those
+// proposals. Asserts both that those votes execute as expected and
+// that the final status of the proposal is what is expected. Returns
+// the address of the governance contract that it has created so that
+// callers may do additional inspection of the contract's state.
+fn do_test_votes(
+    votes: Vec<TestVote>,
+    threshold: Threshold,
+    expected_status: Status,
+    total_supply: Option<Uint128>,
+    deposit_info: Option<DepositInfo>,
+) -> (App, Addr) {
+    let mut app = App::default();
+    let govmod_id = app.store_code(single_govmod_contract());
+
+    let mut initial_balances = votes
+        .iter()
+        .map(|TestVote { voter, weight, .. }| Cw20Coin {
+            address: voter.to_string(),
+            amount: *weight,
+        })
+        .collect::<Vec<Cw20Coin>>();
+    let initial_balances_supply = votes.iter().fold(Uint128::zero(), |p, n| p + n.weight);
+    let to_fill = total_supply.map(|total_supply| total_supply - initial_balances_supply);
+    if let Some(fill) = to_fill {
+        initial_balances.push(Cw20Coin {
+            address: "filler".to_string(),
+            amount: fill,
+        })
+    }
+
+    let proposer = match votes.first() {
+        Some(vote) => vote.voter.clone(),
+        None => panic!("do_test_votes must have at least one vote."),
+    };
+
+    let max_voting_period = cw_utils::Duration::Height(6);
+    let instantiate = InstantiateMsg {
+        threshold,
+        max_voting_period,
+        only_members_execute: false,
+        deposit_info,
+    };
+
+    let governance_addr = instantiate_with_default_governance(
+        &mut app,
+        govmod_id,
+        instantiate,
+        Some(initial_balances),
+    );
+
+    let governance_modules: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            governance_addr.clone(),
+            &cw_governance::msg::QueryMsg::GovernanceModules {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    // Allow a proposal deposit as needed.
+    let config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single.clone(), &QueryMsg::Config {})
+        .unwrap();
+    if let Some(CheckedDepositInfo {
+        ref token, deposit, ..
+    }) = config.deposit_info
+    {
+        app.execute_contract(
+            Addr::unchecked(&proposer),
+            token.clone(),
+            &cw20_base::msg::ExecuteMsg::IncreaseAllowance {
+                spender: govmod_single.to_string(),
+                amount: deposit,
+                expires: None,
+            },
+            &[],
+        )
+        .unwrap();
+    }
+
+    app.execute_contract(
+        Addr::unchecked(&proposer),
+        govmod_single.clone(),
+        &ExecuteMsg::Propose {
+            title: "A simple text proposal".to_string(),
+            description: "This is a simple text proposal".to_string(),
+            msgs: vec![],
+            latest: None,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Cast votes.
+    for vote in votes {
+        let TestVote {
+            voter,
+            position,
+            weight,
+            should_execute,
+        } = vote;
+        // Vote on the proposal.
+        let res = app.execute_contract(
+            Addr::unchecked(voter.clone()),
+            govmod_single.clone(),
+            &ExecuteMsg::Vote {
+                proposal_id: 1,
+                vote: position,
+            },
+            &[],
+        );
+        match should_execute {
+            ShouldExecute::Yes => {
+                assert!(res.is_ok());
+                // Check that the vote was recorded correctly.
+                let vote: VoteResponse = app
+                    .wrap()
+                    .query_wasm_smart(
+                        govmod_single.clone(),
+                        &QueryMsg::Vote {
+                            proposal_id: 1,
+                            voter: voter.clone(),
+                        },
+                    )
+                    .unwrap();
+                let expected = VoteResponse {
+                    vote: Some(VoteInfo {
+                        voter: Addr::unchecked(&voter),
+                        vote: position,
+                        power: match config.deposit_info {
+                            Some(CheckedDepositInfo { deposit, .. }) => {
+                                if proposer == voter {
+                                    weight - deposit
+                                } else {
+                                    weight
+                                }
+                            }
+                            None => weight,
+                        },
+                    }),
+                };
+                assert_eq!(vote, expected)
+            }
+            ShouldExecute::No => assert!(res.is_err()),
+            ShouldExecute::Meh => (),
+        }
+    }
+
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(govmod_single, &QueryMsg::Proposal { proposal_id: 1 })
+        .unwrap();
+
+    assert_eq!(proposal.proposal.status, expected_status);
+
+    (app, governance_addr)
+}
+
+#[test]
+fn test_vote_simple() {
+    do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::Yes,
+            weight: Uint128::new(10),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(100),
+        },
+        Status::Passed,
+        None,
+        None,
+    );
+
+    do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::No,
+            weight: Uint128::new(10),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(100),
+        },
+        Status::Rejected,
+        None,
+        None,
+    );
+}
+
+#[test]
+fn test_vote_no_overflow() {
+    // We should not overflow when computing passing thresholds even
+    // when there are 2^128 votes.
+    do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::Yes,
+            weight: Uint128::new(u128::max_value()),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(100),
+        },
+        Status::Passed,
+        None,
+        None,
+    );
+}
+
+#[test]
+fn test_vote_abstain_only() {
+    do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::Abstain,
+            weight: Uint128::new(u128::max_value()),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(100),
+        },
+        Status::Open,
+        None,
+        None,
+    );
+}
+
+#[test]
+fn test_no_double_votes() {
+    do_test_votes(
+        vec![
+            TestVote {
+                voter: "ekez".to_string(),
+                position: Vote::Abstain,
+                weight: Uint128::new(2),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "ekez".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(2),
+                should_execute: ShouldExecute::No,
+            },
+        ],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(100),
+        },
+        Status::Open,
+        None,
+        None,
+    );
+}
+
+/// The current behavior for passing proposals is that the first
+/// option to reach the threshold wins. For example, with a 50%
+/// passing threshold if 50% of voting power votes no that proposal
+/// fails even if the other 50% would have voted yes. The same goes if
+/// the yes and no were reversed.
+///
+/// TODO(zeke): is this the behavior that we want?
+#[test]
+fn test_close_votes() {
+    do_test_votes(
+        vec![
+            TestVote {
+                voter: "ekez".to_string(),
+                position: Vote::Abstain,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "keze".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(5),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "ezek".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(5),
+                should_execute: ShouldExecute::No,
+            },
+        ],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(50),
+        },
+        Status::Rejected,
+        None,
+        None,
+    );
+
+    do_test_votes(
+        vec![
+            TestVote {
+                voter: "ekez".to_string(),
+                position: Vote::Abstain,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "keze".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(5),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "ezek".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(5),
+                should_execute: ShouldExecute::No,
+            },
+        ],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(50),
+        },
+        Status::Passed,
+        None,
+        None,
+    );
+}
+
+/// Another test which demonstrates the trouble with our "first to
+/// reach threshold" method for determining the winner. In this case
+/// there are more no votes than yes votes but because yes votes are
+/// the first ones to reach the threshold after the quorum has been
+/// passed yes votes win.
+///
+/// This is a pretty nonsense passing threshold but helps demonstrate
+/// the issue well enough.
+#[test]
+fn test_close_votes_quorum() {
+    do_test_votes(
+        vec![
+            TestVote {
+                voter: "ekez".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "keze".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(5),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "ezek".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::No,
+            },
+        ],
+        Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(10),
+            quorum: Decimal::percent(50),
+        },
+        Status::Passed,
+        None,
+        None,
+    );
+}
+
+#[test]
+fn test_pass_threshold_not_quorum() {
+    do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::Yes,
+            weight: Uint128::new(59),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(50),
+            quorum: Decimal::percent(60),
+        },
+        Status::Open,
+        Some(Uint128::new(100)),
+        None,
+    );
+    do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::No,
+            weight: Uint128::new(59),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(50),
+            quorum: Decimal::percent(60),
+        },
+        Status::Open,
+        Some(Uint128::new(100)),
+        None,
+    );
+}
+
+#[test]
+fn test_pass_threshold_exactly_quorum() {
+    do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::Yes,
+            weight: Uint128::new(60),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(50),
+            quorum: Decimal::percent(60),
+        },
+        Status::Passed,
+        Some(Uint128::new(100)),
+        None,
+    );
+    do_test_votes(
+        vec![
+            TestVote {
+                voter: "ekez".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(59),
+                should_execute: ShouldExecute::Yes,
+            },
+            // This is an intersting one because in this case the no
+            // voter is actually incentivised not to vote. By voting
+            // they move the quorum over the threshold and pass the
+            // vote. In a DAO with sufficently involved stakeholders
+            // no voters should effectively never vote if there is a
+            // quorum higher than the threshold as it makes the
+            // passing threshold the quorum threshold.
+            TestVote {
+                voter: "keze".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(1),
+                should_execute: ShouldExecute::Yes,
+            },
+        ],
+        Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(50),
+            quorum: Decimal::percent(60),
+        },
+        Status::Passed,
+        Some(Uint128::new(100)),
+        None,
+    );
+    do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::No,
+            weight: Uint128::new(60),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::ThresholdQuorum {
+            threshold: Decimal::percent(50),
+            quorum: Decimal::percent(60),
+        },
+        Status::Rejected,
+        Some(Uint128::new(100)),
+        None,
+    );
+}
+
+/// Generate some random voting selections and make sure they behave
+/// as expected.
+#[test]
+fn fuzz_voting() {
+    let mut rng = rand::thread_rng();
+    let dist = rand::distributions::Uniform::<u64>::new(1, 200);
+    for _ in 0..25 {
+        let yes: Vec<u64> = (0..50).map(|_| rng.sample(&dist)).collect();
+        let no: Vec<u64> = (0..50).map(|_| rng.sample(&dist)).collect();
+
+        let yes_sum: u64 = yes.iter().sum();
+        let no_sum: u64 = no.iter().sum();
+        let expected_status = match yes_sum.cmp(&no_sum) {
+            std::cmp::Ordering::Less => Status::Rejected,
+            // Depends on which reaches the threshold first. Ignore for now.
+            std::cmp::Ordering::Equal => continue,
+            std::cmp::Ordering::Greater => Status::Passed,
+        };
+
+        let yes = yes.into_iter().enumerate().map(|(idx, weight)| TestVote {
+            voter: format!("yes_{}", idx),
+            position: Vote::Yes,
+            weight: Uint128::new(weight as u128),
+            should_execute: ShouldExecute::Meh,
+        });
+        let no = no.into_iter().enumerate().map(|(idx, weight)| TestVote {
+            voter: format!("no_{}", idx),
+            position: Vote::No,
+            weight: Uint128::new(weight as u128),
+            should_execute: ShouldExecute::Meh,
+        });
+        let mut votes = yes.chain(no).collect::<Vec<_>>();
+        votes.shuffle(&mut rng);
+
+        do_test_votes(
+            votes,
+            Threshold::AbsolutePercentage {
+                percentage: Decimal::percent(50),
+            },
+            expected_status,
+            None,
+            None,
+        );
+    }
+}
+
+/// Instantiate the contract and use the voting module's token
+/// contract as the proposal deposit token.
+#[test]
+fn test_voting_module_token_proposal_deposit_instantiate() {
+    let mut app = App::default();
+    let govmod_id = app.store_code(single_govmod_contract());
+
+    let threshold = Threshold::AbsolutePercentage {
+        percentage: Decimal::percent(50),
+    };
+    let max_voting_period = cw_utils::Duration::Height(6);
+    let instantiate = InstantiateMsg {
+        threshold,
+        max_voting_period,
+        only_members_execute: false,
+        deposit_info: Some(DepositInfo {
+            token: DepositToken::VotingModuleToken,
+            deposit: Uint128::new(1),
+            refund_failed_proposals: true,
+        }),
+    };
+
+    let governance_addr =
+        instantiate_with_default_governance(&mut app, govmod_id, instantiate, None);
+
+    let gov_state: cw_governance::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(governance_addr, &cw_governance::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let governance_modules = gov_state.governance_modules;
+    let voting_module = gov_state.voting_module;
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    let config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single, &QueryMsg::Config {})
+        .unwrap();
+    let expected_token: Addr = app
+        .wrap()
+        .query_wasm_smart(
+            voting_module,
+            &cw_governance_interface::voting::Query::TokenContract {},
+        )
+        .unwrap();
+
+    assert_eq!(
+        config.deposit_info,
+        Some(CheckedDepositInfo {
+            token: expected_token,
+            deposit: Uint128::new(1),
+            refund_failed_proposals: true
+        })
+    )
+}
+
+/// Instantiate the contract and use a cw20 unrealated to the voting
+/// module for the proposal deposit.
+#[test]
+fn test_different_token_proposal_deposit() {
+    let mut app = App::default();
+    let govmod_id = app.store_code(single_govmod_contract());
+    let cw20_id = app.store_code(cw20_contract());
+    let cw20_addr = app
+        .instantiate_contract(
+            cw20_id,
+            Addr::unchecked(CREATOR_ADDR),
+            &cw20_base::msg::InstantiateMsg {
+                name: "OAD OAD".to_string(),
+                symbol: "OAD".to_string(),
+                decimals: 6,
+                initial_balances: vec![],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "random-cw20",
+            None,
+        )
+        .unwrap();
+
+    let threshold = Threshold::AbsolutePercentage {
+        percentage: Decimal::percent(50),
+    };
+    let max_voting_period = cw_utils::Duration::Height(6);
+    let instantiate = InstantiateMsg {
+        threshold,
+        max_voting_period,
+        only_members_execute: false,
+        deposit_info: Some(DepositInfo {
+            token: DepositToken::Token(cw20_addr.to_string()),
+            deposit: Uint128::new(1),
+            refund_failed_proposals: true,
+        }),
+    };
+
+    instantiate_with_default_governance(&mut app, govmod_id, instantiate, None);
+}
+
+/// Try to instantiate the governance module with a non-cw20 as its
+/// proposal deposit token. This should error as the `TokenInfo {}`
+/// query ought to fail.
+#[test]
+#[should_panic(expected = "Error parsing into type cw20_balance_voting::msg::QueryMsg")]
+fn test_bad_token_proposal_deposit() {
+    let mut app = App::default();
+    let govmod_id = app.store_code(single_govmod_contract());
+    let cw20_id = app.store_code(cw20_contract());
+    let votemod_id = app.store_code(cw20_balances_voting());
+
+    let votemod_addr = app
+        .instantiate_contract(
+            votemod_id,
+            Addr::unchecked(CREATOR_ADDR),
+            &cw20_balance_voting::msg::InstantiateMsg {
+                token_info: cw20_balance_voting::msg::TokenInfo::New {
+                    code_id: cw20_id,
+                    label: "DAO DAO governance token".to_string(),
+                    name: "DAO".to_string(),
+                    symbol: "DAO".to_string(),
+                    decimals: 6,
+                    initial_balances: vec![Cw20Coin {
+                        address: CREATOR_ADDR.to_string(),
+                        amount: Uint128::new(1),
+                    }],
+                    marketing: None,
+                },
+            },
+            &[],
+            "random-vote-module",
+            None,
+        )
+        .unwrap();
+
+    let threshold = Threshold::AbsolutePercentage {
+        percentage: Decimal::percent(50),
+    };
+    let max_voting_period = cw_utils::Duration::Height(6);
+    let instantiate = InstantiateMsg {
+        threshold,
+        max_voting_period,
+        only_members_execute: false,
+        deposit_info: Some(DepositInfo {
+            token: DepositToken::Token(votemod_addr.to_string()),
+            deposit: Uint128::new(1),
+            refund_failed_proposals: true,
+        }),
+    };
+
+    instantiate_with_default_governance(&mut app, govmod_id, instantiate, None);
+}
+
+#[test]
+fn test_take_proposal_deposit() {
+    let mut app = App::default();
+    let govmod_id = app.store_code(single_govmod_contract());
+
+    let threshold = Threshold::AbsolutePercentage {
+        percentage: Decimal::percent(50),
+    };
+    let max_voting_period = cw_utils::Duration::Height(6);
+    let instantiate = InstantiateMsg {
+        threshold,
+        max_voting_period,
+        only_members_execute: false,
+        deposit_info: Some(DepositInfo {
+            token: DepositToken::VotingModuleToken,
+            deposit: Uint128::new(1),
+            refund_failed_proposals: true,
+        }),
+    };
+
+    let governance_addr = instantiate_with_default_governance(
+        &mut app,
+        govmod_id,
+        instantiate,
+        Some(vec![Cw20Coin {
+            address: "ekez".to_string(),
+            amount: Uint128::new(2),
+        }]),
+    );
+
+    let gov_state: cw_governance::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(governance_addr, &cw_governance::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let governance_modules = gov_state.governance_modules;
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    let govmod_config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single.clone(), &QueryMsg::Config {})
+        .unwrap();
+    let CheckedDepositInfo {
+        token,
+        deposit,
+        refund_failed_proposals,
+    } = govmod_config.deposit_info.unwrap();
+    assert!(refund_failed_proposals);
+    assert_eq!(deposit, Uint128::new(1));
+
+    // This should fail because we have not created an allowance for
+    // the proposal deposit.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        govmod_single.clone(),
+        &ExecuteMsg::Propose {
+            title: "A simple text proposal".to_string(),
+            description: "This is a simple text proposal".to_string(),
+            msgs: vec![],
+            latest: None,
+        },
+        &[],
+    )
+    .unwrap_err();
+
+    // Allow a proposal deposit.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        token.clone(),
+        &cw20_base::msg::ExecuteMsg::IncreaseAllowance {
+            spender: govmod_single.to_string(),
+            amount: Uint128::new(1),
+            expires: None,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Now we can create a proposal.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        govmod_single,
+        &ExecuteMsg::Propose {
+            title: "A simple text proposal".to_string(),
+            description: "This is a simple text proposal".to_string(),
+            msgs: vec![],
+            latest: None,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Check that our balance was deducted.
+    let balance: cw20::BalanceResponse = app
+        .wrap()
+        .query_wasm_smart(
+            token,
+            &cw20::Cw20QueryMsg::Balance {
+                address: "ekez".to_string(),
+            },
+        )
+        .unwrap();
+    assert_eq!(balance.balance, Uint128::new(1))
+}
+
+#[test]
+fn test_deposit_return_on_execute() {
+    // Will create a proposal and execute it, one token will be
+    // deposited to create said proposal, expectation is that the
+    // token is then returned once the proposal is executed.
+    let (mut app, governance_addr) = do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::Yes,
+            weight: Uint128::new(10),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(90),
+        },
+        Status::Passed,
+        None,
+        Some(DepositInfo {
+            token: DepositToken::VotingModuleToken,
+            deposit: Uint128::new(1),
+            refund_failed_proposals: false,
+        }),
+    );
+    let gov_state: cw_governance::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(governance_addr, &cw_governance::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let governance_modules = gov_state.governance_modules;
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    let govmod_config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single.clone(), &QueryMsg::Config {})
+        .unwrap();
+    let CheckedDepositInfo { token, .. } = govmod_config.deposit_info.unwrap();
+    let balance: cw20::BalanceResponse = app
+        .wrap()
+        .query_wasm_smart(
+            token.clone(),
+            &cw20::Cw20QueryMsg::Balance {
+                address: "ekez".to_string(),
+            },
+        )
+        .unwrap();
+
+    // Proposal has not been executed so deposit has not been
+    // refunded.
+    assert_eq!(balance.balance, Uint128::new(9));
+
+    // Execute the proposal, this should cause the deposit to be
+    // refunded.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        govmod_single,
+        &ExecuteMsg::Execute { proposal_id: 1 },
+        &[],
+    )
+    .unwrap();
+
+    let balance: cw20::BalanceResponse = app
+        .wrap()
+        .query_wasm_smart(
+            token,
+            &cw20::Cw20QueryMsg::Balance {
+                address: "ekez".to_string(),
+            },
+        )
+        .unwrap();
+
+    // Proposal has been executed so deposit has been refunded.
+    assert_eq!(balance.balance, Uint128::new(10));
+}
+
+#[test]
+fn test_close_open_proposal() {
+    let (mut app, governance_addr) = do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::No,
+            weight: Uint128::new(10),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(90),
+        },
+        Status::Open,
+        Some(Uint128::new(100)),
+        Some(DepositInfo {
+            token: DepositToken::VotingModuleToken,
+            deposit: Uint128::new(1),
+            refund_failed_proposals: true,
+        }),
+    );
+
+    let gov_state: cw_governance::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(governance_addr, &cw_governance::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let governance_modules = gov_state.governance_modules;
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    // Close the proposal, this should error as the proposal is still
+    // open and not expired.
+    app.execute_contract(
+        Addr::unchecked("keze"),
+        govmod_single.clone(),
+        &ExecuteMsg::Close { proposal_id: 1 },
+        &[],
+    )
+    .unwrap_err();
+
+    // Make the proposal expire.
+    app.update_block(|block| block.height += 10);
+
+    // Close the proposal, this should work as the proposal is now
+    // open and expired.
+    app.execute_contract(
+        Addr::unchecked("keze"),
+        govmod_single.clone(),
+        &ExecuteMsg::Close { proposal_id: 1 },
+        &[],
+    )
+    .unwrap();
+
+    // Check that a refund was issued.
+    let govmod_config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single, &QueryMsg::Config {})
+        .unwrap();
+    let CheckedDepositInfo { token, .. } = govmod_config.deposit_info.unwrap();
+    let balance: cw20::BalanceResponse = app
+        .wrap()
+        .query_wasm_smart(
+            token,
+            &cw20::Cw20QueryMsg::Balance {
+                address: "ekez".to_string(),
+            },
+        )
+        .unwrap();
+
+    // Proposal has not been closed so deposit has not been
+    // refunded.
+    assert_eq!(balance.balance, Uint128::new(10));
+}
+
+#[test]
+fn test_deposit_return_on_close() {
+    let (mut app, governance_addr) = do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::No,
+            weight: Uint128::new(10),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(90),
+        },
+        Status::Rejected,
+        None,
+        Some(DepositInfo {
+            token: DepositToken::VotingModuleToken,
+            deposit: Uint128::new(1),
+            refund_failed_proposals: true,
+        }),
+    );
+    let gov_state: cw_governance::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(governance_addr, &cw_governance::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let governance_modules = gov_state.governance_modules;
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    let govmod_config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single.clone(), &QueryMsg::Config {})
+        .unwrap();
+    let CheckedDepositInfo { token, .. } = govmod_config.deposit_info.unwrap();
+    let balance: cw20::BalanceResponse = app
+        .wrap()
+        .query_wasm_smart(
+            token.clone(),
+            &cw20::Cw20QueryMsg::Balance {
+                address: "ekez".to_string(),
+            },
+        )
+        .unwrap();
+
+    // Proposal has not been closed so deposit has not been
+    // refunded.
+    assert_eq!(balance.balance, Uint128::new(9));
+
+    // Close the proposal, this should cause the deposit to be
+    // refunded.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        govmod_single,
+        &ExecuteMsg::Close { proposal_id: 1 },
+        &[],
+    )
+    .unwrap();
+
+    let balance: cw20::BalanceResponse = app
+        .wrap()
+        .query_wasm_smart(
+            token,
+            &cw20::Cw20QueryMsg::Balance {
+                address: "ekez".to_string(),
+            },
+        )
+        .unwrap();
+
+    // Proposal has been closed so deposit has been refunded.
+    assert_eq!(balance.balance, Uint128::new(10));
+}
+
+#[test]
+fn test_no_return_if_no_refunds() {
+    let (mut app, governance_addr) = do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::No,
+            weight: Uint128::new(10),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: Decimal::percent(90),
+        },
+        Status::Rejected,
+        None,
+        Some(DepositInfo {
+            token: DepositToken::VotingModuleToken,
+            deposit: Uint128::new(1),
+            refund_failed_proposals: false,
+        }),
+    );
+    let gov_state: cw_governance::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(governance_addr, &cw_governance::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let governance_modules = gov_state.governance_modules;
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    let govmod_config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single.clone(), &QueryMsg::Config {})
+        .unwrap();
+    let CheckedDepositInfo { token, .. } = govmod_config.deposit_info.unwrap();
+
+    // Close the proposal, this should cause the deposit to be
+    // refunded.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        govmod_single,
+        &ExecuteMsg::Close { proposal_id: 1 },
+        &[],
+    )
+    .unwrap();
+
+    let balance: cw20::BalanceResponse = app
+        .wrap()
+        .query_wasm_smart(
+            token,
+            &cw20::Cw20QueryMsg::Balance {
+                address: "ekez".to_string(),
+            },
+        )
+        .unwrap();
+
+    // Proposal has been closed but deposit has not been refunded.
+    assert_eq!(balance.balance, Uint128::new(9));
+}