Repository: DA0-DA0/dao-contracts
Pull request #293 (2022-05-20 21:13:28)

Allow revoting on proposals.
This PR adds a config option to the `cw_proposal_single` contract to allow revoting on proposals. If this config option is toggled:

- Proposals will not be passed or rejected until they have expired.
- Voters may cast new votes before the proposal expires.


Patch:
diff --git a/contracts/cw-proposal-single/schema/config_response.json b/contracts/cw-proposal-single/schema/config_response.json
index c9de7ef5..3bc0e0fa 100644
--- a/contracts/cw-proposal-single/schema/config_response.json
+++ b/contracts/cw-proposal-single/schema/config_response.json
@@ -4,12 +4,17 @@
   "description": "The governance module's configuration.",
   "type": "object",
   "required": [
+    "allow_revoting",
     "dao",
     "max_voting_period",
     "only_members_execute",
     "threshold"
   ],
   "properties": {
+    "allow_revoting": {
+      "description": "Allows changing votes before the proposal expires. If this is enabled proposals will not be able to complete early as final vote information is not known until the time of proposal expiration.",
+      "type": "boolean"
+    },
     "dao": {
       "description": "The address of the DAO that this governance module is associated with.",
       "allOf": [
@@ -203,6 +208,27 @@
             }
           },
           "additionalProperties": false
+        },
+        {
+          "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.",
+          "type": "object",
+          "required": [
+            "absolute_count"
+          ],
+          "properties": {
+            "absolute_count": {
+              "type": "object",
+              "required": [
+                "threshold"
+              ],
+              "properties": {
+                "threshold": {
+                  "$ref": "#/definitions/Uint128"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
         }
       ]
     },
diff --git a/contracts/cw-proposal-single/schema/execute_msg.json b/contracts/cw-proposal-single/schema/execute_msg.json
index d660d12e..c21e013c 100644
--- a/contracts/cw-proposal-single/schema/execute_msg.json
+++ b/contracts/cw-proposal-single/schema/execute_msg.json
@@ -21,17 +21,6 @@
               "description": "A description of the proposal.",
               "type": "string"
             },
-            "latest": {
-              "description": "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.",
-              "anyOf": [
-                {
-                  "$ref": "#/definitions/Expiration"
-                },
-                {
-                  "type": "null"
-                }
-              ]
-            },
             "msgs": {
               "description": "The messages that should be executed in response to this proposal passing.",
               "type": "array",
@@ -139,12 +128,17 @@
         "update_config": {
           "type": "object",
           "required": [
+            "allow_revoting",
             "dao",
             "max_voting_period",
             "only_members_execute",
             "threshold"
           ],
           "properties": {
+            "allow_revoting": {
+              "description": "Allows changing votes before the proposal expires. If this is enabled proposals will not be able to complete early as final vote information is not known until the time of proposal expiration.",
+              "type": "boolean"
+            },
             "dao": {
               "description": "The address if tge DAO that this governance module is associated with.",
               "type": "string"
@@ -568,52 +562,6 @@
       "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"
     },
-    "Expiration": {
-      "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
-      "oneOf": [
-        {
-          "description": "AtHeight will expire when `env.block.height` >= height",
-          "type": "object",
-          "required": [
-            "at_height"
-          ],
-          "properties": {
-            "at_height": {
-              "type": "integer",
-              "format": "uint64",
-              "minimum": 0.0
-            }
-          },
-          "additionalProperties": false
-        },
-        {
-          "description": "AtTime will expire when `env.block.time` >= time",
-          "type": "object",
-          "required": [
-            "at_time"
-          ],
-          "properties": {
-            "at_time": {
-              "$ref": "#/definitions/Timestamp"
-            }
-          },
-          "additionalProperties": false
-        },
-        {
-          "description": "Never will never expire. Used to express the empty variant",
-          "type": "object",
-          "required": [
-            "never"
-          ],
-          "properties": {
-            "never": {
-              "type": "object"
-            }
-          },
-          "additionalProperties": false
-        }
-      ]
-    },
     "PercentageThreshold": {
       "description": "A percentage of voting power that must vote yes for a proposal to pass. An example of why this is needed:\n\nIf a user specifies a 60% passing threshold, and there are 10 voters they likely expect that proposal to pass when there are 6 yes votes. This implies that the condition for passing should be `yes_votes >= total_votes * threshold`.\n\nWith this in mind, how should a user specify that they would like proposals to pass if the majority of voters choose yes? Selecting a 50% passing threshold with those rules doesn't properly cover that case as 5 voters voting yes out of 10 would pass the proposal. Selecting 50.0001% or or some variation of that also does not work as a very small yes vote which technically makes the majority yes may not reach that threshold.\n\nTo handle these cases we provide both a majority and percent option for all percentages. If majority is selected passing will be determined by `yes > total_votes * 0.5`. If percent is selected passing is determined by `yes >= total_votes * percent`.\n\nIn both of these cases a proposal with only abstain votes must fail. This requires a special case passing logic.",
       "oneOf": [
@@ -777,14 +725,27 @@
             }
           },
           "additionalProperties": false
-        }
-      ]
-    },
-    "Timestamp": {
-      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
-      "allOf": [
+        },
         {
-          "$ref": "#/definitions/Uint64"
+          "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.",
+          "type": "object",
+          "required": [
+            "absolute_count"
+          ],
+          "properties": {
+            "absolute_count": {
+              "type": "object",
+              "required": [
+                "threshold"
+              ],
+              "properties": {
+                "threshold": {
+                  "$ref": "#/definitions/Uint128"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
         }
       ]
     },
@@ -792,10 +753,6 @@
       "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
       "type": "string"
     },
-    "Uint64": {
-      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
-      "type": "string"
-    },
     "Vote": {
       "type": "string",
       "enum": [
diff --git a/contracts/cw-proposal-single/schema/instantiate_msg.json b/contracts/cw-proposal-single/schema/instantiate_msg.json
index 0ab932d2..a33699b6 100644
--- a/contracts/cw-proposal-single/schema/instantiate_msg.json
+++ b/contracts/cw-proposal-single/schema/instantiate_msg.json
@@ -3,11 +3,16 @@
   "title": "InstantiateMsg",
   "type": "object",
   "required": [
+    "allow_revoting",
     "max_voting_period",
     "only_members_execute",
     "threshold"
   ],
   "properties": {
+    "allow_revoting": {
+      "description": "Allows changing votes before the proposal expires. If this is enabled proposals will not be able to complete early as final vote information is not known until the time of proposal expiration.",
+      "type": "boolean"
+    },
     "deposit_info": {
       "description": "Information about the deposit required to create a proposal. None if there is no deposit requirement, Some otherwise.",
       "anyOf": [
@@ -228,6 +233,27 @@
             }
           },
           "additionalProperties": false
+        },
+        {
+          "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.",
+          "type": "object",
+          "required": [
+            "absolute_count"
+          ],
+          "properties": {
+            "absolute_count": {
+              "type": "object",
+              "required": [
+                "threshold"
+              ],
+              "properties": {
+                "threshold": {
+                  "$ref": "#/definitions/Uint128"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
         }
       ]
     },
diff --git a/contracts/cw-proposal-single/schema/list_proposals_response.json b/contracts/cw-proposal-single/schema/list_proposals_response.json
index 35a6d48e..909b0005 100644
--- a/contracts/cw-proposal-single/schema/list_proposals_response.json
+++ b/contracts/cw-proposal-single/schema/list_proposals_response.json
@@ -327,6 +327,7 @@
     "Proposal": {
       "type": "object",
       "required": [
+        "allow_revoting",
         "description",
         "expiration",
         "msgs",
@@ -339,6 +340,9 @@
         "votes"
       ],
       "properties": {
+        "allow_revoting": {
+          "type": "boolean"
+        },
         "deposit_info": {
           "description": "Information about the deposit that was sent as part of this proposal. None if no deposit.",
           "anyOf": [
@@ -547,6 +551,27 @@
             }
           },
           "additionalProperties": false
+        },
+        {
+          "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.",
+          "type": "object",
+          "required": [
+            "absolute_count"
+          ],
+          "properties": {
+            "absolute_count": {
+              "type": "object",
+              "required": [
+                "threshold"
+              ],
+              "properties": {
+                "threshold": {
+                  "$ref": "#/definitions/Uint128"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
         }
       ]
     },
diff --git a/contracts/cw-proposal-single/schema/proposal_response.json b/contracts/cw-proposal-single/schema/proposal_response.json
index 7deb8c1c..5d106697 100644
--- a/contracts/cw-proposal-single/schema/proposal_response.json
+++ b/contracts/cw-proposal-single/schema/proposal_response.json
@@ -330,6 +330,7 @@
     "Proposal": {
       "type": "object",
       "required": [
+        "allow_revoting",
         "description",
         "expiration",
         "msgs",
@@ -342,6 +343,9 @@
         "votes"
       ],
       "properties": {
+        "allow_revoting": {
+          "type": "boolean"
+        },
         "deposit_info": {
           "description": "Information about the deposit that was sent as part of this proposal. None if no deposit.",
           "anyOf": [
@@ -532,6 +536,27 @@
             }
           },
           "additionalProperties": false
+        },
+        {
+          "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.",
+          "type": "object",
+          "required": [
+            "absolute_count"
+          ],
+          "properties": {
+            "absolute_count": {
+              "type": "object",
+              "required": [
+                "threshold"
+              ],
+              "properties": {
+                "threshold": {
+                  "$ref": "#/definitions/Uint128"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
         }
       ]
     },
diff --git a/contracts/cw-proposal-single/schema/reverse_proposals_response.json b/contracts/cw-proposal-single/schema/reverse_proposals_response.json
index 51593dbe..d26586b2 100644
--- a/contracts/cw-proposal-single/schema/reverse_proposals_response.json
+++ b/contracts/cw-proposal-single/schema/reverse_proposals_response.json
@@ -327,6 +327,7 @@
     "Proposal": {
       "type": "object",
       "required": [
+        "allow_revoting",
         "description",
         "expiration",
         "msgs",
@@ -339,6 +340,9 @@
         "votes"
       ],
       "properties": {
+        "allow_revoting": {
+          "type": "boolean"
+        },
         "deposit_info": {
           "description": "Information about the deposit that was sent as part of this proposal. None if no deposit.",
           "anyOf": [
@@ -547,6 +551,27 @@
             }
           },
           "additionalProperties": false
+        },
+        {
+          "description": "An absolute number of votes needed for something to cross the threshold. Useful for multisig style voting.",
+          "type": "object",
+          "required": [
+            "absolute_count"
+          ],
+          "properties": {
+            "absolute_count": {
+              "type": "object",
+              "required": [
+                "threshold"
+              ],
+              "properties": {
+                "threshold": {
+                  "$ref": "#/definitions/Uint128"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
         }
       ]
     },
diff --git a/contracts/cw-proposal-single/src/contract.rs b/contracts/cw-proposal-single/src/contract.rs
index aa5c333d..43193b10 100644
--- a/contracts/cw-proposal-single/src/contract.rs
+++ b/contracts/cw-proposal-single/src/contract.rs
@@ -57,6 +57,7 @@ pub fn instantiate(
         only_members_execute: msg.only_members_execute,
         dao: dao.clone(),
         deposit_info,
+        allow_revoting: msg.allow_revoting,
     };
 
     CONFIG.save(deps.storage, &config)?;
@@ -86,6 +87,7 @@ pub fn execute(
             threshold,
             max_voting_period,
             only_members_execute,
+            allow_revoting,
             dao,
             deposit_info,
         } => execute_update_config(
@@ -94,6 +96,7 @@ pub fn execute(
             threshold,
             max_voting_period,
             only_members_execute,
+            allow_revoting,
             dao,
             deposit_info,
         ),
@@ -166,6 +169,7 @@ pub fn execute_propose(
             msgs,
             status: Status::Open,
             votes: Votes::zero(),
+            allow_revoting: config.allow_revoting,
             deposit_info: config.deposit_info.clone(),
         };
         // Update the proposal's status. Addresses case where proposal
@@ -297,11 +301,29 @@ pub fn execute_vote(
         return Err(ContractError::NotRegistered {});
     }
 
+    let mut previous_ballot = None;
     BALLOTS.update(
         deps.storage,
         (proposal_id, info.sender.clone()),
         |bal| match bal {
-            Some(_) => Err(ContractError::AlreadyVoted {}),
+            Some(current_ballot) => {
+                if prop.allow_revoting {
+                    if current_ballot.vote == vote {
+                        // Don't allow casting the same vote more than
+                        // once. This seems liable to be confusing
+                        // behavior.
+                        Err(ContractError::AlreadyCast {})
+                    } else {
+                        previous_ballot = Some(current_ballot);
+                        Ok(Ballot {
+                            power: vote_power,
+                            vote,
+                        })
+                    }
+                } else {
+                    Err(ContractError::AlreadyVoted {})
+                }
+            }
             None => Ok(Ballot {
                 power: vote_power,
                 vote,
@@ -310,9 +332,17 @@ pub fn execute_vote(
     )?;
 
     let old_status = prop.status;
+
+    // Remove the old vote if this is a re-vote.
+    if let Some(ballot) = previous_ballot {
+        prop.votes.remove_vote(ballot.vote, ballot.power)
+    }
+
     prop.votes.add_vote(vote, vote_power);
     prop.update_status(&env.block);
+
     PROPOSALS.save(deps.storage, proposal_id, &prop)?;
+
     let new_status = prop.status;
     let change_hooks = proposal_status_changed_hooks(
         PROPOSAL_HOOKS,
@@ -346,7 +376,8 @@ pub fn execute_close(
 ) -> Result<Response, ContractError> {
     let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
 
-    // Update status to ensure that proposals which were open and have expired are moved to "rejected."
+    // Update status to ensure that proposals which were open and have
+    // expired are moved to "rejected."
     prop.update_status(&env.block);
     if prop.status != Status::Rejected {
         return Err(ContractError::WrongCloseStatus {});
@@ -384,12 +415,14 @@ pub fn execute_close(
         .add_attribute("proposal_id", proposal_id.to_string()))
 }
 
+#[allow(clippy::too_many_arguments)]
 pub fn execute_update_config(
     deps: DepsMut,
     info: MessageInfo,
     threshold: Threshold,
     max_voting_period: Duration,
     only_members_execute: bool,
+    allow_revoting: bool,
     dao: String,
     deposit_info: Option<DepositInfo>,
 ) -> Result<Response, ContractError> {
@@ -412,6 +445,7 @@ pub fn execute_update_config(
             threshold,
             max_voting_period,
             only_members_execute,
+            allow_revoting,
             dao,
             deposit_info,
         },
diff --git a/contracts/cw-proposal-single/src/error.rs b/contracts/cw-proposal-single/src/error.rs
index b84e02ef..e1edd83a 100644
--- a/contracts/cw-proposal-single/src/error.rs
+++ b/contracts/cw-proposal-single/src/error.rs
@@ -36,9 +36,12 @@ pub enum ContractError {
     #[error("Not registered to vote (no voting power) at time of proposal creation.")]
     NotRegistered {},
 
-    #[error("Already voted")]
+    #[error("Already voted. This proposal does not support revoting.")]
     AlreadyVoted {},
 
+    #[error("Already cast a vote with that option. Change your vote to revote.")]
+    AlreadyCast {},
+
     #[error("Proposal is not in 'passed' state.")]
     NotPassed {},
 
diff --git a/contracts/cw-proposal-single/src/msg.rs b/contracts/cw-proposal-single/src/msg.rs
index 973f25b1..e7b55d7d 100644
--- a/contracts/cw-proposal-single/src/msg.rs
+++ b/contracts/cw-proposal-single/src/msg.rs
@@ -17,6 +17,11 @@ pub struct InstantiateMsg {
     /// proposals. Otherwise, any address may execute a passed
     /// proposal.
     pub only_members_execute: bool,
+    /// Allows changing votes before the proposal expires. If this is
+    /// enabled proposals will not be able to complete early as final
+    /// vote information is not known until the time of proposal
+    /// expiration.
+    pub allow_revoting: bool,
     /// Information about the deposit required to create a
     /// proposal. None if there is no deposit requirement, Some
     /// otherwise.
@@ -98,6 +103,11 @@ pub enum ExecuteMsg {
         /// proposals. Otherwise, any address may execute a passed
         /// proposal. Applies to all outstanding and future proposals.
         only_members_execute: bool,
+        /// Allows changing votes before the proposal expires. If this is
+        /// enabled proposals will not be able to complete early as final
+        /// vote information is not known until the time of proposal
+        /// expiration.
+        allow_revoting: bool,
         /// The address if tge DAO that this governance module is
         /// associated with.
         dao: String,
diff --git a/contracts/cw-proposal-single/src/proposal.rs b/contracts/cw-proposal-single/src/proposal.rs
index 2b04ab42..af064a35 100644
--- a/contracts/cw-proposal-single/src/proposal.rs
+++ b/contracts/cw-proposal-single/src/proposal.rs
@@ -25,6 +25,7 @@ pub struct Proposal {
 
     pub status: Status,
     pub votes: Votes,
+    pub allow_revoting: bool,
 
     /// Information about the deposit that was sent as part of this
     /// proposal. None if no deposit.
@@ -110,8 +111,14 @@ impl Proposal {
 
     /// Returns true iff this proposal is sure to pass (even before
     /// expiration if no future sequence of possible votes can cause
-    /// it to fail)
+    /// it to fail).
     pub fn is_passed(&self, block: &BlockInfo) -> bool {
+        // If re-voting is allowed nothing is known until the proposal
+        // has expired.
+        if self.allow_revoting && !self.expiration.is_expired(block) {
+            return false;
+        }
+
         match self.threshold {
             Threshold::AbsolutePercentage { percentage } => {
                 let options = self.total_power - self.votes.abstain;
@@ -134,12 +141,19 @@ impl Proposal {
                     does_vote_count_pass(self.votes.yes, options, threshold)
                 }
             }
+            Threshold::AbsoluteCount { threshold } => self.votes.yes >= threshold,
         }
     }
 
     /// As above for the passed check, used to check if a proposal is
     /// already rejected.
     pub fn is_rejected(&self, block: &BlockInfo) -> bool {
+        // If re-voting is allowed and the proposal is not expired no
+        // information is known.
+        if self.allow_revoting && !self.expiration.is_expired(block) {
+            return false;
+        }
+
         match self.threshold {
             Threshold::AbsolutePercentage {
                 percentage: percentage_needed,
@@ -235,6 +249,12 @@ impl Proposal {
                     (false, true) => true,
                 }
             }
+            Threshold::AbsoluteCount { threshold } => {
+                // If all the outstanding votes voting yes would not
+                // cause this proposal to pass then it is rejected.
+                let outstanding_votes = self.total_power - self.votes.total();
+                self.votes.yes + outstanding_votes < threshold
+            }
         }
     }
 }
@@ -249,6 +269,7 @@ mod test {
         votes: Votes,
         total_power: Uint128,
         is_expired: bool,
+        allow_revoting: bool,
     ) -> (Proposal, BlockInfo) {
         let block = mock_env().block;
         let expiration = match is_expired {
@@ -261,6 +282,7 @@ mod test {
             proposer: Addr::unchecked("test"),
             start_height: 100,
             expiration,
+            allow_revoting,
             msgs: vec![],
             status: Status::Open,
             threshold,
@@ -276,8 +298,9 @@ mod test {
         votes: Votes,
         total_power: Uint128,
         is_expired: bool,
+        allow_revoting: bool,
     ) -> bool {
-        let (prop, block) = setup_prop(threshold, votes, total_power, is_expired);
+        let (prop, block) = setup_prop(threshold, votes, total_power, is_expired, allow_revoting);
         prop.is_passed(&block)
     }
 
@@ -286,8 +309,9 @@ mod test {
         votes: Votes,
         total_power: Uint128,
         is_expired: bool,
+        allow_revoting: bool,
     ) -> bool {
-        let (prop, block) = setup_prop(threshold, votes, total_power, is_expired);
+        let (prop, block) = setup_prop(threshold, votes, total_power, is_expired, allow_revoting);
         prop.is_rejected(&block)
     }
 
@@ -308,18 +332,203 @@ mod test {
             threshold.clone(),
             votes.clone(),
             Uint128::new(15),
-            false
+            false,
+            false,
         ));
         // Proposal being expired should not effect those results.
         assert!(check_is_passed(
             threshold.clone(),
             votes.clone(),
             Uint128::new(15),
-            true
+            true,
+            false
         ));
 
         // More votes == higher threshold => not passed.
-        assert!(!check_is_passed(threshold, votes, Uint128::new(17), false));
+        assert!(!check_is_passed(
+            threshold,
+            votes,
+            Uint128::new(17),
+            false,
+            false
+        ));
+    }
+
+    #[test]
+    fn test_revoting_majority_no_pass() {
+        // Revoting being allowed means that proposals may not be
+        // passed or rejected before they expire.
+        let threshold = Threshold::AbsolutePercentage {
+            percentage: PercentageThreshold::Majority {},
+        };
+        let votes = Votes {
+            yes: Uint128::new(7),
+            no: Uint128::new(4),
+            abstain: Uint128::new(2),
+        };
+
+        // 15 total votes. 7 yes and 2 abstain. Majority threshold. This
+        // should pass but revoting is enabled.
+        assert!(!check_is_passed(
+            threshold.clone(),
+            votes.clone(),
+            Uint128::new(15),
+            false,
+            true,
+        ));
+        // Proposal being expired should cause the proposal to be
+        // passed as votes may no longer be cast.
+        assert!(check_is_passed(
+            threshold,
+            votes,
+            Uint128::new(15),
+            true,
+            true
+        ));
+    }
+
+    #[test]
+    fn test_revoting_majority_rejection() {
+        // Revoting being allowed means that proposals may not be
+        // passed or rejected before they expire.
+        let threshold = Threshold::AbsolutePercentage {
+            percentage: PercentageThreshold::Majority {},
+        };
+        let votes = Votes {
+            yes: Uint128::new(4),
+            no: Uint128::new(7),
+            abstain: Uint128::new(2),
+        };
+
+        // Not expired, revoting allowed => no rejection.
+        assert!(!check_is_rejected(
+            threshold.clone(),
+            votes.clone(),
+            Uint128::new(15),
+            false,
+            true
+        ));
+
+        // Expired, revoting allowed => rejection.
+        assert!(check_is_rejected(
+            threshold,
+            votes,
+            Uint128::new(15),
+            true,
+            true
+        ));
+    }
+
+    /// Simple checks for absolute count passing and failing
+    /// conditions.
+    #[test]
+    fn test_absolute_count_threshold() {
+        let threshold = Threshold::AbsoluteCount {
+            threshold: Uint128::new(10),
+        };
+
+        assert!(check_is_passed(
+            threshold.clone(),
+            Votes {
+                yes: Uint128::new(10),
+                no: Uint128::zero(),
+                abstain: Uint128::zero(),
+            },
+            Uint128::new(100),
+            false,
+            false
+        ));
+
+        assert!(check_is_rejected(
+            threshold.clone(),
+            Votes {
+                yes: Uint128::new(9),
+                no: Uint128::new(1),
+                abstain: Uint128::zero()
+            },
+            Uint128::new(10),
+            false,
+            false
+        ));
+
+        assert!(!check_is_rejected(
+            threshold.clone(),
+            Votes {
+                yes: Uint128::new(9),
+                no: Uint128::new(1),
+                abstain: Uint128::zero()
+            },
+            Uint128::new(11),
+            false,
+            false
+        ));
+
+        assert!(!check_is_passed(
+            threshold,
+            Votes {
+                yes: Uint128::new(9),
+                no: Uint128::new(1),
+                abstain: Uint128::zero()
+            },
+            Uint128::new(11),
+            false,
+            false
+        ));
+    }
+
+    /// Tests that revoting works as expected with an absolute count
+    /// style threshold.
+    #[test]
+    fn test_absolute_count_threshold_revoting() {
+        let threshold = Threshold::AbsoluteCount {
+            threshold: Uint128::new(10),
+        };
+
+        assert!(!check_is_passed(
+            threshold.clone(),
+            Votes {
+                yes: Uint128::new(10),
+                no: Uint128::zero(),
+                abstain: Uint128::zero(),
+            },
+            Uint128::new(100),
+            false,
+            true
+        ));
+        assert!(check_is_passed(
+            threshold.clone(),
+            Votes {
+                yes: Uint128::new(10),
+                no: Uint128::zero(),
+                abstain: Uint128::zero(),
+            },
+            Uint128::new(100),
+            true,
+            true
+        ));
+
+        assert!(!check_is_rejected(
+            threshold.clone(),
+            Votes {
+                yes: Uint128::new(9),
+                no: Uint128::new(1),
+                abstain: Uint128::zero()
+            },
+            Uint128::new(10),
+            false,
+            true
+        ));
+        assert!(check_is_rejected(
+            threshold,
+            Votes {
+                yes: Uint128::new(9),
+                no: Uint128::new(1),
+                abstain: Uint128::zero()
+            },
+            Uint128::new(10),
+            true,
+            true
+        ));
     }
 
     #[test]
@@ -332,7 +541,13 @@ mod test {
             no: Uint128::new(6),
             abstain: Uint128::zero(),
         };
-        assert!(check_is_passed(threshold, votes, Uint128::new(13), false))
+        assert!(check_is_passed(
+            threshold,
+            votes,
+            Uint128::new(13),
+            false,
+            false
+        ))
     }
 
     #[test]
@@ -349,12 +564,14 @@ mod test {
             threshold.clone(),
             votes.clone(),
             Uint128::new(13),
+            false,
             false
         ));
         assert!(!check_is_rejected(
             threshold,
             votes,
             Uint128::new(13),
+            false,
             false
         ));
     }
@@ -373,9 +590,16 @@ mod test {
             threshold.clone(),
             votes.clone(),
             Uint128::new(13),
+            false,
             false
         ));
-        assert!(!check_is_passed(threshold, votes, Uint128::new(14), false))
+        assert!(!check_is_passed(
+            threshold,
+            votes,
+            Uint128::new(14),
+            false,
+            false
+        ))
     }
 
     #[test]
@@ -398,13 +622,15 @@ mod test {
             percent.clone(),
             votes.clone(),
             Uint128::new(15),
-            false
+            false,
+            false,
         ));
         assert!(check_is_rejected(
             percent.clone(),
             votes.clone(),
             Uint128::new(15),
-            true
+            true,
+            false
         ));
 
         // 17 total voting power
@@ -413,13 +639,15 @@ mod test {
             percent.clone(),
             votes.clone(),
             Uint128::new(17),
+            false,
             false
         ));
         assert!(!check_is_rejected(
             percent.clone(),
             votes.clone(),
             Uint128::new(17),
-            true
+            true,
+            false
         ));
 
         // Rejected if total was lower
@@ -427,9 +655,16 @@ mod test {
             percent.clone(),
             votes.clone(),
             Uint128::new(14),
+            false,
+            false
+        ));
+        assert!(check_is_rejected(
+            percent,
+            votes,
+            Uint128::new(14),
+            true,
             false
         ));
-        assert!(check_is_rejected(percent, votes, Uint128::new(14), true));
     }
 
     #[test]
@@ -463,14 +698,16 @@ mod test {
             quorum.clone(),
             passing.clone(),
             Uint128::new(30),
-            true
+            true,
+            false,
         ));
         // under quorum it is not passing (40% of 33 = 13.2 > 13)
         assert!(!check_is_passed(
             quorum.clone(),
             passing.clone(),
             Uint128::new(33),
-            true
+            true,
+            false
         ));
         // over quorum, threshold passes if we ignore abstain
         // 17 total votes w/ abstain => 40% quorum of 40 total
@@ -479,14 +716,16 @@ mod test {
             quorum.clone(),
             passes_ignoring_abstain.clone(),
             Uint128::new(40),
-            true
+            true,
+            false,
         ));
         // over quorum, but under threshold fails also
         assert!(!check_is_passed(
             quorum.clone(),
             failing,
             Uint128::new(20),
-            true
+            true,
+            false
         ));
 
         // now, check with open voting period
@@ -495,12 +734,14 @@ mod test {
             quorum.clone(),
             passing.clone(),
             Uint128::new(30),
+            false,
             false
         ));
         assert!(!check_is_passed(
             quorum.clone(),
             passes_ignoring_abstain.clone(),
             Uint128::new(40),
+            false,
             false
         ));
         // if we have threshold * total_weight as yes votes this must pass
@@ -508,6 +749,7 @@ mod test {
             quorum.clone(),
             passing.clone(),
             Uint128::new(14),
+            false,
             false
         ));
         // all votes have been cast, some abstain
@@ -515,10 +757,17 @@ mod test {
             quorum.clone(),
             passes_ignoring_abstain,
             Uint128::new(17),
+            false,
             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));
+        assert!(check_is_passed(
+            quorum,
+            passing,
+            Uint128::new(16),
+            false,
+            false
+        ));
     }
 
     #[test]
@@ -552,7 +801,8 @@ mod test {
             quorum.clone(),
             rejecting.clone(),
             Uint128::new(30),
-            true
+            true,
+            false
         ));
         // Total power of 33. 13 total votes. 8 no votes, 3 yes, 2
         // abstain. 39.3% turnout. Expired. As it is expired we see if
@@ -562,7 +812,8 @@ mod test {
             quorum.clone(),
             rejecting.clone(),
             Uint128::new(33),
-            true
+            true,
+            false
         ));
 
         // over quorum, threshold passes if we ignore abstain
@@ -572,7 +823,8 @@ mod test {
             quorum.clone(),
             rejected_ignoring_abstain.clone(),
             Uint128::new(40),
-            true
+            true,
+            false
         ));
 
         // Over quorum, but under threshold fails if the proposal is
@@ -583,12 +835,14 @@ mod test {
             quorum.clone(),
             failing.clone(),
             Uint128::new(20),
-            true
+            true,
+            false
         ));
         assert!(!check_is_rejected(
             quorum.clone(),
             failing,
             Uint128::new(20),
+            false,
             false
         ));
 
@@ -598,12 +852,14 @@ mod test {
             quorum.clone(),
             rejecting.clone(),
             Uint128::new(30),
+            false,
             false
         ));
         assert!(!check_is_rejected(
             quorum.clone(),
             rejected_ignoring_abstain.clone(),
             Uint128::new(40),
+            false,
             false
         ));
         // if we have threshold * total_weight as no votes this must reject
@@ -611,6 +867,7 @@ mod test {
             quorum.clone(),
             rejecting.clone(),
             Uint128::new(14),
+            false,
             false
         ));
         // all votes have been cast, some abstain
@@ -618,6 +875,7 @@ mod test {
             quorum.clone(),
             rejected_ignoring_abstain,
             Uint128::new(17),
+            false,
             false
         ));
         // 3 votes uncast, if they all vote yes, we have 7 no, 7 yes+veto, 2 abstain (out of 16)
@@ -625,6 +883,7 @@ mod test {
             quorum,
             rejecting,
             Uint128::new(16),
+            false,
             false
         ));
     }
@@ -650,13 +909,15 @@ mod test {
             quorum.clone(),
             missing_voters.clone(),
             Uint128::new(15),
+            false,
             false
         ));
         assert!(!check_is_passed(
             quorum.clone(),
             missing_voters,
             Uint128::new(15),
-            true
+            true,
+            false
         ));
 
         // 1 less yes, 3 vetos and this passes only when expired.
@@ -669,13 +930,15 @@ mod test {
             quorum.clone(),
             wait_til_expired.clone(),
             Uint128::new(15),
+            false,
             false
         ));
         assert!(check_is_passed(
             quorum.clone(),
             wait_til_expired,
             Uint128::new(15),
-            true
+            true,
+            false
         ));
 
         // 9 yes and 3 nos passes early
@@ -688,13 +951,15 @@ mod test {
             quorum.clone(),
             passes_early.clone(),
             Uint128::new(15),
+            false,
             false
         ));
         assert!(check_is_passed(
             quorum,
             passes_early,
             Uint128::new(15),
-            true
+            true,
+            false
         ));
     }
 }
