Repository: DA0-DA0/dao-contracts
Pull request #206 (2022-03-21 20:35:43)

Move voting and interface logic into packages.
This moves logic around voting (types / meets threshold functions) and the interface / macros packages into a `packages/` subfolder.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 6f904d2b..fea684d1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -243,6 +243,7 @@ dependencies = [
  "schemars",
  "serde",
  "thiserror",
+ "voting",
 ]
 
 [[package]]
@@ -1052,6 +1053,15 @@ version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
 
+[[package]]
+name = "voting"
+version = "0.1.0"
+dependencies = [
+ "cosmwasm-std",
+ "schemars",
+ "serde",
+]
+
 [[package]]
 name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
diff --git a/Cargo.toml b/Cargo.toml
index 1a793faa..24573ba2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
 [workspace]
-members = ["contracts/*"]
+members = ["contracts/*", "packages/*"]
 
 [profile.release.package.cw3-dao]
 codegen-units = 1
diff --git a/contracts/cw-governance/Cargo.toml b/contracts/cw-governance/Cargo.toml
index b9e9a415..9e37343b 100644
--- a/contracts/cw-governance/Cargo.toml
+++ b/contracts/cw-governance/Cargo.toml
@@ -37,8 +37,8 @@ cw-utils = "0.12"
 schemars = "0.8"
 serde = { version = "1.0", default-features = false, features = ["derive"] }
 thiserror = { version = "1.0" }
-cw-governance-interface = { version = "0.1.0", path = "../cw-governance-interface" }
-cw-governance-macros = { version = "0.1.0", path = "../cw-governance-macros" }
+cw-governance-interface = { version = "0.1.0", path = "../../packages/cw-governance-interface" }
+cw-governance-macros = { version = "0.1.0", path = "../../packages/cw-governance-macros" }
 
 [dev-dependencies]
 cosmwasm-schema = { version = "1.0.0-beta5" }
diff --git a/contracts/cw-govmod-single/Cargo.toml b/contracts/cw-govmod-single/Cargo.toml
index 4b04124d..d560a440 100644
--- a/contracts/cw-govmod-single/Cargo.toml
+++ b/contracts/cw-govmod-single/Cargo.toml
@@ -40,8 +40,9 @@ 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" }
+cw-governance-macros = { path = "../../packages/cw-governance-macros", version = "0.1.0" }
+cw-governance-interface = { version = "0.1.0", path = "../../packages/cw-governance-interface" }
+voting = { version = "0.1.0", path = "../../packages/voting" }
 
 [dev-dependencies]
 cosmwasm-schema = { version = "1.0.0-beta5" }
diff --git a/contracts/cw-govmod-single/src/contract.rs b/contracts/cw-govmod-single/src/contract.rs
index 096f828e..02045c9b 100644
--- a/contracts/cw-govmod-single/src/contract.rs
+++ b/contracts/cw-govmod-single/src/contract.rs
@@ -8,10 +8,12 @@ use cw2::set_contract_version;
 use cw_storage_plus::Bound;
 use cw_utils::{Duration, Expiration};
 
