Repository: DA0-DA0/dao-contracts
Pull request #538 (2022-11-16 21:23:54)

Formally verify and fix overflow in cw20-stake stake and unstake methods
this fixes a fairly pathological, non-severe, integer overflow bug in the stake-cw20 contract. after discussion with ben2x4, meow, and bluenote, we've decided that this bug is extremely unlikely to be reached in production; if it is reached, it is entirely recoverable. as such, i'm making the patch public.

see here for a description of the bug, and a recovery strategy should anyone ever hit it:

https://github.com/DA0-DA0/dao-contracts/wiki/Overflow-in-stake-cw20

once this lands, i'll work on some detailed upgrade instructions for DAO DAO DAOs. it is nice we have a bug like this, because it is a great chance to get DAOs practicing doing upgrades in a safe environment. we can run a fire drill next week for DAOs that would like to participate and help them practice upgrading.

github-actions[bot]: <h1>Cosm-Orc Gas Usage</h1> 
 
 | Contract | Op Name | Gas Used | Old Gas Used | Gas Diff | File | 
 | --- | --- | --- | --- | --- | --- | 
| cw20_stake | Store__Store | 30110720 | 29285060 | +2.8194% | ci/integration-tests/src/helpers/chain.rs:98 |
 
 <details><summary>Raw Report for 1d700de085751d3cc10c71b73e195ea0343b99f3 </summary><br/> 

| Contract | Op Name | Gas Used | Gas Wanted | File | 
 | --- | --- | --- | --- | --- |
| cwd_core | Instantiate__inst_dao_no_admin | 1200254 | 1775769 | ci/integration-tests/src/helpers/helper.rs:98 |
| cwd_core | Execute__exc_admin_msgs_pause_dao | 194394 | 267078 | ci/integration-tests/src/tests/cw_core_test.rs:79 |
| cwd_core | Instantiate__exc_admin_msgs_create_dao | 1200254 | 1775769 | ci/integration-tests/src/helpers/helper.rs:98 |
| cwd_core | Instantiate__exc_admin_msgs_create_dao_with_admin | 1201304 | 1777343 | ci/integration-tests/src/helpers/helper.rs:98 |
| cwd_core | Execute__exc_items_rm | 192600 | 264389 | ci/integration-tests/src/tests/cw_core_test.rs:174 |
| cwd_core | Execute__exc_items_set | 194268 | 266891 | ci/integration-tests/src/tests/cw_core_test.rs:139 |
| cwd_core | Instantiate__exc_items_create_dao | 1201304 | 1777343 | ci/integration-tests/src/helpers/helper.rs:98 |
| cwd_core | Instantiate__exc_stake_create_dao | 1200230 | 1775733 | ci/integration-tests/src/helpers/helper.rs:98 |
| cwd_core | Instantiate__inst_admin_create_dao | 1201304 | 1777343 | ci/integration-tests/src/helpers/helper.rs:98 |
| cwd_core | Store__Store | 45996040 | 68969420 | ci/integration-tests/src/helpers/chain.rs:98 |
| cw721_base | Execute__mint_nft | 140322 | 185973 | ci/integration-tests/src/tests/cwd_voting_cw721_staked_test.rs:85 |
| cw721_base | Execute__stake_nft | 234371 | 327044 | ci/integration-tests/src/tests/cwd_voting_cw721_staked_test.rs:68 |
| cw721_base | Instantiate__instantiate_cw721_base | 163617 | 220917 | ci/integration-tests/src/tests/cwd_voting_cw721_staked_test.rs:22 |
| cw721_base | Store__Store | 28006640 | 41985320 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_voting_cw721_staked | Execute__unstake_nfts | 230098 | 320636 | ci/integration-tests/src/tests/cwd_voting_cw721_staked_test.rs:103 |
| cwd_voting_cw721_staked | Instantiate__instantiate_cwd_voting_cw721_staked | 172601 | 234393 | ci/integration-tests/src/tests/cwd_voting_cw721_staked_test.rs:48 |
| cwd_voting_cw721_staked | Store__Store | 27602830 | 41379605 | ci/integration-tests/src/helpers/chain.rs:98 |
| cw20_base | Execute__exc_stake_stake_tokens | 234578 | 319224 | ci/integration-tests/src/tests/cw20_stake_test.rs:76 |
| cw20_base | Store__Store | 34543940 | 51791270 | ci/integration-tests/src/helpers/chain.rs:98 |
| cw20_stake | Store__Store | 30110720 | 45141440 | ci/integration-tests/src/helpers/chain.rs:98 |
| cw4_group | Store__Store | 23034310 | 34526825 | ci/integration-tests/src/helpers/chain.rs:98 |
| cw_admin_factory | Store__Store | 16332340 | 24473870 | ci/integration-tests/src/helpers/chain.rs:98 |
| cw_token_swap | Store__Store | 20276940 | 30390770 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_pre_propose_multiple | Store__Store | 33837080 | 50730980 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_pre_propose_single | Store__Store | 33151120 | 49702040 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_proposal_multiple | Store__Store | 52271980 | 78383330 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_proposal_single | Store__Store | 53896020 | 80819390 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_voting_cw20_staked | Store__Store | 29905635 | 44834141 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_voting_cw4 | Store__Store | 22062790 | 33069545 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_voting_native_staked | Store__Store | 24601370 | 36877415 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_voting_staking_denom_staked | Store__Store | 17908970 | 26838815 | ci/integration-tests/src/helpers/chain.rs:98 |
| stake_cw20_external_rewards | Store__Store | 26120250 | 39155735 | ci/integration-tests/src/helpers/chain.rs:98 |
| stake_cw20_reward_distributor | Store__Store | 19956840 | 29910620 | ci/integration-tests/src/helpers/chain.rs:98 |
</details>

Patch:
diff --git a/contracts/proposal/cwd-proposal-multiple/README.md b/contracts/proposal/cwd-proposal-multiple/README.md
index 657ccaa6..e1870e42 100644
--- a/contracts/proposal/cwd-proposal-multiple/README.md
+++ b/contracts/proposal/cwd-proposal-multiple/README.md
@@ -1,4 +1,4 @@
-## cw-proposal-multiple
+## cwd-proposal-multiple
 
 A proposal module for a DAO DAO DAO which allows the users to select
 their voting choice(s) from an array of `MultipleChoiceOption`.
diff --git a/contracts/proposal/cwd-proposal-single/README.md b/contracts/proposal/cwd-proposal-single/README.md
index 851fcb74..202e3b11 100644
--- a/contracts/proposal/cwd-proposal-single/README.md
+++ b/contracts/proposal/cwd-proposal-single/README.md
@@ -1,4 +1,4 @@
-# cw-proposal-single
+# cwd-proposal-single
 
 A proposal module for a DAO DAO DAO which supports simple "yes", "no",
 "abstain" voting. Proposals may have associated messages which will be
diff --git a/contracts/proposal/cwd-proposal-single/schema/cwd-proposal-single.json b/contracts/proposal/cwd-proposal-single/schema/cwd-proposal-single.json
index 4751997d..ed6d0573 100644
--- a/contracts/proposal/cwd-proposal-single/schema/cwd-proposal-single.json
+++ b/contracts/proposal/cwd-proposal-single/schema/cwd-proposal-single.json
@@ -409,6 +409,13 @@
                 "format": "uint64",
                 "minimum": 0.0
               },
+              "rationale": {
+                "description": "An optional rationale for why this vote was cast. This can be updated, set, or removed later by the address casting the vote.",
+                "type": [
+                  "string",
+                  "null"
+                ]
+              },
               "vote": {
                 "description": "The senders position on the proposal.",
                 "allOf": [
@@ -423,6 +430,36 @@
         },
         "additionalProperties": false
       },