diff --git a/contracts/cw-proposal-single/src/state.rs b/contracts/cw-proposal-single/src/state.rs
index bc814b09..56949556 100644
--- a/contracts/cw-proposal-single/src/state.rs
+++ b/contracts/cw-proposal-single/src/state.rs
@@ -38,6 +38,11 @@ pub struct Config {
     /// proposals. Otherwise, any address may execute a passed
     /// proposal.
     pub only_members_execute: bool,
+    /// Allows changing votes before the proposal expires. If this is
+    /// enabled proposals will not be able to complete early as final
+    /// vote information is not known until the time of proposal
+    /// expiration.
+    pub allow_revoting: bool,
     /// The address of the DAO that this governance module is
     /// associated with.
     pub dao: Addr,
diff --git a/packages/voting/src/threshold.rs b/packages/voting/src/threshold.rs
index 20a3eab6..29ae8642 100644
--- a/packages/voting/src/threshold.rs
+++ b/packages/voting/src/threshold.rs
@@ -1,4 +1,4 @@
-use cosmwasm_std::Decimal;
+use cosmwasm_std::{Decimal, Uint128};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
@@ -64,6 +64,10 @@ pub enum Threshold {
         threshold: PercentageThreshold,
         quorum: PercentageThreshold,
     },
+
+    /// An absolute number of votes needed for something to cross the
+    /// threshold. Useful for multisig style voting.
+    AbsoluteCount { threshold: Uint128 },
 }
 
 /// Asserts that the 0.0 < percent <= 1.0