+use voting::{Vote, Votes};
+
 use crate::{
     error::ContractError,
     msg::{DepositInfo, ExecuteMsg, InstantiateMsg, QueryMsg},
-    proposal::{advance_proposal_id, Proposal, Status, Vote, Votes},
+    proposal::{advance_proposal_id, Proposal, Status},
     query::{ProposalResponse, VoteInfo, VoteListResponse, VoteResponse},
     state::{
         get_deposit_msg, get_return_deposit_msg, Ballot, Config, BALLOTS, CONFIG, PROPOSALS,
diff --git a/contracts/cw-govmod-single/src/msg.rs b/contracts/cw-govmod-single/src/msg.rs
index 03284f65..252851ed 100644
--- a/contracts/cw-govmod-single/src/msg.rs
+++ b/contracts/cw-govmod-single/src/msg.rs
@@ -3,7 +3,7 @@ use cw_utils::{Duration, Expiration};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
-use crate::{proposal::Vote, threshold::Threshold};
+use crate::threshold::Threshold;
 use cw_governance_macros::govmod_query;
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -74,7 +74,7 @@ pub enum ExecuteMsg {
         /// The ID of the proposal to vote on.
         proposal_id: u64,
         /// The senders position on the proposal.
-        vote: Vote,
+        vote: voting::Vote,
     },
     /// Causes the messages associated with a passed proposal to be
     /// executed by the DAO.
diff --git a/contracts/cw-govmod-single/src/proposal.rs b/contracts/cw-govmod-single/src/proposal.rs
index c572aa82..214bd14b 100644
--- a/contracts/cw-govmod-single/src/proposal.rs
+++ b/contracts/cw-govmod-single/src/proposal.rs
@@ -1,11 +1,8 @@
-use std::convert::TryInto;
-
-use cosmwasm_std::{
-    Addr, BlockInfo, CosmosMsg, Decimal, Empty, StdResult, Storage, Uint128, Uint256,
-};
+use cosmwasm_std::{Addr, BlockInfo, CosmosMsg, Empty, StdResult, Storage, Uint128};
 use cw_utils::Expiration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
+use voting::{votes_meet_threshold, Votes};
 
 use crate::{
     query::ProposalResponse,
@@ -13,10 +10,6 @@ use crate::{
     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)]
@@ -34,26 +27,6 @@ pub enum Status {
     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,
@@ -76,130 +49,12 @@ pub struct Proposal {
     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
@@ -297,81 +152,10 @@ impl std::fmt::Display for Status {
     }
 }
 
-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))
-        );
-    }
+    use cosmwasm_std::{testing::mock_env, Decimal};
 
     fn setup_prop(
         threshold: Threshold,
diff --git a/contracts/cw-govmod-single/src/query.rs b/contracts/cw-govmod-single/src/query.rs
index 57f48ca4..9d20124c 100644
--- a/contracts/cw-govmod-single/src/query.rs
+++ b/contracts/cw-govmod-single/src/query.rs
@@ -2,7 +2,9 @@ use cosmwasm_std::{Addr, Uint128};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
-use crate::proposal::{Proposal, Vote};
+use voting::Vote;
+
+use crate::proposal::Proposal;
 
 /// Information about a proposal returned by proposal queries.
 #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
diff --git a/contracts/cw-govmod-single/src/state.rs b/contracts/cw-govmod-single/src/state.rs
index 19d5ce7a..c9ec1ad0 100644
--- a/contracts/cw-govmod-single/src/state.rs
+++ b/contracts/cw-govmod-single/src/state.rs
@@ -5,9 +5,11 @@ use cw_utils::Duration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
+use voting::Vote;
+
 use crate::{
     msg::{DepositInfo, DepositToken},
-    proposal::{Proposal, Status, Vote},
+    proposal::{Proposal, Status},
     threshold::Threshold,
 };
 
diff --git a/contracts/cw-govmod-sudo/Cargo.toml b/contracts/cw-govmod-sudo/Cargo.toml
index 01b9f0e3..cf4734aa 100644
--- a/contracts/cw-govmod-sudo/Cargo.toml
+++ b/contracts/cw-govmod-sudo/Cargo.toml
@@ -37,8 +37,8 @@ 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" }
+cw-governance-macros = { version = "0.1.0", path = "../../packages/cw-governance-macros" }
+cw-governance-interface = { version = "0.1.0", path = "../../packages/cw-governance-interface" }
 
 [dev-dependencies]
 cosmwasm-schema = { version = "1.0.0-beta5" }
diff --git a/contracts/cw20-balance-voting/Cargo.toml b/contracts/cw20-balance-voting/Cargo.toml
index b00bc37f..58564a00 100644
--- a/contracts/cw20-balance-voting/Cargo.toml
+++ b/contracts/cw20-balance-voting/Cargo.toml
@@ -38,8 +38,8 @@ cw-utils = "0.12"
 schemars = "0.8"
 serde = { version = "1.0", default-features = false, features = ["derive"] }
 thiserror = { version = "1.0" }
-cw-governance-macros = { version = "0.1.0", path = "../cw-governance-macros" }
-cw-governance-interface = { version = "0.1.0", path = "../cw-governance-interface" }
+cw-governance-macros = { version = "0.1.0", path = "../../packages/cw-governance-macros" }
+cw-governance-interface = { version = "0.1.0", path = "../../packages/cw-governance-interface" }
 cw20-base = {  version = "0.12", features = ["library"] }
 
 [dev-dependencies]
diff --git a/contracts/cw-governance-interface/.cargo/config b/packages/cw-governance-interface/.cargo/config
similarity index 100%
rename from contracts/cw-governance-interface/.cargo/config
rename to packages/cw-governance-interface/.cargo/config
diff --git a/contracts/cw-governance-interface/Cargo.toml b/packages/cw-governance-interface/Cargo.toml
similarity index 81%
rename from contracts/cw-governance-interface/Cargo.toml
rename to packages/cw-governance-interface/Cargo.toml
index 82e1d5f0..cc652979 100644
--- a/contracts/cw-governance-interface/Cargo.toml
+++ b/packages/cw-governance-interface/Cargo.toml
@@ -7,7 +7,7 @@ edition = "2021"
 
 [dependencies]
 cosmwasm-std = { version = "1.0.0-beta5" }
-cw-governance-macros = { version = "0.1.0", path = "../cw-governance-macros"}
+cw-governance-macros = { version = "0.1.0", path = "../../packages/cw-governance-macros" }
 serde = { version = "1.0", default-features = false, features = ["derive"] }
 schemars = "0.8"
 cw2 = "0.12"
diff --git a/contracts/cw-governance-interface/examples/schema.rs b/packages/cw-governance-interface/examples/schema.rs
similarity index 100%
rename from contracts/cw-governance-interface/examples/schema.rs
rename to packages/cw-governance-interface/examples/schema.rs
diff --git a/contracts/cw-governance-interface/schema/voting_query.json b/packages/cw-governance-interface/schema/voting_query.json
similarity index 100%
rename from contracts/cw-governance-interface/schema/voting_query.json
rename to packages/cw-governance-interface/schema/voting_query.json
diff --git a/contracts/cw-governance-interface/src/lib.rs b/packages/cw-governance-interface/src/lib.rs
similarity index 100%
rename from contracts/cw-governance-interface/src/lib.rs
rename to packages/cw-governance-interface/src/lib.rs
diff --git a/contracts/cw-governance-interface/src/voting.rs b/packages/cw-governance-interface/src/voting.rs
similarity index 100%
rename from contracts/cw-governance-interface/src/voting.rs
rename to packages/cw-governance-interface/src/voting.rs
diff --git a/contracts/cw-governance-macros/.cargo/config b/packages/cw-governance-macros/.cargo/config
similarity index 100%
rename from contracts/cw-governance-macros/.cargo/config
rename to packages/cw-governance-macros/.cargo/config
diff --git a/contracts/cw-governance-macros/Cargo.toml b/packages/cw-governance-macros/Cargo.toml
similarity index 100%
rename from contracts/cw-governance-macros/Cargo.toml
rename to packages/cw-governance-macros/Cargo.toml
diff --git a/contracts/cw-governance-macros/examples/schema.rs b/packages/cw-governance-macros/examples/schema.rs
similarity index 100%
rename from contracts/cw-governance-macros/examples/schema.rs
rename to packages/cw-governance-macros/examples/schema.rs
diff --git a/contracts/cw-governance-macros/schema/voting_query.json b/packages/cw-governance-macros/schema/voting_query.json
similarity index 100%
rename from contracts/cw-governance-macros/schema/voting_query.json
rename to packages/cw-governance-macros/schema/voting_query.json
diff --git a/contracts/cw-governance-macros/src/lib.rs b/packages/cw-governance-macros/src/lib.rs
similarity index 100%
rename from contracts/cw-governance-macros/src/lib.rs
rename to packages/cw-governance-macros/src/lib.rs
diff --git a/contracts/cw-governance-macros/tests/govmod.rs b/packages/cw-governance-macros/tests/govmod.rs
similarity index 100%
rename from contracts/cw-governance-macros/tests/govmod.rs
rename to packages/cw-governance-macros/tests/govmod.rs
diff --git a/contracts/cw-governance-macros/tests/voting.rs b/packages/cw-governance-macros/tests/voting.rs
similarity index 100%
rename from contracts/cw-governance-macros/tests/voting.rs
rename to packages/cw-governance-macros/tests/voting.rs
diff --git a/packages/voting/Cargo.toml b/packages/voting/Cargo.toml
new file mode 100644
index 00000000..cab1eb1b
--- /dev/null
+++ b/packages/voting/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "voting"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+cosmwasm-std = { version = "1.0.0-beta5" }
+schemars = "0.8"
+serde = { version = "1.0", default-features = false, features = ["derive"] }
diff --git a/packages/voting/src/lib.rs b/packages/voting/src/lib.rs
new file mode 100644
index 00000000..281ff21a
--- /dev/null
+++ b/packages/voting/src/lib.rs
@@ -0,0 +1,259 @@
+use cosmwasm_std::{Decimal, Uint128, Uint256};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+// 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)]
+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,
+}
+
+/// Information about the number of votes needed to pass a proposal.
+#[derive(PartialEq, Clone, Debug)]
+pub 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.
+pub fn votes_needed(total_power: Uint128, passing_percentage: Decimal) -> VotesNeeded {
+    // If there are no votes possible then no threshold will ever be
+    // reachable except for the case where there is a zero percent
+    // passing percentage in which case all vote counts should pass.
+    //
+    // A zero percent passing threshold is not actually allowed in the
+    // `validate_percentage` function so we should really never hit
+    // that case.
+    if total_power.is_zero() && !passing_percentage.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)
+}
+
+/// Determines if a number of votes meets the provided threshold given
+/// the total number of votes outstanding.
+pub 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,
+    }
+}
+
+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 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::*;
+
+    #[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))
+        );
+    }
+
+    #[test]
+    fn tricky_vote_counts() {
+        let threshold = Decimal::percent(50);
+        for count in 1..50_000 {
+            assert!(votes_meet_threshold(
+                Uint128::new(count),
+                Uint128::new(count * 2),
+                threshold
+            ))
+        }
+        assert!(!votes_meet_threshold(
+            Uint128::new(0),
+            Uint128::new(0),
+            threshold
+        ));
+        assert!(votes_meet_threshold(
+            Uint128::zero(),
+            Uint128::new(1),
+            Decimal::percent(0)
+        ));
+        assert!(votes_meet_threshold(
+            Uint128::zero(),
+            Uint128::new(0),
+            Decimal::percent(0)
+        ))
+    }
+}


Test patch:
diff --git a/contracts/cw-govmod-single/src/tests.rs b/contracts/cw-govmod-single/src/tests.rs
index 3afd4f25..2c66db56 100644
--- a/contracts/cw-govmod-single/src/tests.rs
+++ b/contracts/cw-govmod-single/src/tests.rs
@@ -4,9 +4,11 @@ use cosmwasm_std::{to_binary, Addr, Decimal, Empty, Uint128};
 use cw20::Cw20Coin;
 use cw_multi_test::{App, Contract, ContractWrapper, Executor};
 
+use voting::{Vote, Votes};
+
 use crate::{
     msg::{DepositInfo, DepositToken, ExecuteMsg, InstantiateMsg, QueryMsg},
-    proposal::{Proposal, Status, Vote, Votes},
+    proposal::{Proposal, Status},
     query::{ProposalResponse, VoteInfo, VoteResponse},
     state::{CheckedDepositInfo, Config},
     threshold::Threshold,