+      {
+        "description": "Updates the sender's rationale for their vote on the specified proposal. Errors if no vote vote has been cast.",
+        "type": "object",
+        "required": [
+          "update_rationale"
+        ],
+        "properties": {
+          "update_rationale": {
+            "type": "object",
+            "required": [
+              "proposal_id"
+            ],
+            "properties": {
+              "proposal_id": {
+                "type": "integer",
+                "format": "uint64",
+                "minimum": 0.0
+              },
+              "rationale": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
       {
         "description": "Causes the messages associated with a passed proposal to be executed by the DAO.",
         "type": "object",
@@ -2394,6 +2431,13 @@
                 }
               ]
             },
+            "rationale": {
+              "description": "Address-specified rationale for the vote.",
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "vote": {
               "description": "Position on the vote.",
               "allOf": [
@@ -3585,6 +3629,13 @@
                 }
               ]
             },
+            "rationale": {
+              "description": "Address-specified rationale for the vote.",
+              "type": [
+                "string",
+                "null"
+              ]
+            },
             "vote": {
               "description": "Position on the vote.",
               "allOf": [
diff --git a/contracts/proposal/cwd-proposal-single/src/contract.rs b/contracts/proposal/cwd-proposal-single/src/contract.rs
index 2905c74a..1dfd7fb3 100644
--- a/contracts/proposal/cwd-proposal-single/src/contract.rs
+++ b/contracts/proposal/cwd-proposal-single/src/contract.rs
@@ -97,7 +97,15 @@ pub fn execute(
             msgs,
             proposer,
         } => execute_propose(deps, env, info.sender, title, description, msgs, proposer),
-        ExecuteMsg::Vote { proposal_id, vote } => execute_vote(deps, env, info, proposal_id, vote),
+        ExecuteMsg::Vote {
+            proposal_id,
+            vote,
+            rationale,
+        } => execute_vote(deps, env, info, proposal_id, vote, rationale),
+        ExecuteMsg::UpdateRationale {
+            proposal_id,
+            rationale,
+        } => execute_update_rationale(deps, info, proposal_id, rationale),
         ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id),
         ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id),
         ExecuteMsg::UpdateConfig {
@@ -362,6 +370,7 @@ pub fn execute_vote(
     info: MessageInfo,
     proposal_id: u64,
     vote: Vote,
+    rationale: Option<String>,
 ) -> Result<Response, ContractError> {
     let config = CONFIG.load(deps.storage)?;
     let mut prop = PROPOSALS
@@ -381,36 +390,37 @@ pub fn execute_vote(
         return Err(ContractError::NotRegistered {});
     }
 
-    BALLOTS.update(
-        deps.storage,
-        (proposal_id, info.sender.clone()),
-        |bal| match bal {
-            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 {
-                        // Remove the old vote if this is a re-vote.
-                        prop.votes
-                            .remove_vote(current_ballot.vote, current_ballot.power);
-                        Ok(Ballot {
-                            power: vote_power,
-                            vote,
-                        })
-                    }
+    BALLOTS.update(deps.storage, (proposal_id, &info.sender), |bal| match bal {
+        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 {
-                    Err(ContractError::AlreadyVoted {})
+                    // Remove the old vote if this is a re-vote.
+                    prop.votes
+                        .remove_vote(current_ballot.vote, current_ballot.power);
+                    Ok(Ballot {
+                        power: vote_power,
+                        vote,
+                        // Roll over the previous rationale. If
+                        // you're changing your vote, you've also
+                        // likely changed your thinking.
+                        rationale: rationale.clone(),
+                    })
                 }
+            } else {
+                Err(ContractError::AlreadyVoted {})
             }
-            None => Ok(Ballot {
-                power: vote_power,
-                vote,
-            }),
-        },
-    )?;
+        }
+        None => Ok(Ballot {
+            power: vote_power,
+            vote,
+            rationale: rationale.clone(),
+        }),
+    })?;
 
     let old_status = prop.status;
 
@@ -443,9 +453,40 @@ pub fn execute_vote(
         .add_attribute("sender", info.sender)
         .add_attribute("proposal_id", proposal_id.to_string())
         .add_attribute("position", vote.to_string())
+        .add_attribute("rationale", rationale.as_deref().unwrap_or("_none"))
         .add_attribute("status", prop.status.to_string()))
 }
 
+pub fn execute_update_rationale(
+    deps: DepsMut,
+    info: MessageInfo,
+    proposal_id: u64,
+    rationale: Option<String>,
+) -> Result<Response, ContractError> {
+    BALLOTS.update(
+        deps.storage,
+        // info.sender can't be forged so we implicitly access control
+        // with the key.
+        (proposal_id, &info.sender),
+        |ballot| match ballot {
+            Some(ballot) => Ok(Ballot {
+                rationale: rationale.clone(),
+                ..ballot
+            }),
+            None => Err(ContractError::NoSuchVote {
+                id: proposal_id,
+                voter: info.sender.to_string(),
+            }),
+        },
+    )?;
+
+    Ok(Response::default()
+        .add_attribute("action", "update_rationale")
+        .add_attribute("sender", info.sender)
+        .add_attribute("proposal_id", proposal_id.to_string())
+        .add_attribute("rationale", rationale.as_deref().unwrap_or("_none")))
+}
+
 pub fn execute_close(
     deps: DepsMut,
     env: Env,
@@ -764,11 +805,12 @@ pub fn query_proposal_count(deps: Deps) -> StdResult<Binary> {
 
 pub fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult<Binary> {
     let voter = deps.api.addr_validate(&voter)?;
-    let ballot = BALLOTS.may_load(deps.storage, (proposal_id, voter.clone()))?;
+    let ballot = BALLOTS.may_load(deps.storage, (proposal_id, &voter))?;
     let vote = ballot.map(|ballot| VoteInfo {
         voter,
         vote: ballot.vote,
         power: ballot.power,
+        rationale: ballot.rationale,
     });
     to_binary(&VoteResponse { vote })
 }
@@ -783,7 +825,7 @@ pub fn query_list_votes(
     let start_after = start_after
         .map(|addr| deps.api.addr_validate(&addr))
         .transpose()?;
-    let min = start_after.map(Bound::<Addr>::exclusive);
+    let min = start_after.as_ref().map(Bound::<&Addr>::exclusive);
 
     let votes = BALLOTS
         .prefix(proposal_id)
@@ -795,6 +837,7 @@ pub fn query_list_votes(
                 voter,
                 vote: ballot.vote,
                 power: ballot.power,
+                rationale: ballot.rationale,
             })
         })
         .collect::<StdResult<Vec<_>>>()?;
diff --git a/contracts/proposal/cwd-proposal-single/src/error.rs b/contracts/proposal/cwd-proposal-single/src/error.rs
index 7be785be..9a30116e 100644
--- a/contracts/proposal/cwd-proposal-single/src/error.rs
+++ b/contracts/proposal/cwd-proposal-single/src/error.rs
@@ -29,6 +29,9 @@ pub enum ContractError {
     #[error("no such proposal ({id})")]
     NoSuchProposal { id: u64 },
 
+    #[error("no vote exists for proposal ({id}) and voter ({voter})")]
+    NoSuchVote { id: u64, voter: String },
+
     #[error("proposal is ({size}) bytes, must be <= ({max}) bytes")]
     ProposalTooLarge { size: u64, max: u64 },
 
diff --git a/contracts/proposal/cwd-proposal-single/src/msg.rs b/contracts/proposal/cwd-proposal-single/src/msg.rs
index c0234d98..6c98373a 100644
--- a/contracts/proposal/cwd-proposal-single/src/msg.rs
+++ b/contracts/proposal/cwd-proposal-single/src/msg.rs
@@ -63,6 +63,16 @@ pub enum ExecuteMsg {
         proposal_id: u64,
         /// The senders position on the proposal.
         vote: Vote,
+        /// An optional rationale for why this vote was cast. This can
+        /// be updated, set, or removed later by the address casting
+        /// the vote.
+        rationale: Option<String>,
+    },
+    /// Updates the sender's rationale for their vote on the specified
+    /// proposal. Errors if no vote vote has been cast.
+    UpdateRationale {
+        proposal_id: u64,
+        rationale: Option<String>,
     },
     /// Causes the messages associated with a passed proposal to be
     /// executed by the DAO.
diff --git a/contracts/proposal/cwd-proposal-single/src/query.rs b/contracts/proposal/cwd-proposal-single/src/query.rs
index 1c947a91..bf1352f0 100644
--- a/contracts/proposal/cwd-proposal-single/src/query.rs
+++ b/contracts/proposal/cwd-proposal-single/src/query.rs
@@ -20,6 +20,8 @@ pub struct VoteInfo {
     pub vote: Vote,
     /// The voting power behind the vote.
     pub power: Uint128,
+    /// Address-specified rationale for the vote.
+    pub rationale: Option<String>,
 }
 
 /// Information about a vote.
diff --git a/contracts/proposal/cwd-proposal-single/src/state.rs b/contracts/proposal/cwd-proposal-single/src/state.rs
index b9b7fc3d..e3279867 100644
--- a/contracts/proposal/cwd-proposal-single/src/state.rs
+++ b/contracts/proposal/cwd-proposal-single/src/state.rs
@@ -14,6 +14,12 @@ pub struct Ballot {
     pub power: Uint128,
     /// The position.
     pub vote: Vote,
+
+    /// An optional rationale for why this vote was cast. If the key
+    /// is missing (i.e. the ballot was cast in a v1 proposal module),
+    /// we deserialize into None (i.e. Option::default()).
+    #[serde(default)]
+    pub rationale: Option<String>,
 }
 /// The governance module's configuration.
 #[cw_serde]
@@ -57,7 +63,7 @@ pub const CONFIG: Item<Config> = Item::new("config_v2");
 /// The number of proposals that have been created.
 pub const PROPOSAL_COUNT: Item<u64> = Item::new("proposal_count");
 pub const PROPOSALS: Map<u64, SingleChoiceProposal> = Map::new("proposals_v2");
-pub const BALLOTS: Map<(u64, Addr), Ballot> = Map::new("ballots");
+pub const BALLOTS: Map<(u64, &Addr), Ballot> = Map::new("ballots");
 /// Consumers of proposal state change hooks.
 pub const PROPOSAL_HOOKS: Hooks = Hooks::new("proposal_hooks");
 /// Consumers of vote hooks.
diff --git a/contracts/staking/cw20-stake/src/contract.rs b/contracts/staking/cw20-stake/src/contract.rs
index ccbeb5d8..3716f7ce 100644
--- a/contracts/staking/cw20-stake/src/contract.rs
+++ b/contracts/staking/cw20-stake/src/contract.rs
@@ -10,6 +10,7 @@ use cosmwasm_std::{
 use cw20::Cw20ReceiveMsg;
 
 use crate::hooks::{stake_hook_msgs, unstake_hook_msgs};
+use crate::math;
 use crate::msg::{
     ExecuteMsg, GetHooksResponse, InstantiateMsg, ListStakersResponse, MigrateMsg, QueryMsg,
     ReceiveMsg, StakedBalanceAtHeightResponse, StakedValueResponse, StakerBalanceResponse,
@@ -189,15 +190,7 @@ pub fn execute_stake(
 ) -> Result<Response, ContractError> {
     let balance = BALANCE.load(deps.storage)?;
     let staked_total = STAKED_TOTAL.load(deps.storage)?;
-    let amount_to_stake = if staked_total == Uint128::zero() || balance == Uint128::zero() {
-        amount
-    } else {
-        staked_total
-            .checked_mul(amount)
-            .map_err(StdError::overflow)?
-            .checked_div(balance)
-            .map_err(StdError::divide_by_zero)?
-    };
+    let amount_to_stake = math::amount_to_stake(staked_total, balance, amount);
     STAKED_BALANCES.update(
         deps.storage,
         &sender,
@@ -233,11 +226,17 @@ pub fn execute_unstake(
     let config = CONFIG.load(deps.storage)?;
     let balance = BALANCE.load(deps.storage)?;
     let staked_total = STAKED_TOTAL.load(deps.storage)?;
-    let amount_to_claim = amount
-        .checked_mul(balance)
-        .map_err(StdError::overflow)?
-        .checked_div(staked_total)
-        .map_err(StdError::divide_by_zero)?;
+    // invariant checks for amount_to_claim
+    if staked_total.is_zero() {
+        return Err(ContractError::NothingStaked {});
+    }
+    if amount.saturating_add(balance) == Uint128::MAX {
+        return Err(ContractError::Cw20InvaraintViolation {});
+    }
+    if amount > staked_total {
+        return Err(ContractError::ImpossibleUnstake {});
+    }
+    let amount_to_claim = math::amount_to_claim(staked_total, balance, amount);
     STAKED_BALANCES.update(
         deps.storage,
         &info.sender,
diff --git a/contracts/staking/cw20-stake/src/error.rs b/contracts/staking/cw20-stake/src/error.rs
index c3e17f5c..9fd355eb 100644
--- a/contracts/staking/cw20-stake/src/error.rs
+++ b/contracts/staking/cw20-stake/src/error.rs
@@ -9,6 +9,12 @@ pub enum ContractError {
     Cw20Error(#[from] cw20_base::ContractError),
     #[error("Nothing to claim")]
     NothingToClaim {},
+    #[error("Nothing to unstake")]
+    NothingStaked {},
+    #[error("Unstaking this amount violates the invariant: (cw20 total_supply <= 2^128)")]
+    Cw20InvaraintViolation {},
+    #[error("Can not unstake more than has been staked")]
+    ImpossibleUnstake {},
     #[error("Invalid token")]
     InvalidToken { received: Addr, expected: Addr },
     #[error("Unauthorized")]
diff --git a/contracts/staking/cw20-stake/src/lib.rs b/contracts/staking/cw20-stake/src/lib.rs
index efc1a5ec..ff75b130 100644
--- a/contracts/staking/cw20-stake/src/lib.rs
+++ b/contracts/staking/cw20-stake/src/lib.rs
@@ -1,6 +1,7 @@
 pub mod contract;
 mod error;
 pub mod hooks;
+mod math;
 pub mod msg;
 pub mod state;
 
diff --git a/contracts/staking/cw20-stake/src/math.rs b/contracts/staking/cw20-stake/src/math.rs
new file mode 100644
index 00000000..879a0973
--- /dev/null
+++ b/contracts/staking/cw20-stake/src/math.rs
@@ -0,0 +1,139 @@
+use std::{convert::TryInto, ops::Div};
+
+use cosmwasm_std::{Uint128, Uint256};
+
+/// Computes the amount to add to an address' staked balance when
+/// staking.
+///
+/// # Arguments
+///
+/// * `staked_total` - The number of tokens that have been staked.
+/// * `balance` - The number of tokens the contract has (staked_total + rewards).
+/// * `sent` - The number of tokens the user has sent to be staked.
+pub(crate) fn amount_to_stake(staked_total: Uint128, balance: Uint128, sent: Uint128) -> Uint128 {
+    if staked_total.is_zero() || balance.is_zero() {
+        sent
+    } else {
+        staked_total
+            .full_mul(sent)
+            .div(Uint256::from(balance))
+            .try_into()
+            .unwrap() // balance := staked_total + rewards
+                      // => balance >= staked_total
+                      // => staked_total / balance <= 1
+                      // => staked_total * sent / balance <= sent
+                      // => we can safely unwrap here as sent fits into a u128 by construction.
+    }
+}
+
+/// Computes the number of tokens to return to an address when
+/// claiming.
+///
+/// # Arguments
+///
+/// * `staked_total` - The number of tokens that have been staked.
+/// * `balance` - The number of tokens the contract has (staked_total + rewards).
+/// * `ask` - The number of tokens being claimed.
+///
+/// # Invariants
+///
+/// These must be checked by the caller. If checked, this function is
+/// guarenteed not to panic.
+///
+/// 1. staked_total != 0.
+/// 2. ask + balance <= 2^128
+/// 3. ask <= staked_total
+///
+/// For information on the panic conditions for math, see:
+/// <https://rust-lang.github.io/rfcs/0560-integer-overflow.html>
+pub(crate) fn amount_to_claim(staked_total: Uint128, balance: Uint128, ask: Uint128) -> Uint128 {
+    // we know that:
+    //
+    // 1. cw20's max supply is 2^128
+    // 2. balance := staked_total + rewards
+    //
+    // for non-malicious inputs:
+    //
+    // 3. 1 => ask + balance <= 2^128
+    // 4. ask <= staked_total
+    // 5. staked_total != 0
+    // 6. 4 => ask / staked_total <= 1
+    // 7. 3 => balance <= 2^128
+    // 8. 6 + 7 => ask / staked_total * balance <= 2^128
+    //
+    // which, as addition and division are communative, proves that
+    // ask * balance / staked_total will fit into a 128 bit integer.
+    ask.full_mul(balance)
+        .div(Uint256::from(staked_total))
+        .try_into()
+        .unwrap()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_amount_to_stake_no_overflow() {
+        let sent = Uint128::new(2);
+        let balance = Uint128::MAX - sent;
+
+        let overflows_naively = sent.checked_mul(balance).is_err();
+        assert!(overflows_naively);
+
+        // will panic and fail the test if we've done this wrong.
+        amount_to_stake(balance, balance, sent);
+    }
+
+    #[test]
+    fn test_amount_to_stake_with_zeros() {
+        let sent = Uint128::new(42);
+        let balance = Uint128::zero();
+        let amount = amount_to_stake(balance, balance, sent);
+        assert_eq!(amount, sent);
+    }
+
+    #[test]
+    fn test_amount_to_claim_no_overflow() {
+        let ask = Uint128::new(2);
+        let balance = Uint128::MAX - ask;
+
+        let overflows_naively = ask.checked_mul(balance).is_err();
+        assert!(overflows_naively);
+
+        amount_to_claim(balance, balance, ask);
+    }
+
+    // check that our invariants are indeed invariants.
+
+    #[test]
+    #[should_panic(expected = "attempt to divide by zero")]
+    fn test_amount_to_claim_invariant_one() {
+        let ask = Uint128::new(2);
+        let balance = Uint128::zero();
+
+        amount_to_claim(balance, balance, ask);
+    }
+
+    #[test]
+    #[should_panic(expected = "ConversionOverflowError")]
+    fn test_amount_to_claim_invariant_two() {
+        // Could end up in a situation like this if there are a lot of
+        // rewards, but very few staked tokens.
+        let ask = Uint128::new(2);
+        let balance = Uint128::MAX;
+        let staked_total = Uint128::new(1);
+
+        amount_to_claim(staked_total, balance, ask);
+    }
+
+    #[test]
+    #[should_panic(expected = "ConversionOverflowError")]
+    fn test_amount_to_claim_invariant_three() {
+        let ask = Uint128::new(2);
+        let balance = Uint128::MAX;
+        let staked_total = Uint128::new(1);
+
+        amount_to_claim(staked_total, balance, ask);
+    }
+}
diff --git a/contracts/voting/cwd-voting-cw20-staked/schema/cwd-voting-cw20-staked.json b/contracts/voting/cwd-voting-cw20-staked/schema/cwd-voting-cw20-staked.json
index 03589b70..804f5f13 100644
--- a/contracts/voting/cwd-voting-cw20-staked/schema/cwd-voting-cw20-staked.json
+++ b/contracts/voting/cwd-voting-cw20-staked/schema/cwd-voting-cw20-staked.json
@@ -27,8 +27,10 @@
     "additionalProperties": false,
     "definitions": {
       "ActiveThreshold": {
+        "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.",
         "oneOf": [
           {
+            "description": "The absolute number of tokens that must be staked for the module to be active.",
             "type": "object",
             "required": [
               "absolute_count"
@@ -50,6 +52,7 @@
             "additionalProperties": false
           },
           {
+            "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.",
             "type": "object",
             "required": [
               "percentage"
@@ -227,6 +230,7 @@
         ]
       },
       "StakingInfo": {
+        "description": "Information about the staking contract to be used with this voting module.",
         "oneOf": [
           {
             "type": "object",
@@ -241,6 +245,7 @@
                 ],
                 "properties": {
                   "staking_contract_address": {
+                    "description": "Address of an already instantiated staking contract.",
                     "type": "string"
                   }
                 },
@@ -262,11 +267,13 @@
                 ],
                 "properties": {
                   "staking_code_id": {
+                    "description": "Code ID for staking contract to instantiate.",
                     "type": "integer",
                     "format": "uint64",
                     "minimum": 0.0
                   },
                   "unstaking_duration": {
+                    "description": "See corresponding field in cw20-stake's instantiation. This will be used when instantiating the new staking contract.",
                     "anyOf": [
                       {
                         "$ref": "#/definitions/Duration"
@@ -300,10 +307,16 @@
                 ],
                 "properties": {
                   "address": {
+                    "description": "Address of an already instantiated cw20 token contract.",
                     "type": "string"
                   },
                   "staking_contract": {
-                    "$ref": "#/definitions/StakingInfo"
+                    "description": "Information about the staking contract to use.",
+                    "allOf": [
+                      {
+                        "$ref": "#/definitions/StakingInfo"
+                      }
+                    ]
                   }
                 },
                 "additionalProperties": false
@@ -330,6 +343,7 @@
                 ],
                 "properties": {
                   "code_id": {
+                    "description": "Code ID for cw20 token contract.",
                     "type": "integer",
                     "format": "uint64",
                     "minimum": 0.0
@@ -356,6 +370,7 @@
                     ]
                   },
                   "label": {
+                    "description": "Label to use for instantiated cw20 contract.",
                     "type": "string"
                   },
                   "marketing": {
@@ -408,6 +423,7 @@
     "title": "ExecuteMsg",
     "oneOf": [
       {
+        "description": "Sets the active threshold to a new value. Only the instantiator this contract (a DAO most likely) may call this method.",
         "type": "object",
         "required": [
           "update_active_threshold"
@@ -435,8 +451,10 @@
     ],
     "definitions": {
       "ActiveThreshold": {
+        "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.",
         "oneOf": [
           {
+            "description": "The absolute number of tokens that must be staked for the module to be active.",
             "type": "object",
             "required": [
               "absolute_count"
@@ -458,6 +476,7 @@
             "additionalProperties": false
           },
           {
+            "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.",
             "type": "object",
             "required": [
               "percentage"
@@ -495,6 +514,7 @@
     "title": "QueryMsg",
     "oneOf": [
       {
+        "description": "Gets the address of the cw20-stake contract this voting module is wrapping.",
         "type": "object",
         "required": [
           "staking_contract"
@@ -508,6 +528,7 @@
         "additionalProperties": false
       },
       {
+        "description": "Gets the address that instantiated this module.",
         "type": "object",
         "required": [
           "dao"
@@ -521,6 +542,7 @@
         "additionalProperties": false
       },
       {
+        "description": "Gets the active threshold for this module.",
         "type": "object",
         "required": [
           "active_threshold"
@@ -653,8 +675,10 @@
       "additionalProperties": false,
       "definitions": {
         "ActiveThreshold": {
+          "description": "The threshold of tokens that must be staked in order for this voting module to be active. If this is not reached, this module will response to `is_active` queries with false and proposal modules which respect active thresholds will not allow the creation of proposals.",
           "oneOf": [
             {
+              "description": "The absolute number of tokens that must be staked for the module to be active.",
               "type": "object",
               "required": [
                 "absolute_count"
@@ -676,6 +700,7 @@
               "additionalProperties": false
             },
             {
+              "description": "The percentage of tokens that must be staked for the module to be active. Computed as `staked / total_supply`.",
               "type": "object",
               "required": [
                 "percentage"
diff --git a/contracts/voting/cwd-voting-cw20-staked/src/contract.rs b/contracts/voting/cwd-voting-cw20-staked/src/contract.rs
index b3e06348..d3a1a927 100644
--- a/contracts/voting/cwd-voting-cw20-staked/src/contract.rs
+++ b/contracts/voting/cwd-voting-cw20-staked/src/contract.rs
@@ -39,13 +39,14 @@ pub fn instantiate(
 ) -> Result<Response, ContractError> {
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
     DAO.save(deps.storage, &info.sender)?;
-    if let Some(active_threshold) = msg.active_threshold.clone() {
+
+    if let Some(active_threshold) = msg.active_threshold.as_ref() {
         if let ActiveThreshold::Percentage { percent } = active_threshold {
-            if percent > Decimal::percent(100) || percent <= Decimal::percent(0) {
+            if *percent > Decimal::percent(100) || *percent <= Decimal::percent(0) {
                 return Err(ContractError::InvalidActivePercentage {});
             }
         }
-        ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?;
+        ACTIVE_THRESHOLD.save(deps.storage, active_threshold)?;
     }
 
     match msg.token_info {
@@ -56,7 +57,7 @@ pub fn instantiate(
             let address = deps.api.addr_validate(&address)?;
             TOKEN.save(deps.storage, &address)?;
             if let Some(ActiveThreshold::AbsoluteCount { count }) = msg.active_threshold {
-                assert_valid_absolute_count_threshold(deps.as_ref(), address.clone(), count)?;
+                assert_valid_absolute_count_threshold(deps.as_ref(), &address, count)?;
             }
 
             match staking_contract {
@@ -169,7 +170,7 @@ pub fn instantiate(
 
 pub fn assert_valid_absolute_count_threshold(
     deps: Deps,
-    token_addr: Addr,
+    token_addr: &Addr,
     count: Uint128,
 ) -> Result<(), ContractError> {
     let token_info: cw20::TokenInfoResponse = deps
@@ -215,7 +216,7 @@ pub fn execute_update_active_threshold(
             }
             ActiveThreshold::AbsoluteCount { count } => {
                 let token = TOKEN.load(deps.storage)?;
-                assert_valid_absolute_count_threshold(deps.as_ref(), token, count)?;
+                assert_valid_absolute_count_threshold(deps.as_ref(), &token, count)?;
             }
         }
         ACTIVE_THRESHOLD.save(deps.storage, &active_threshold)?;
@@ -313,12 +314,38 @@ pub fn query_is_active(deps: Deps) -> StdResult<Binary> {
                 active: actual_power.total >= count,
             }),
             ActiveThreshold::Percentage { percent } => {
+                // percent is bounded between [0, 100]. decimal
+                // represents percents in u128 terms as p *
+                // 10^15. this bounds percent between [0, 10^17].
+                //
+                // total_potential_power is bounded between [0, 2^128]
+                // as it tracks the balances of a cw20 token which has
+                // a max supply of 2^128.
+                //
+                // with our precision factor being 10^9:
+                //
+                // total_power <= 2^128 * 10^9 <= 2^256
+                //
+                // so we're good to put that in a u256.
+                //
+                // multiply_ratio promotes to a u512 under the hood,
+                // so it won't overflow, multiplying by a percent less
+                // than 100 is gonna make something the same size or
+                // smaller, applied + 10^9 <= 2^128 * 10^9 + 10^9 <=
+                // 2^256, so the top of the round won't overflow, and
+                // rounding is rounding down, so the whole thing can
+                // be safely unwrapped at the end of the day thank you
+                // for coming to my ted talk.
                 let total_potential_power: TokenInfoResponse = deps
                     .querier
                     .query_wasm_smart(token_contract, &cw20_base::msg::QueryMsg::TokenInfo {})?;
                 let total_power = total_potential_power
                     .total_supply
                     .full_mul(PRECISION_FACTOR);
+                // under the hood decimals are `atomics / 10^decimal_places`.
+                // cosmwasm doesn't give us a Decimal * Uint256
+                // implementation so we take the decimal apart and
+                // multiply by the fraction.
                 let applied = total_power.multiply_ratio(
                     percent.atomics(),
                     Uint256::from(10u64).pow(percent.decimal_places()),
@@ -358,6 +385,8 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractEr
                 Ok(res) => {
                     let token = TOKEN.may_load(deps.storage)?;
                     if token.is_some() {
+                        // There is no known way this error could ever
+                        // be triggered, we're just paranoid.
                         return Err(ContractError::DuplicateToken {});
                     }
                     let token = deps.api.addr_validate(&res.contract_address)?;
@@ -365,7 +394,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractEr
 
                     let active_threshold = ACTIVE_THRESHOLD.may_load(deps.storage)?;
                     if let Some(ActiveThreshold::AbsoluteCount { count }) = active_threshold {
-                        assert_valid_absolute_count_threshold(deps.as_ref(), token.clone(), count)?;
+                        assert_valid_absolute_count_threshold(deps.as_ref(), &token, count)?;
                     }
 
                     let staking_contract_code_id = STAKING_CONTRACT_CODE_ID.load(deps.storage)?;
@@ -396,21 +425,18 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractEr
             let res = parse_reply_instantiate_data(msg);
             match res {
                 Ok(res) => {
-                    // Validate contract address
                     let staking_contract_addr = deps.api.addr_validate(&res.contract_address)?;
 
-                    // Check if we have a duplicate
                     let staking = STAKING_CONTRACT.may_load(deps.storage)?;
                     if staking.is_some() {
                         return Err(ContractError::DuplicateStakingContract {});
                     }
 
-                    // Save staking contract addr
                     STAKING_CONTRACT.save(deps.storage, &staking_contract_addr)?;
 
                     Ok(Response::new().add_attribute("staking_contract", staking_contract_addr))
                 }
-                Err(_) => Err(ContractError::TokenInstantiateError {}),
+                Err(_) => Err(ContractError::StakingInstantiateError {}),
             }
         }
         _ => Err(ContractError::UnknownReplyId { id: msg.id }),
diff --git a/contracts/voting/cwd-voting-cw20-staked/src/error.rs b/contracts/voting/cwd-voting-cw20-staked/src/error.rs
index 10d2392a..344fcaba 100644
--- a/contracts/voting/cwd-voting-cw20-staked/src/error.rs
+++ b/contracts/voting/cwd-voting-cw20-staked/src/error.rs
@@ -18,6 +18,9 @@ pub enum ContractError {
     #[error("Error instantiating token")]
     TokenInstantiateError {},
 
+    #[error("Error instantiating staking contract")]
+    StakingInstantiateError {},
+
     #[error("Got a submessage reply with unknown id: {id}")]
     UnknownReplyId { id: u64 },
 
diff --git a/contracts/voting/cwd-voting-cw20-staked/src/msg.rs b/contracts/voting/cwd-voting-cw20-staked/src/msg.rs
index ccd08aaf..0f0ec3a8 100644
--- a/contracts/voting/cwd-voting-cw20-staked/src/msg.rs
+++ b/contracts/voting/cwd-voting-cw20-staked/src/msg.rs
@@ -6,13 +6,20 @@ use cw_utils::Duration;
 
 use cwd_macros::{active_query, info_query, token_query, voting_query};
 
+/// Information about the staking contract to be used with this voting
+/// module.
 #[cw_serde]
 pub enum StakingInfo {
     Existing {
+        /// Address of an already instantiated staking contract.
         staking_contract_address: String,
     },
     New {
+        /// Code ID for staking contract to instantiate.
         staking_code_id: u64,
+        /// See corresponding field in cw20-stake's
+        /// instantiation. This will be used when instantiating the
+        /// new staking contract.
         unstaking_duration: Option<Duration>,
     },
 }
@@ -21,11 +28,15 @@ pub enum StakingInfo {
 #[allow(clippy::large_enum_variant)]
 pub enum TokenInfo {
     Existing {
+        /// Address of an already instantiated cw20 token contract.
         address: String,
+        /// Information about the staking contract to use.
         staking_contract: StakingInfo,
     },
     New {
+        /// Code ID for cw20 token contract.
         code_id: u64,
+        /// Label to use for instantiated cw20 contract.
         label: String,
 
         name: String,
@@ -33,15 +44,25 @@ pub enum TokenInfo {
         decimals: u8,
         initial_balances: Vec<Cw20Coin>,
         marketing: Option<InstantiateMarketingInfo>,
+
         staking_code_id: u64,
         unstaking_duration: Option<Duration>,
         initial_dao_balance: Option<Uint128>,
     },
 }
 
+/// The threshold of tokens that must be staked in order for this
+/// voting module to be active. If this is not reached, this module
+/// will response to `is_active` queries with false and proposal
+/// modules which respect active thresholds will not allow the
+/// creation of proposals.
 #[cw_serde]
 pub enum ActiveThreshold {
+    /// The absolute number of tokens that must be staked for the
+    /// module to be active.
     AbsoluteCount { count: Uint128 },
+    /// The percentage of tokens that must be staked for the module to
+    /// be active. Computed as `staked / total_supply`.
     Percentage { percent: Decimal },
 }
 
@@ -53,6 +74,9 @@ pub struct InstantiateMsg {
 
 #[cw_serde]
 pub enum ExecuteMsg {
+    /// Sets the active threshold to a new value. Only the
+    /// instantiator this contract (a DAO most likely) may call this
+    /// method.
     UpdateActiveThreshold {
         new_threshold: Option<ActiveThreshold>,
     },
@@ -65,10 +89,14 @@ pub enum ExecuteMsg {
 #[cw_serde]
 #[derive(QueryResponses)]
 pub enum QueryMsg {
+    /// Gets the address of the cw20-stake contract this voting module
+    /// is wrapping.
     #[returns(cosmwasm_std::Addr)]
     StakingContract {},
+    /// Gets the address that instantiated this module.
     #[returns(cosmwasm_std::Addr)]
     Dao {},
+    /// Gets the active threshold for this module.
     #[returns(ActiveThresholdResponse)]
     ActiveThreshold {},
 }
diff --git a/scripts/create-v2-dao-native-voting.sh b/scripts/create-v2-dao-native-voting.sh
new file mode 100755
index 00000000..3c0505d2
--- /dev/null
+++ b/scripts/create-v2-dao-native-voting.sh
@@ -0,0 +1,104 @@
+#!/bin/bash
+
+if [ "$1" = "" ]
+then
+  echo "Usage: $0 junod key name required as an argument, e.g. ./create-v2-dao-native-voting.sh mykeyname"
+  exit
+fi
+
+export CHAIN_ID="uni-5"
+export TESTNET_NAME="uni-5"
+export DENOM="ujunox"
+export BECH32_HRP="juno"
+export WASMD_VERSION="v0.20.0"
+export JUNOD_VERSION="v2.1.0"
+export CONFIG_DIR=".juno"
+export BINARY="junod"
+
+export COSMJS_VERSION="v0.26.5"
+export GENESIS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/genesis.json"
+export PERSISTENT_PEERS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/persistent_peers.txt"
+export SEEDS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/seeds.txt"
+
+export RPC="https://rpc.uni.juno.deuslabs.fi:443"
+export LCD="https://lcd.uni.juno.deuslabs.fi"
+export FAUCET="https://faucet.uni.juno.deuslabs.fi"
+
+export COSMOVISOR_VERSION="v0.1.0"
+export COSMOVISOR_HOME=$HOME/.juno
+export COSMOVISOR_NAME=junod
+
+export TXFLAG="--chain-id ${CHAIN_ID} --gas-prices 0.025ujunox --gas auto --gas-adjustment 1.3 --broadcast-mode block"
+export NODE="https://juno-testnet-rpc.polkachu.com:443"
+
+KEY_NAME=$1
+MODULE_MSG='{
+        "allow_revoting": false,
+        "max_voting_period": {
+          "time": 604800
+        },
+        "close_proposal_on_execution_failure": true,
+        "pre_propose_info": {"AnyoneMayPropose":{}},
+        "only_members_execute": true,
+        "threshold": {
+          "threshold_quorum": {
+            "quorum": {
+              "percent": "0.20"
+            },
+            "threshold": {
+              "majority": {}
+            }
+          }
+        }
+      }'
+
+ENCODED_PROP_MESSAGE=`echo -n $MODULE_MSG | tr -d '[:space:]' | openssl base64 | tr -d '[:space:]'`
+echo -e '\nENCODED PROP MESSAGE'
+echo $ENCODED_PROP_MESSAGE
+
+VOTING_MSG='{"owner":{"core_module":{}},"denom":"ujunox"}'
+
+ENCODED_VOTING_MESSAGE=`echo $VOTING_MSG | tr -d '[:space:]' | openssl base64 | tr -d '[:space:]'`
+echo -e '\nENCODED VOTING MESSAGE'
+echo $ENCODED_VOTING_MESSAGE
+
+CW_CORE_INIT='{
+  "admin": "juno12jphyrpd82v8s8cq4n0nu7fa9qcx5hppdwevulhqdhyqu7vkrscs3sv2ct",
+  "automatically_add_cw20s": true,
+  "automatically_add_cw721s": true,
+  "description": "V2 DAO",
+  "name": "V2 DAO",
+  "proposal_modules_instantiate_info": [
+    {
+      "admin": {
+        "core_module": {}
+      },
+      "code_id": 696,
+      "label": "v2 dao",
+      "msg": "'$ENCODED_PROP_MESSAGE'"
+    }
+  ],
+  "voting_module_instantiate_info": {
+    "admin": {
+      "core_module": {}
+    },
+    "code_id": 799,
+    "label": "test_v2_dao-cw-native-voting",
+    "msg": "'$ENCODED_VOTING_MESSAGE'"
+  }
+}'
+
+# encode
+CW_CORE_STRIPPED=`echo -n $CW_CORE_INIT | tr -d '[:space:]'`
+echo -e 'CW-CORE INSTANTIATE MESSAGE:\n'
+echo -$CW_CORE_STRIPPED 
+CW_CORE_ENCODED=`echo -n $CW_CORE_STRIPPED | openssl base64 | tr -d '[:space:]'`
+echo -e '\nCW-CORE ENCODED MESSAGE:\n'
+echo  $CW_CORE_ENCODED
+
+# init with factory
+INIT_MSG='{"instantiate_contract_with_self_admin":{"code_id":695, "label": "v2 subDAO subDAO", "instantiate_msg":"'$CW_CORE_ENCODED'"}}'
+
+# instantiate with factory 
+echo 'instantiating cw-core with factory'
+junod tx wasm execute juno143quaa25ynh6j5chhqh8w6lj647l4kpu5r6aqxvzul8du0mwyvns2g6u45 "$INIT_MSG" --from bluenote --node $NODE $TXFLAG
diff --git a/scripts/create-v2-dao-prepropose.sh b/scripts/create-v2-dao-prepropose.sh
new file mode 100755
index 00000000..0753886e
--- /dev/null
+++ b/scripts/create-v2-dao-prepropose.sh
@@ -0,0 +1,140 @@
+#!/bin/bash
+
+if [ "$1" = "" ]
+then
+  echo "Usage: $0 junod key name required as an argument, e.g. ./create-v2-dao-native-voting.sh mykeyname"
+  exit
+fi
+
+export CHAIN_ID="uni-5"
+export TESTNET_NAME="uni-5"
+export DENOM="ujunox"
+export BECH32_HRP="juno"
+export WASMD_VERSION="v0.20.0"
+export JUNOD_VERSION="v2.1.0"
+export CONFIG_DIR=".juno"
+export BINARY="junod"
+
+export COSMJS_VERSION="v0.26.5"
+export GENESIS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/genesis.json"
+export PERSISTENT_PEERS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/persistent_peers.txt"
+export SEEDS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/seeds.txt"
+
+export RPC="https://rpc.uni.juno.deuslabs.fi:443"
+export LCD="https://lcd.uni.juno.deuslabs.fi"
+export FAUCET="https://faucet.uni.juno.deuslabs.fi"
+
+export COSMOVISOR_VERSION="v0.1.0"
+export COSMOVISOR_HOME=$HOME/.juno
+export COSMOVISOR_NAME=junod
+
+export TXFLAG="--chain-id ${CHAIN_ID} --gas-prices 0.025ujunox --gas auto --gas-adjustment 1.3 --broadcast-mode block"
+export NODE="https://juno-testnet-rpc.polkachu.com:443"
+
+KEY_NAME=$1
+
+PRE_PROPOSE_INSTANTIATE_MSG='{
+    "deposit_info": {
+      "denom": {
+        "token": {
+          "denom": {
+            "native": "ujunox"
+          }
+        }
+      },
+      "amount": "1",
+      "refund_policy": "always"
+    },
+    "open_proposal_submission": false,
+    "extension": {}
+  }'
+
+# NO DEPOSIT
+
+  # PRE_PROPOSE_INSTANTIATE_MSG='{
+  #   "open_proposal_submission": false,
+  #   "extension": {}
+  # }'
+
+ENCODED_PRE_PROPOSE=`echo -n $PRE_PROPOSE_INSTANTIATE_MSG | tr -d '[:space:]' | openssl base64 | tr -d '[:space:]'`
+echo -e '\nENCODED PREPROPOSE MESSAGE'
+echo $ENCODED_PRE_PROPOSE
+
+MODULE_MSG='{
+        "allow_revoting": false,
+        "max_voting_period": {
+          "time": 604800
+        },
+        "close_proposal_on_execution_failure": true,
+        "pre_propose_info": {"ModuleMayPropose":{
+          "info": {
+          "admin": {
+            "core_module": {}
+          },
+          "code_id": 845,
+          "label": "pre propose module",
+          "msg": "'$ENCODED_PRE_PROPOSE'"
+        }}},
+        "only_members_execute": true,
+        "threshold": {
+          "threshold_quorum": {
+            "quorum": {
+              "percent": "0.20"
+            },
+            "threshold": {
+              "majority": {}
+            }
+          }
+        }
+      }'
+
+ENCODED_PROP_MESSAGE=`echo -n $MODULE_MSG | tr -d '[:space:]' | openssl base64 | tr -d '[:space:]'`
+echo -e '\nENCODED PROP MESSAGE'
+echo $ENCODED_PROP_MESSAGE
+
+VOTING_MSG='{"cw4_group_code_id":701,"initial_members":[{"addr":"juno1873my89qs478e56austefw0ewpp774xmq5m4xv","weight":30},{"addr":"juno16mrjtqffn3awme2eczhlpwzj7mnatkeluvhj6c","weight":1}]}'
+
+ENCODED_VOTING_MESSAGE=`echo $VOTING_MSG | tr -d '[:space:]' | openssl base64 | tr -d '[:space:]'`
+echo -e '\nENCODED VOTING MESSAGE'
+echo $ENCODED_VOTING_MESSAGE
+
+CW_CORE_INIT='{
+  "admin": "juno12jphyrpd82v8s8cq4n0nu7fa9qcx5hppdwevulhqdhyqu7vkrscs3sv2ct",
+  "automatically_add_cw20s": true,
+  "automatically_add_cw721s": true,
+  "description": "V2 DAO",
+  "name": "V2 DAO",
+  "proposal_modules_instantiate_info": [
+    {
+      "admin": {
+        "core_module": {}
+      },
+      "code_id": 844,
+      "label": "v2 dao",
+      "msg": "'$ENCODED_PROP_MESSAGE'"
+    }
+  ],
+  "voting_module_instantiate_info": {
+    "admin": {
+      "core_module": {}
+    },
+    "code_id": 846,
+    "label": "test_v2_dao-cw4-voting",
+    "msg": "'$ENCODED_VOTING_MESSAGE'"
+  }
+}'
+
+# encode
+CW_CORE_STRIPPED=`echo -n $CW_CORE_INIT | tr -d '[:space:]'`
+echo -e 'CW-CORE INSTANTIATE MESSAGE:\n'
+echo -$CW_CORE_STRIPPED 
+CW_CORE_ENCODED=`echo -n $CW_CORE_STRIPPED | openssl base64 | tr -d '[:space:]'`
+echo -e '\nCW-CORE ENCODED MESSAGE:\n'
+echo  $CW_CORE_ENCODED
+
+# init with factory
+INIT_MSG='{"instantiate_contract_with_self_admin":{"code_id":843, "label": "v2 subDAO subDAO", "instantiate_msg":"'$CW_CORE_ENCODED'"}}'
+
+# instantiate with factory 
+echo 'instantiating cw-core with factory'
+junod tx wasm execute juno143quaa25ynh6j5chhqh8w6lj647l4kpu5r6aqxvzul8du0mwyvns2g6u45 "$INIT_MSG" --from $KEY_NAME --node $NODE $TXFLAG
diff --git a/scripts/create-v2-dao.sh b/scripts/create-v2-dao.sh
new file mode 100755
index 00000000..3cccb312
--- /dev/null
+++ b/scripts/create-v2-dao.sh
@@ -0,0 +1,105 @@
+#!/bin/bash
+
+if [ "$1" = "" ]
+then
+  echo "Usage: $0 junod key name required as an argument, e.g. ./create-v2-dao-native-voting.sh mykeyname"
+  exit
+fi
+
+export CHAIN_ID="uni-5"
+export TESTNET_NAME="uni-5"
+export DENOM="ujunox"
+export BECH32_HRP="juno"
+export WASMD_VERSION="v0.20.0"
+export JUNOD_VERSION="v2.1.0"
+export CONFIG_DIR=".juno"
+export BINARY="junod"
+
+export COSMJS_VERSION="v0.26.5"
+export GENESIS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/genesis.json"
+export PERSISTENT_PEERS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/persistent_peers.txt"
+export SEEDS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/seeds.txt"
+
+export RPC="https://rpc.uni.juno.deuslabs.fi:443"
+export LCD="https://lcd.uni.juno.deuslabs.fi"
+export FAUCET="https://faucet.uni.juno.deuslabs.fi"
+
+export COSMOVISOR_VERSION="v0.1.0"
+export COSMOVISOR_HOME=$HOME/.juno
+export COSMOVISOR_NAME=junod
+
+export TXFLAG="--chain-id ${CHAIN_ID} --gas-prices 0.025ujunox --gas auto --gas-adjustment 1.3 --broadcast-mode block"
+export NODE="https://juno-testnet-rpc.polkachu.com:443"
+
+KEY_NAME=$1
+
+MODULE_MSG='{
+        "allow_revoting": false,
+        "max_voting_period": {
+          "time": 604800
+        },
+        "close_proposal_on_execution_failure": true,
+        "pre_propose_info": {"AnyoneMayPropose":{}},
+        "only_members_execute": true,
+        "threshold": {
+          "threshold_quorum": {
+            "quorum": {
+              "percent": "0.20"
+            },
+            "threshold": {
+              "majority": {}
+            }
+          }
+        }
+      }'
+
+ENCODED_PROP_MESSAGE=`echo -n $MODULE_MSG | tr -d '[:space:]' | openssl base64 | tr -d '[:space:]'`
+echo -e '\nENCODED PROP MESSAGE'
+echo $ENCODED_PROP_MESSAGE
+
+VOTING_MSG='{"cw4_group_code_id":701,"initial_members":[{"addr":"juno1873my89qs478e56austefw0ewpp774xmq5m4xv","weight":30},{"addr":"juno16mrjtqffn3awme2eczhlpwzj7mnatkeluvhj6c","weight":1}]}'
+
+ENCODED_VOTING_MESSAGE=`echo $VOTING_MSG | tr -d '[:space:]' | openssl base64 | tr -d '[:space:]'`
+echo -e '\nENCODED VOTING MESSAGE'
+echo $ENCODED_VOTING_MESSAGE
+
+CW_CORE_INIT='{
+  "admin": "juno12jphyrpd82v8s8cq4n0nu7fa9qcx5hppdwevulhqdhyqu7vkrscs3sv2ct",
+  "automatically_add_cw20s": true,
+  "automatically_add_cw721s": true,
+  "description": "V2 DAO",
+  "name": "V2 DAO",
+  "proposal_modules_instantiate_info": [
+    {
+      "admin": {
+        "core_module": {}
+      },
+      "code_id": 696,
+      "label": "v2 dao",
+      "msg": "'$ENCODED_PROP_MESSAGE'"
+    }
+  ],
+  "voting_module_instantiate_info": {
+    "admin": {
+      "core_module": {}
+    },
+    "code_id": 698,
+    "label": "test_v2_dao-cw4-voting",
+    "msg": "'$ENCODED_VOTING_MESSAGE'"
+  }
+}'
+
+# encode
+CW_CORE_STRIPPED=`echo -n $CW_CORE_INIT | tr -d '[:space:]'`
+echo -e 'CW-CORE INSTANTIATE MESSAGE:\n'
+echo -$CW_CORE_STRIPPED 
+CW_CORE_ENCODED=`echo -n $CW_CORE_STRIPPED | openssl base64 | tr -d '[:space:]'`
+echo -e '\nCW-CORE ENCODED MESSAGE:\n'
+echo  $CW_CORE_ENCODED
+
+# init with factory
+INIT_MSG='{"instantiate_contract_with_self_admin":{"code_id":695, "label": "v2 subDAO subDAO", "instantiate_msg":"'$CW_CORE_ENCODED'"}}'
+
+# instantiate with factory 
+echo 'instantiating cw-core with factory'
+junod tx wasm execute juno143quaa25ynh6j5chhqh8w6lj647l4kpu5r6aqxvzul8du0mwyvns2g6u45 "$INIT_MSG" --from $KEY_NAME --node $NODE $TXFLAG
diff --git a/scripts/update-v2-subdaos.sh b/scripts/update-v2-subdaos.sh
new file mode 100755
index 00000000..84421d23
--- /dev/null
+++ b/scripts/update-v2-subdaos.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+if [ "$1" = "" ]
+then
+  echo "Usage: $0 junod key name required as an argument, e.g. ./create-v2-dao-native-voting.sh mykeyname"
+  exit
+fi
+
+export CHAIN_ID="uni-5"
+export TESTNET_NAME="uni-5"
+export DENOM="ujunox"
+export BECH32_HRP="juno"
+export WASMD_VERSION="v0.20.0"
+export JUNOD_VERSION="v2.1.0"
+export CONFIG_DIR=".juno"
+export BINARY="junod"
+
+export COSMJS_VERSION="v0.26.5"
+export GENESIS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/genesis.json"
+export PERSISTENT_PEERS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/persistent_peers.txt"
+export SEEDS_URL="https://raw.githubusercontent.com/CosmosContracts/testnets/main/uni-2/seeds.txt"
+
+export RPC="https://rpc.uni.juno.deuslabs.fi:443"
+export LCD="https://lcd.uni.juno.deuslabs.fi"
+export FAUCET="https://faucet.uni.juno.deuslabs.fi"
+
+export COSMOVISOR_VERSION="v0.1.0"
+export COSMOVISOR_HOME=$HOME/.juno
+export COSMOVISOR_NAME=junod
+
+export TXFLAG="--chain-id ${CHAIN_ID} --gas-prices 0.025ujunox --gas auto --gas-adjustment 1.3 --broadcast-mode block"
+export NODE="https://juno-testnet-rpc.polkachu.com:443"
+
+UPDATE_SUBDAOS_MESSAGE='{"update_sub_daos": {"to_add": [{"addr": "juno1xvkad3623mnrhrse6y0atuj0sqk7ugveqxzu0xll64u230ugckaqvh2z92", "charter": "The test v2 subDAO."}], "to_remove":[]}}'
+PARENT_DAO_ADDRESS="juno165enxtrex9lhukghct277umy3tcrvhj5p3rrnkw0q34rckgznekse8vlz7"
+PROPOSAL_MODULE_ADDRESS="juno1gxst20k2k8xjwjjaw8vrg340zy9aqym7evk8gmut54nxevluey9s2x7tmy"
+KEY_NAME=$1
+
+ENCODED=`echo -n $UPDATE_SUBDAOS_MESSAGE | openssl base64 | tr -d '[:space:]'`
+
+EXECUTE_PROPOSE_MESSAGE='{"propose": {
+                "description": "update subdaos", 
+                "msgs": [
+                  {
+                    "wasm": {
+                      "execute": {
+                        "contract_addr": "'$PARENT_DAO_ADDRESS'",
+                        "funds": [],
+                        "msg": "'$ENCODED'"
+                      }
+                    }
+                  }
+                ],
+                "title": "Update subDAOs list"
+              }}'
+
+echo $EXECUTE_PROPOSE_MESSAGE
+
+junod tx wasm execute $PROPOSAL_MODULE_ADDRESS "$EXECUTE_PROPOSE_MESSAGE" $TXFLAG --node $NODE --from $KEY_NAME
+
+VOTING_MESSAGE='{"vote": {
+  "proposal_id": 1,
+  "vote": "yes"
+}}'
+
+junod tx wasm execute $PROPOSAL_MODULE_ADDRESS "$VOTING_MESSAGE" $TXFLAG --node $NODE --from $KEY_NAME
+
+EXECUTE_MSG='{"execute": {"proposal_id":1}}'
+
+junod tx wasm execute $PROPOSAL_MODULE_ADDRESS '{"execute": {"proposal_id":1}}' $TXFLAG --node $NODE --from $KEY_NAME
+
+junod query wasm contract-state smart $PARENT_DAO_ADDRESS '{"list_sub_daos":{}}' --node $NODE
+
+LIST_SUBDAOS='{"list_sub_daos":{}}'
+junod query wasm contract $PARENT_DAO_ADDRESS '{"list_sub_daos":{}}'--node $NODE
\ No newline at end of file
diff --git a/typescript/contracts/cwd-proposal-single/CwdProposalSingle.client.ts b/typescript/contracts/cwd-proposal-single/CwdProposalSingle.client.ts
index 3718f56e..eb83f031 100644
--- a/typescript/contracts/cwd-proposal-single/CwdProposalSingle.client.ts
+++ b/typescript/contracts/cwd-proposal-single/CwdProposalSingle.client.ts
@@ -195,11 +195,20 @@ export interface CwdProposalSingleInterface extends CwdProposalSingleReadOnlyInt
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   vote: ({
     proposalId,
+    rationale,
     vote
   }: {
     proposalId: number;
+    rationale?: string;
     vote: Vote;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  updateRationale: ({
+    proposalId,
+    rationale
+  }: {
+    proposalId: number;
+    rationale?: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   execute: ({
     proposalId
   }: {
@@ -265,6 +274,7 @@ export class CwdProposalSingleClient extends CwdProposalSingleQueryClient implem
     this.contractAddress = contractAddress;
     this.propose = this.propose.bind(this);
     this.vote = this.vote.bind(this);
+    this.updateRationale = this.updateRationale.bind(this);
     this.execute = this.execute.bind(this);
     this.close = this.close.bind(this);
     this.updateConfig = this.updateConfig.bind(this);
@@ -297,18 +307,35 @@ export class CwdProposalSingleClient extends CwdProposalSingleQueryClient implem
   };
   vote = async ({
     proposalId,
+    rationale,
     vote
   }: {
     proposalId: number;
+    rationale?: string;
     vote: Vote;
   }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
     return await this.client.execute(this.sender, this.contractAddress, {
       vote: {
         proposal_id: proposalId,
+        rationale,
         vote
       }
     }, fee, memo, funds);
   };
+  updateRationale = async ({
+    proposalId,
+    rationale
+  }: {
+    proposalId: number;
+    rationale?: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      update_rationale: {
+        proposal_id: proposalId,
+        rationale
+      }
+    }, fee, memo, funds);
+  };
   execute = async ({
     proposalId
   }: {
diff --git a/typescript/contracts/cwd-proposal-single/CwdProposalSingle.types.ts b/typescript/contracts/cwd-proposal-single/CwdProposalSingle.types.ts
index 76b1fe01..334c146e 100644
--- a/typescript/contracts/cwd-proposal-single/CwdProposalSingle.types.ts
+++ b/typescript/contracts/cwd-proposal-single/CwdProposalSingle.types.ts
@@ -70,8 +70,14 @@ export type ExecuteMsg = {
 } | {
   vote: {
     proposal_id: number;
+    rationale?: string | null;
     vote: Vote;
   };
+} | {
+  update_rationale: {
+    proposal_id: number;
+    rationale?: string | null;
+  };
 } | {
   execute: {
     proposal_id: number;
@@ -323,6 +329,7 @@ export interface VoteResponse {
 }
 export interface VoteInfo {
   power: Uint128;
+  rationale?: string | null;
   vote: Vote;
   voter: Addr;
 }


Test patch:
diff --git a/contracts/pre-propose/cwd-pre-propose-single/src/tests.rs b/contracts/pre-propose/cwd-pre-propose-single/src/tests.rs
index c4c9c4b5..10c85470 100644
--- a/contracts/pre-propose/cwd-pre-propose-single/src/tests.rs
+++ b/contracts/pre-propose/cwd-pre-propose-single/src/tests.rs
@@ -261,6 +261,7 @@ fn vote(app: &mut App, module: Addr, sender: &str, id: u64, position: Vote) -> S
         Addr::unchecked(sender),
         module.clone(),
         &cps::msg::ExecuteMsg::Vote {
+            rationale: None,
             proposal_id: id,
             vote: position,
         },
diff --git a/contracts/proposal/cwd-proposal-single/src/testing/do_votes.rs b/contracts/proposal/cwd-proposal-single/src/testing/do_votes.rs
index 59bb9177..04eabdbe 100644
--- a/contracts/proposal/cwd-proposal-single/src/testing/do_votes.rs
+++ b/contracts/proposal/cwd-proposal-single/src/testing/do_votes.rs
@@ -214,6 +214,7 @@ where
             &ExecuteMsg::Vote {
                 proposal_id: 1,
                 vote: position,
+                rationale: None,
             },
             &[],
         );
@@ -233,6 +234,7 @@ where
                     .unwrap();
                 let expected = VoteResponse {
                     vote: Some(VoteInfo {
+                        rationale: None,
                         voter: Addr::unchecked(&voter),
                         vote: position,
                         power: match deposit_config.deposit_info {
diff --git a/contracts/proposal/cwd-proposal-single/src/testing/execute.rs b/contracts/proposal/cwd-proposal-single/src/testing/execute.rs
index 8407e8d4..3235cef9 100644
--- a/contracts/proposal/cwd-proposal-single/src/testing/execute.rs
+++ b/contracts/proposal/cwd-proposal-single/src/testing/execute.rs
@@ -132,7 +132,11 @@ pub(crate) fn vote_on_proposal(
     app.execute_contract(
         Addr::unchecked(sender),
         proposal_single.clone(),
-        &ExecuteMsg::Vote { proposal_id, vote },
+        &ExecuteMsg::Vote {
+            proposal_id,
+            vote,
+            rationale: None,
+        },
         &[],
     )
     .unwrap();
@@ -148,7 +152,11 @@ pub(crate) fn vote_on_proposal_should_fail(
     app.execute_contract(
         Addr::unchecked(sender),
         proposal_single.clone(),
-        &ExecuteMsg::Vote { proposal_id, vote },
+        &ExecuteMsg::Vote {
+            proposal_id,
+            vote,
+            rationale: None,
+        },
         &[],
     )
     .unwrap_err()
@@ -173,6 +181,46 @@ pub(crate) fn execute_proposal_should_fail(
     .unwrap()
 }
 
+pub(crate) fn vote_on_proposal_with_rationale(
+    app: &mut App,
+    proposal_single: &Addr,
+    sender: &str,
+    proposal_id: u64,
+    vote: Vote,
+    rationale: Option<String>,
+) {
+    app.execute_contract(
+        Addr::unchecked(sender),
+        proposal_single.clone(),
+        &ExecuteMsg::Vote {
+            proposal_id,
+            vote,
+            rationale,
+        },
+        &[],
+    )
+    .unwrap();
+}
+
+pub(crate) fn update_rationale(
+    app: &mut App,
+    proposal_single: &Addr,
+    sender: &str,
+    proposal_id: u64,
+    rationale: Option<String>,
+) {
+    app.execute_contract(
+        Addr::unchecked(sender),
+        proposal_single.clone(),
+        &ExecuteMsg::UpdateRationale {
+            proposal_id,
+            rationale,
+        },
+        &[],
+    )
+    .unwrap();
+}
+
 pub(crate) fn execute_proposal(
     app: &mut App,
     proposal_single: &Addr,
diff --git a/contracts/proposal/cwd-proposal-single/src/testing/queries.rs b/contracts/proposal/cwd-proposal-single/src/testing/queries.rs
index b8d42784..516c1c8a 100644
--- a/contracts/proposal/cwd-proposal-single/src/testing/queries.rs
+++ b/contracts/proposal/cwd-proposal-single/src/testing/queries.rs
@@ -8,7 +8,7 @@ use cwd_voting::pre_propose::ProposalCreationPolicy;
 
 use crate::{
     msg::QueryMsg,
-    query::{ProposalListResponse, ProposalResponse, VoteListResponse},
+    query::{ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse},
     state::Config,
 };
 
@@ -72,6 +72,23 @@ pub(crate) fn query_list_votes(
         .unwrap()
 }
 
+pub(crate) fn query_vote(
+    app: &App,
+    proposal_module: &Addr,
+    who: &str,
+    proposal_id: u64,
+) -> VoteResponse {
+    app.wrap()
+        .query_wasm_smart(
+            proposal_module,
+            &QueryMsg::GetVote {
+                proposal_id,
+                voter: who.to_string(),
+            },
+        )
+        .unwrap()
+}
+
 pub(crate) fn query_proposal_hooks(app: &App, proposal_single: &Addr) -> HooksResponse {
     app.wrap()
         .query_wasm_smart(proposal_single, &QueryMsg::ProposalHooks {})
diff --git a/contracts/proposal/cwd-proposal-single/src/testing/tests.rs b/contracts/proposal/cwd-proposal-single/src/testing/tests.rs
index 2090b943..2a6a6336 100644
--- a/contracts/proposal/cwd-proposal-single/src/testing/tests.rs
+++ b/contracts/proposal/cwd-proposal-single/src/testing/tests.rs
@@ -44,7 +44,7 @@ use crate::{
             execute_proposal, execute_proposal_should_fail, instantiate_cw20_base_default,
             make_proposal, mint_cw20s, mint_natives, remove_proposal_hook,
             remove_proposal_hook_should_fail, remove_vote_hook, remove_vote_hook_should_fail,
-            vote_on_proposal, vote_on_proposal_should_fail,
+            update_rationale, vote_on_proposal, vote_on_proposal_should_fail,
         },
         instantiate::{
             get_default_non_token_dao_proposal_module_instantiate,
@@ -64,7 +64,10 @@ use crate::{
     ContractError,
 };
 
-use super::{do_votes::do_votes_staked_balances, CREATOR_ADDR};
+use super::{
+    do_votes::do_votes_staked_balances, execute::vote_on_proposal_with_rationale,
+    queries::query_vote, CREATOR_ADDR,
+};
 
 struct CommonTest {
     app: App,
@@ -1734,6 +1737,19 @@ fn test_migrate_from_v1() {
         }
     );
 
+    // Make sure we can still query for ballots (rationale works post
+    // migration).
+    let vote = query_vote(&app, &proposal_module, CREATOR_ADDR, 1);
+    assert_eq!(
+        vote.vote.unwrap(),
+        VoteInfo {
+            voter: Addr::unchecked(CREATOR_ADDR),
+            vote: Vote::No,
+            power: Uint128::new(100),
+            rationale: None
+        }
+    );
+
     let proposal_creation_policy = query_creation_policy(&app, &proposal_module);
 
     // Check that a new creation policy has been birthed.
@@ -2226,26 +2242,31 @@ fn test_query_list_votes() {
         votes.votes,
         vec![
             VoteInfo {
+                rationale: None,
                 voter: Addr::unchecked("five"),
                 vote: Vote::Yes,
                 power: Uint128::new(1)
             },
             VoteInfo {
+                rationale: None,
                 voter: Addr::unchecked("four"),
                 vote: Vote::Yes,
                 power: Uint128::new(1)
             },
             VoteInfo {
+                rationale: None,
                 voter: Addr::unchecked("one"),
                 vote: Vote::Yes,
                 power: Uint128::new(1)
             },
             VoteInfo {
+                rationale: None,
                 voter: Addr::unchecked("three"),
                 vote: Vote::No,
                 power: Uint128::new(1)
             },
             VoteInfo {
+                rationale: None,
                 voter: Addr::unchecked("two"),
                 vote: Vote::No,
                 power: Uint128::new(1)
@@ -2264,11 +2285,13 @@ fn test_query_list_votes() {
         votes.votes,
         vec![
             VoteInfo {
+                rationale: None,
                 voter: Addr::unchecked("one"),
                 vote: Vote::Yes,
                 power: Uint128::new(1)
             },
             VoteInfo {
+                rationale: None,
                 voter: Addr::unchecked("three"),
                 vote: Vote::No,
                 power: Uint128::new(1)
@@ -2450,7 +2473,86 @@ fn test_pre_propose_admin_is_dao() {
     assert_eq!(info.admin, Some(core_addr.into_string()));
 }
 
-// TODO: test pre-propose module that fails on new proposal hook (ugh).
+// I can add a rationale to my vote. My rational is queryable when
+// listing votes. I can later change my rationale.
+#[test]
+fn test_rationale() {
+    let CommonTest {
+        mut app,
+        proposal_module,
+        proposal_id,
+        ..
+    } = setup_test(vec![]);
+
+    let rationale = Some("i support dog charities".to_string());
+
+    vote_on_proposal_with_rationale(
+        &mut app,
+        &proposal_module,
+        CREATOR_ADDR,
+        proposal_id,
+        Vote::Yes,
+        rationale.clone(),
+    );
+
+    let vote = query_vote(&app, &proposal_module, CREATOR_ADDR, proposal_id);
+    assert_eq!(vote.vote.unwrap().rationale, rationale);
+
+    let rationale =
+        Some("i did not realize that dog charity was gambling with customer funds".to_string());
+
+    update_rationale(
+        &mut app,
+        &proposal_module,
+        CREATOR_ADDR,
+        proposal_id,
+        rationale.clone(),
+    );
+
+    let vote = query_vote(&app, &proposal_module, CREATOR_ADDR, proposal_id);
+    assert_eq!(vote.vote.unwrap().rationale, rationale);
+}
+
+// Revoting should override any previous rationale. If no new
+// rationalle is provided, the old one will be wiped regardless.
+#[test]
+fn test_rational_clobbered_on_revote() {
+    let mut app = App::default();
+    let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app);
+    instantiate.allow_revoting = true;
+    let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None);
+    let gov_token = query_dao_token(&app, &core_addr);
+    let proposal_module = query_single_proposal_module(&app, &core_addr);
+
+    mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000);
+    let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]);
+
+    let rationale = Some("to_string".to_string());
+
+    vote_on_proposal_with_rationale(
+        &mut app,
+        &proposal_module,
+        CREATOR_ADDR,
+        proposal_id,
+        Vote::Yes,
+        rationale.clone(),
+    );
+
+    let vote = query_vote(&app, &proposal_module, CREATOR_ADDR, proposal_id);
+    assert_eq!(vote.vote.unwrap().rationale, rationale);
+
+    let rationale = None;
 
-// - What happens if you have proposals that can not be executed but
-//   took deposits and want to migrate?
+    // revote and clobber.
+    vote_on_proposal_with_rationale(
+        &mut app,
+        &proposal_module,
+        CREATOR_ADDR,
+        proposal_id,
+        Vote::No,
+        None,
+    );
+
+    let vote = query_vote(&app, &proposal_module, CREATOR_ADDR, proposal_id);
+    assert_eq!(vote.vote.unwrap().rationale, rationale);
+}
diff --git a/test-contracts/cwd-proposal-hook-counter/src/tests.rs b/test-contracts/cwd-proposal-hook-counter/src/tests.rs
index 0ae408d4..36e89a04 100644
--- a/test-contracts/cwd-proposal-hook-counter/src/tests.rs
+++ b/test-contracts/cwd-proposal-hook-counter/src/tests.rs
@@ -285,6 +285,7 @@ fn test_counters() {
         &cwd_proposal_single::msg::ExecuteMsg::Vote {
             proposal_id: 1,
             vote: Vote::Yes,
+            rationale: None,
         },
         &[],
     )
@@ -402,6 +403,7 @@ fn test_counters() {
         Addr::unchecked(CREATOR_ADDR),
         govmod_single.clone(),
         &cwd_proposal_single::msg::ExecuteMsg::Vote {
+            rationale: None,
             proposal_id: 2,
             vote: Vote::Yes,
         },