@@ -106,6 +110,13 @@ impl Threshold {
                 validate_percentage(threshold)?;
                 validate_quorum(quorum)
             }
+            Threshold::AbsoluteCount { threshold } => {
+                if threshold.is_zero() {
+                    Err(ThresholdError::ZeroThreshold {})
+                } else {
+                    Ok(())
+                }
+            }
         }
     }
 }
diff --git a/packages/voting/src/voting.rs b/packages/voting/src/voting.rs
index 184d2213..96578357 100644
--- a/packages/voting/src/voting.rs
+++ b/packages/voting/src/voting.rs
@@ -113,6 +113,17 @@ impl Votes {
         }
     }
 
+    /// Removes a vote from the votes. The vote being removed must
+    /// have been previously added or this method will cause an
+    /// overflow.
+    pub fn remove_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


Test patch:
diff --git a/contracts/cw-proposal-single/src/staking_tests.rs b/contracts/cw-proposal-single/src/staking_tests.rs
index 73d1fb61..e4dc4f7d 100644
--- a/contracts/cw-proposal-single/src/staking_tests.rs
+++ b/contracts/cw-proposal-single/src/staking_tests.rs
@@ -109,6 +109,7 @@ fn instantiate_with_staked_balances_voting() {
                 },
                 max_voting_period: Duration::Height(10u64),
                 only_members_execute: true,
+                allow_revoting: false,
                 deposit_info: None,
             })
             .unwrap(),
diff --git a/contracts/cw-proposal-single/src/tests.rs b/contracts/cw-proposal-single/src/tests.rs
index 6cd1cbfc..2b597d56 100644
--- a/contracts/cw-proposal-single/src/tests.rs
+++ b/contracts/cw-proposal-single/src/tests.rs
@@ -1,3 +1,5 @@
+use std::u128;
+
 use cosmwasm_std::{to_binary, Addr, Decimal, Empty, Uint128};
 use cw20::Cw20Coin;
 use cw20_staked_balance_voting::msg::ActiveThreshold;
@@ -16,6 +18,7 @@ use crate::{
     proposal::Proposal,
     query::{ProposalListResponse, ProposalResponse, VoteInfo, VoteResponse},
     state::{CheckedDepositInfo, Config},
+    ContractError,
 };
 
 const CREATOR_ADDR: &str = "creator";
@@ -38,7 +41,7 @@ fn cw20_stake_contract() -> Box<dyn Contract<Empty>> {
     Box::new(contract)
 }
 
-fn single_govmod_contract() -> Box<dyn Contract<Empty>> {
+fn single_proposal_contract() -> Box<dyn Contract<Empty>> {
     let contract = ContractWrapper::new(
         crate::contract::execute,
         crate::contract::instantiate,
@@ -515,7 +518,7 @@ where
     F: Fn(&mut App, u64, InstantiateMsg, Option<Vec<Cw20Coin>>) -> Addr,
 {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
 
     let mut initial_balances = votes
         .iter()
@@ -543,6 +546,7 @@ where
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info,
     };
 
@@ -687,7 +691,7 @@ fn do_test_votes_cw20_balances(
 #[test]
 fn test_propose() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
 
     let threshold = Threshold::AbsolutePercentage {
         percentage: PercentageThreshold::Majority {},
@@ -697,6 +701,7 @@ fn test_propose() {
         threshold: threshold.clone(),
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: None,
     };
 
@@ -725,6 +730,7 @@ fn test_propose() {
         threshold: threshold.clone(),
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         dao: governance_addr,
         deposit_info: None,
     };
@@ -755,6 +761,7 @@ fn test_propose() {
         start_height: current_block.height,
         expiration: max_voting_period.after(&current_block),
         threshold,
+        allow_revoting: false,
         total_power: Uint128::new(100_000_000),
         msgs: vec![],
         status: Status::Open,
@@ -861,7 +868,7 @@ fn fuzz_voting() {
 #[test]
 fn test_voting_module_token_proposal_deposit_instantiate() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
 
     let threshold = Threshold::AbsolutePercentage {
         percentage: PercentageThreshold::Majority {},
@@ -871,6 +878,7 @@ fn test_voting_module_token_proposal_deposit_instantiate() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: Some(DepositInfo {
             token: DepositToken::VotingModuleToken {},
             deposit: Uint128::new(1),
@@ -918,7 +926,7 @@ fn test_voting_module_token_proposal_deposit_instantiate() {
 #[test]
 fn test_different_token_proposal_deposit() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
     let cw20_id = app.store_code(cw20_contract());
     let cw20_addr = app
         .instantiate_contract(
@@ -946,6 +954,7 @@ fn test_different_token_proposal_deposit() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: Some(DepositInfo {
             token: DepositToken::Token {
                 address: cw20_addr.to_string(),
@@ -965,7 +974,7 @@ fn test_different_token_proposal_deposit() {
 #[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 govmod_id = app.store_code(single_proposal_contract());
     let cw20_id = app.store_code(cw20_contract());
     let votemod_id = app.store_code(cw20_balances_voting());
 
@@ -1001,6 +1010,7 @@ fn test_bad_token_proposal_deposit() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: Some(DepositInfo {
             token: DepositToken::Token {
                 address: votemod_addr.to_string(),
@@ -1016,7 +1026,7 @@ fn test_bad_token_proposal_deposit() {
 #[test]
 fn test_take_proposal_deposit() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
 
     let threshold = Threshold::AbsolutePercentage {
         percentage: PercentageThreshold::Majority {},
@@ -1026,6 +1036,7 @@ fn test_take_proposal_deposit() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: Some(DepositInfo {
             token: DepositToken::VotingModuleToken {},
             deposit: Uint128::new(1),
@@ -1363,7 +1374,7 @@ fn test_deposit_return_on_close() {
 #[test]
 fn test_execute_expired_proposal() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
     let core_addr = instantiate_with_staked_balances_governance(
         &mut app,
         govmod_id,
@@ -1374,6 +1385,7 @@ fn test_execute_expired_proposal() {
             },
             max_voting_period: Duration::Height(10),
             only_members_execute: true,
+            allow_revoting: false,
             deposit_info: None,
         },
         Some(vec![
@@ -1533,6 +1545,7 @@ fn test_update_config() {
             },
             max_voting_period: cw_utils::Duration::Height(10),
             only_members_execute: false,
+            allow_revoting: false,
             dao: CREATOR_ADDR.to_string(),
             deposit_info: None,
         },
@@ -1550,6 +1563,7 @@ fn test_update_config() {
             },
             max_voting_period: cw_utils::Duration::Height(10),
             only_members_execute: false,
+            allow_revoting: false,
             dao: CREATOR_ADDR.to_string(),
             deposit_info: None,
         },
@@ -1568,6 +1582,7 @@ fn test_update_config() {
         },
         max_voting_period: cw_utils::Duration::Height(10),
         only_members_execute: false,
+        allow_revoting: false,
         dao: Addr::unchecked(CREATOR_ADDR),
         deposit_info: None,
     };
@@ -1584,6 +1599,7 @@ fn test_update_config() {
             },
             max_voting_period: cw_utils::Duration::Height(10),
             only_members_execute: false,
+            allow_revoting: false,
             dao: CREATOR_ADDR.to_string(),
             deposit_info: None,
         },
@@ -1654,7 +1670,7 @@ fn test_no_return_if_no_refunds() {
 #[test]
 fn test_query_list_proposals() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
     let gov_addr = instantiate_with_cw20_balances_governance(
         &mut app,
         govmod_id,
@@ -1665,6 +1681,7 @@ fn test_query_list_proposals() {
             },
             max_voting_period: cw_utils::Duration::Height(100),
             only_members_execute: true,
+            allow_revoting: false,
             deposit_info: None,
         },
         Some(vec![Cw20Coin {
@@ -1738,6 +1755,7 @@ fn test_query_list_proposals() {
                 threshold: PercentageThreshold::Majority {},
                 quorum: PercentageThreshold::Percent(Decimal::percent(0)),
             },
+            allow_revoting: false,
             total_power: Uint128::new(100),
             msgs: vec![],
             status: Status::Open,
@@ -1781,6 +1799,7 @@ fn test_query_list_proposals() {
                 threshold: PercentageThreshold::Majority {},
                 quorum: PercentageThreshold::Percent(Decimal::percent(0)),
             },
+            allow_revoting: false,
             total_power: Uint128::new(100),
             msgs: vec![],
             status: Status::Open,
@@ -1798,7 +1817,7 @@ fn test_query_list_proposals() {
 #[test]
 fn test_hooks() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
 
     let threshold = Threshold::AbsolutePercentage {
         percentage: PercentageThreshold::Majority {},
@@ -1808,6 +1827,7 @@ fn test_hooks() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: None,
     };
 
@@ -1970,7 +1990,7 @@ fn test_hooks() {
 #[test]
 fn test_active_threshold_absolute() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
 
     let threshold = Threshold::AbsolutePercentage {
         percentage: PercentageThreshold::Majority {},
@@ -1980,6 +2000,7 @@ fn test_active_threshold_absolute() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: None,
     };
 
@@ -2094,7 +2115,7 @@ fn test_active_threshold_absolute() {
 #[test]
 fn test_active_threshold_percent() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
 
     let threshold = Threshold::AbsolutePercentage {
         percentage: PercentageThreshold::Majority {},
@@ -2104,6 +2125,7 @@ fn test_active_threshold_percent() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: None,
     };
 
@@ -2219,7 +2241,7 @@ fn test_active_threshold_percent() {
 #[test]
 fn test_active_threshold_none() {
     let mut app = App::default();
-    let govmod_id = app.store_code(single_govmod_contract());
+    let govmod_id = app.store_code(single_proposal_contract());
 
     let threshold = Threshold::AbsolutePercentage {
         percentage: PercentageThreshold::Majority {},
@@ -2229,6 +2251,7 @@ fn test_active_threshold_none() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: None,
     };
 
@@ -2306,6 +2329,7 @@ fn test_active_threshold_none() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: None,
     };
 
@@ -2339,3 +2363,898 @@ fn test_active_threshold_none() {
         )
         .unwrap();
 }
+
+/// Simple test for revoting.
+#[test]
+fn test_revoting() {
+    let mut app = App::default();
+    let proposal_id = app.store_code(single_proposal_contract());
+    let core_addr = instantiate_with_staked_balances_governance(
+        &mut app,
+        proposal_id,
+        InstantiateMsg {
+            threshold: Threshold::ThresholdQuorum {
+                threshold: PercentageThreshold::Majority {},
+                quorum: PercentageThreshold::Percent(Decimal::percent(10)),
+            },
+            max_voting_period: Duration::Height(10),
+            only_members_execute: true,
+            allow_revoting: true,
+            deposit_info: None,
+        },
+        Some(vec![
+            Cw20Coin {
+                address: "ekez".to_string(),
+                amount: Uint128::new(90),
+            },
+            Cw20Coin {
+                address: "slarbibfast".to_string(),
+                amount: Uint128::new(10),
+            },
+        ]),
+    );
+
+    let core_state: cw_core::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr, &cw_core::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let proposal_module = core_state.proposal_modules.into_iter().next().unwrap();
+
+    // The supreme galatic floob rules over many DAOs with benevolance
+    // and grace. The people of floob have become complacent in the
+    // goodness of the floob.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Propose {
+            title: "Supreme galactic floob.".to_string(),
+            description: "Recognize the supreme galactic floob as our DAO leader.".to_string(),
+            msgs: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    // The people initially jump at the chance to recognize the supreme
+    // galactic floob!
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // The wise slarbibfast does not agree. After some digging they
+    // discover that the floob has been bugging the hotel rooms of
+    // political rivals.
+    app.execute_contract(
+        Addr::unchecked("slarbibfast"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::No,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Time passes.
+    app.update_block(|b| b.height += 5);
+
+    // Word spreads.
+    app.update_block(|b| b.height += 4);
+
+    // At the last moment the people realize their mistake.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::No,
+        },
+        &[],
+    )
+    .unwrap();
+
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(
+            proposal_module.clone(),
+            &QueryMsg::Proposal { proposal_id: 1 },
+        )
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Open);
+    assert_eq!(
+        proposal.proposal.votes,
+        Votes {
+            yes: Uint128::zero(),
+            no: Uint128::new(100),
+            abstain: Uint128::zero()
+        }
+    );
+
+    // As the clock strikes midnight on the last day of the proposal,
+    // revoting has saved the day!
+    app.update_block(|b| b.height += 1);
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(proposal_module, &QueryMsg::Proposal { proposal_id: 1 })
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Rejected);
+}
+
+/// Tests that revoting is stored at a per-proposal level. Proposals
+/// created while revoting is enabled should not have it disabled if a
+/// config change turns if off.
+#[test]
+fn test_allow_revoting_config_changes() {
+    let mut app = App::default();
+    let proposal_id = app.store_code(single_proposal_contract());
+    let core_addr = instantiate_with_staked_balances_governance(
+        &mut app,
+        proposal_id,
+        InstantiateMsg {
+            threshold: Threshold::ThresholdQuorum {
+                threshold: PercentageThreshold::Majority {},
+                quorum: PercentageThreshold::Percent(Decimal::percent(10)),
+            },
+            max_voting_period: Duration::Height(10),
+            only_members_execute: true,
+            allow_revoting: true,
+            deposit_info: None,
+        },
+        Some(vec![
+            Cw20Coin {
+                address: "ekez".to_string(),
+                amount: Uint128::new(90),
+            },
+            Cw20Coin {
+                address: "slarbibfast".to_string(),
+                amount: Uint128::new(10),
+            },
+        ]),
+    );
+
+    let core_state: cw_core::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr.clone(), &cw_core::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let proposal_module = core_state.proposal_modules.into_iter().next().unwrap();
+
+    // Create a proposal. This proposal should allow revoting.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Propose {
+            title: "Supreme galactic floob.".to_string(),
+            description: "Recognize the supreme galactic floob as our DAO leader.".to_string(),
+            msgs: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Disable revoting.
+    app.execute_contract(
+        core_addr.clone(),
+        proposal_module.clone(),
+        &ExecuteMsg::UpdateConfig {
+            threshold: Threshold::ThresholdQuorum {
+                threshold: PercentageThreshold::Majority {},
+                quorum: PercentageThreshold::Percent(Decimal::percent(10)),
+            },
+            max_voting_period: Duration::Height(10),
+            only_members_execute: true,
+            allow_revoting: false,
+            deposit_info: None,
+            dao: core_addr.to_string(),
+        },
+        &[],
+    )
+    .unwrap();
+
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(
+            proposal_module.clone(),
+            &QueryMsg::Proposal { proposal_id: 1 },
+        )
+        .unwrap();
+    // The first created proposal should still allow revoting.
+    assert!(proposal.proposal.allow_revoting);
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::No,
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // New proposals should not allow revoting.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Propose {
+            title: "Supreme galactic floob.".to_string(),
+            description: "Recognize the supreme galactic floob as our DAO leader.".to_string(),
+            msgs: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("slarbibfast"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 2,
+            vote: Vote::No,
+        },
+        &[],
+    )
+    .unwrap();
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked("slarbibfast"),
+            proposal_module,
+            &ExecuteMsg::Vote {
+                proposal_id: 2,
+                vote: Vote::Yes,
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    assert!(matches!(err, ContractError::AlreadyVoted {}))
+}
+
+/// Tests that we error if a revote casts the same vote as the
+/// previous vote.
+#[test]
+fn test_revoting_same_vote_twice() {
+    let mut app = App::default();
+    let proposal_id = app.store_code(single_proposal_contract());
+    let core_addr = instantiate_with_staked_balances_governance(
+        &mut app,
+        proposal_id,
+        InstantiateMsg {
+            threshold: Threshold::ThresholdQuorum {
+                threshold: PercentageThreshold::Majority {},
+                quorum: PercentageThreshold::Percent(Decimal::percent(10)),
+            },
+            max_voting_period: Duration::Height(10),
+            only_members_execute: true,
+            allow_revoting: true,
+            deposit_info: None,
+        },
+        Some(vec![
+            Cw20Coin {
+                address: "ekez".to_string(),
+                amount: Uint128::new(90),
+            },
+            Cw20Coin {
+                address: "slarbibfast".to_string(),
+                amount: Uint128::new(10),
+            },
+        ]),
+    );
+
+    let core_state: cw_core::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr, &cw_core::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let proposal_module = core_state.proposal_modules.into_iter().next().unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Propose {
+            title: "Supreme galactic floob.".to_string(),
+            description: "Recognize the supreme galactic floob as our DAO leader.".to_string(),
+            msgs: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked("ekez"),
+            proposal_module.clone(),
+            &ExecuteMsg::Vote {
+                proposal_id: 1,
+                vote: Vote::Yes,
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    // Can't cast the same vote twice.
+    assert!(matches!(err, ContractError::AlreadyCast {}));
+
+    // Casting a different vote is fine. You can do that as many times
+    // as you'd like.
+    for _ in 1..5 {
+        app.execute_contract(
+            Addr::unchecked("ekez"),
+            proposal_module.clone(),
+            &ExecuteMsg::Vote {
+                proposal_id: 1,
+                vote: Vote::No,
+            },
+            &[],
+        )
+        .unwrap();
+        app.execute_contract(
+            Addr::unchecked("ekez"),
+            proposal_module.clone(),
+            &ExecuteMsg::Vote {
+                proposal_id: 1,
+                vote: Vote::Yes,
+            },
+            &[],
+        )
+        .unwrap();
+    }
+}
+
+/// Tests a simple three of five multisig configuration.
+#[test]
+fn test_three_of_five_multisig() {
+    let mut app = App::default();
+    let proposal_id = app.store_code(single_proposal_contract());
+    let core_addr = instantiate_with_cw4_groups_governance(
+        &mut app,
+        proposal_id,
+        InstantiateMsg {
+            threshold: Threshold::AbsoluteCount {
+                threshold: Uint128::new(3),
+            },
+            max_voting_period: Duration::Height(10),
+            only_members_execute: true,
+            allow_revoting: false,
+            deposit_info: None,
+        },
+        Some(vec![
+            Cw20Coin {
+                address: "one".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "two".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "three".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "four".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "five".to_string(),
+                amount: Uint128::new(1),
+            },
+        ]),
+    );
+
+    let core_state: cw_core::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr, &cw_core::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let proposal_module = core_state.proposal_modules.into_iter().next().unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("one"),
+        proposal_module.clone(),
+        &ExecuteMsg::Propose {
+            title: "Propose a thing.".to_string(),
+            description: "Do the thing.".to_string(),
+            msgs: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("one"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+    app.execute_contract(
+        Addr::unchecked("two"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Make sure it doesn't pass early.
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(
+            proposal_module.clone(),
+            &QueryMsg::Proposal { proposal_id: 1 },
+        )
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Open);
+
+    app.execute_contract(
+        Addr::unchecked("three"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(
+            proposal_module.clone(),
+            &QueryMsg::Proposal { proposal_id: 1 },
+        )
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Passed);
+
+    app.execute_contract(
+        Addr::unchecked("four"),
+        proposal_module.clone(),
+        &ExecuteMsg::Execute { proposal_id: 1 },
+        &[],
+    )
+    .unwrap();
+
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(proposal_module, &QueryMsg::Proposal { proposal_id: 1 })
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Executed);
+}
+
+/// Tests proposal rejection with three of five multisig style voting.
+#[test]
+fn test_three_of_five_multisig_reject() {
+    let mut app = App::default();
+    let proposal_id = app.store_code(single_proposal_contract());
+    let core_addr = instantiate_with_cw4_groups_governance(
+        &mut app,
+        proposal_id,
+        InstantiateMsg {
+            threshold: Threshold::AbsoluteCount {
+                threshold: Uint128::new(3),
+            },
+            max_voting_period: Duration::Height(10),
+            only_members_execute: true,
+            allow_revoting: false,
+            deposit_info: None,
+        },
+        Some(vec![
+            Cw20Coin {
+                address: "one".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "two".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "three".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "four".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "five".to_string(),
+                amount: Uint128::new(1),
+            },
+        ]),
+    );
+
+    let core_state: cw_core::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr, &cw_core::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let proposal_module = core_state.proposal_modules.into_iter().next().unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("one"),
+        proposal_module.clone(),
+        &ExecuteMsg::Propose {
+            title: "Propose a thing.".to_string(),
+            description: "Do the thing.".to_string(),
+            msgs: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("one"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+    app.execute_contract(
+        Addr::unchecked("two"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::No,
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("three"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::No,
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("four"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::No,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Still one vote outstanding but the module ought to have
+    // rejected it already as that one vote can not make the proposal
+    // pass.
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(
+            proposal_module.clone(),
+            &QueryMsg::Proposal { proposal_id: 1 },
+        )
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Rejected);
+
+    app.execute_contract(
+        Addr::unchecked("four"),
+        proposal_module.clone(),
+        &ExecuteMsg::Close { proposal_id: 1 },
+        &[],
+    )
+    .unwrap();
+
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(proposal_module, &QueryMsg::Proposal { proposal_id: 1 })
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Closed);
+}
+
+/// Tests that we fail to instantiate when using multisig style voting
+/// power and `VotingModuleToken {}`.
+#[test]
+#[should_panic]
+fn test_voting_module_token_with_multisig_style_voting() {
+    let mut app = App::default();
+    let proposal_id = app.store_code(single_proposal_contract());
+    instantiate_with_cw4_groups_governance(
+        &mut app,
+        proposal_id,
+        InstantiateMsg {
+            threshold: Threshold::AbsoluteCount {
+                threshold: Uint128::new(3),
+            },
+            max_voting_period: Duration::Height(10),
+            only_members_execute: true,
+            allow_revoting: false,
+            deposit_info: Some(DepositInfo {
+                token: DepositToken::VotingModuleToken {},
+                deposit: Uint128::new(1),
+                refund_failed_proposals: true,
+            }),
+        },
+        Some(vec![
+            Cw20Coin {
+                address: "one".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "two".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "three".to_string(),
+                amount: Uint128::new(1),
+            },
+        ]),
+    );
+}
+
+/// Tests revoting with multisig style absolute count thresholds.
+#[test]
+fn test_three_of_five_multisig_revoting() {
+    let mut app = App::default();
+    let proposal_id = app.store_code(single_proposal_contract());
+    let core_addr = instantiate_with_cw4_groups_governance(
+        &mut app,
+        proposal_id,
+        InstantiateMsg {
+            threshold: Threshold::AbsoluteCount {
+                threshold: Uint128::new(3),
+            },
+            max_voting_period: Duration::Height(10),
+            only_members_execute: true,
+            allow_revoting: true,
+            deposit_info: None,
+        },
+        Some(vec![
+            Cw20Coin {
+                address: "one".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "two".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "three".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "four".to_string(),
+                amount: Uint128::new(1),
+            },
+            Cw20Coin {
+                address: "five".to_string(),
+                amount: Uint128::new(1),
+            },
+        ]),
+    );
+
+    let core_state: cw_core::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr, &cw_core::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let proposal_module = core_state.proposal_modules.into_iter().next().unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("one"),
+        proposal_module.clone(),
+        &ExecuteMsg::Propose {
+            title: "Propose a thing.".to_string(),
+            description: "Do the thing.".to_string(),
+            msgs: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("one"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+    app.execute_contract(
+        Addr::unchecked("two"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("three"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked("four"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::No,
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Make sure it doesn't pass early.
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(
+            proposal_module.clone(),
+            &QueryMsg::Proposal { proposal_id: 1 },
+        )
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Open);
+
+    // Four changes their mind.
+    app.execute_contract(
+        Addr::unchecked("four"),
+        proposal_module.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id: 1,
+            vote: Vote::Yes,
+        },
+        &[],
+    )
+    .unwrap();
+
+    app.update_block(|b| b.height += 10);
+
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(
+            proposal_module.clone(),
+            &QueryMsg::Proposal { proposal_id: 1 },
+        )
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Passed);
+
+    app.execute_contract(
+        Addr::unchecked("four"),
+        proposal_module.clone(),
+        &ExecuteMsg::Execute { proposal_id: 1 },
+        &[],
+    )
+    .unwrap();
+
+    let proposal: ProposalResponse = app
+        .wrap()
+        .query_wasm_smart(proposal_module, &QueryMsg::Proposal { proposal_id: 1 })
+        .unwrap();
+    assert_eq!(proposal.proposal.status, Status::Executed);
+}
+
+/// Tests that absolute count style thresholds work with token style
+/// voting.
+#[test]
+fn test_absolute_count_threshold_non_multisig() {
+    do_votes_staked_balances(
+        vec![
+            TestVote {
+                voter: "one".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "two".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(200),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "three".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(1),
+                should_execute: ShouldExecute::Yes,
+            },
+        ],
+        Threshold::AbsoluteCount {
+            threshold: Uint128::new(11),
+        },
+        Status::Passed,
+        None,
+    );
+}
+
+/// Tests that we do not overflow when faced with really high token /
+/// vote supply.
+#[test]
+fn test_large_absolute_count_threshold() {
+    do_votes_staked_balances(
+        vec![
+            // Instant rejection after this.
+            TestVote {
+                voter: "two".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(1),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "one".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(u128::MAX - 1),
+                should_execute: ShouldExecute::No,
+            },
+        ],
+        Threshold::AbsoluteCount {
+            threshold: Uint128::new(u128::MAX),
+        },
+        Status::Rejected,
+        None,
+    );
+
+    do_votes_staked_balances(
+        vec![
+            TestVote {
+                voter: "one".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(u128::MAX - 1),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "two".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(1),
+                should_execute: ShouldExecute::Yes,
+            },
+        ],
+        Threshold::AbsoluteCount {
+            threshold: Uint128::new(u128::MAX),
+        },
+        Status::Rejected,
+        None,
+    );
+}
diff --git a/debug/proposal-hooks-counter/src/tests.rs b/debug/proposal-hooks-counter/src/tests.rs
index 095e2309..f998dee3 100644
--- a/debug/proposal-hooks-counter/src/tests.rs
+++ b/debug/proposal-hooks-counter/src/tests.rs
@@ -136,6 +136,7 @@ fn test_counters() {
         threshold,
         max_voting_period,
         only_members_execute: false,
+        allow_revoting: false,
         deposit_info: None,
     };