Repository: DA0-DA0/dao-contracts
Pull request #549 (2022-11-22 01:50:54)

Improve docs
This is a PR to improve docs!

We now include the README for each package and contract in `lib.rs`. This has two benefits:
- `README.md` is included in docs generated with `cargo doc`
- Code in `README.md` files is tested

github-actions[bot]: <h1>Cosm-Orc Gas Usage</h1> 
 
 No gas diff larger than 0.5% 
 
 <details><summary>Raw Report for 9d962530dca985d65806e62df787d2d4b21c0338 </summary><br/> 

| Contract | Op Name | Gas Used | Gas Wanted | File | 
 | --- | --- | --- | --- | --- |
| cwd_core | Instantiate__inst_admin_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 | Store__Store | 45996040 | 68969420 | ci/integration-tests/src/helpers/chain.rs:98 |
| cwd_core | Instantiate__inst_dao_no_admin | 1200254 | 1775769 | 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 | 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 |
| cw20_base | Execute__exc_stake_stake_tokens | 234578 | 319223 | 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 | 29285060 | 43902950 | ci/integration-tests/src/helpers/chain.rs:98 |
| cw4_group | Store__Store | 23034310 | 34526825 | ci/integration-tests/src/helpers/chain.rs:98 |
| cw721_base | Store__Store | 28006640 | 41985320 | 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 |
| 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_cw721_staked | Store__Store | 27602830 | 41379605 | 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_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/README b/contracts/README.md
similarity index 100%
rename from contracts/README
rename to contracts/README.md
diff --git a/contracts/cwd-core/README b/contracts/cwd-core/README.md
similarity index 100%
rename from contracts/cwd-core/README
rename to contracts/cwd-core/README.md
diff --git a/contracts/cwd-core/src/contract.rs b/contracts/cwd-core/src/contract.rs
index a8fb9e8f..19045520 100644
--- a/contracts/cwd-core/src/contract.rs
+++ b/contracts/cwd-core/src/contract.rs
@@ -613,13 +613,13 @@ pub fn query_proposal_modules(
     )?)
 }
 
-/// Note: this is not gas efficient as we need to potentially visit all modules in order to
-/// filter out the modules with active status.
 pub fn query_active_proposal_modules(
     deps: Deps,
     start_after: Option<String>,
     limit: Option<u32>,
 ) -> StdResult<Binary> {
+    // Note: this is not gas efficient as we need to potentially visit all modules in order to
+    // filter out the modules with active status.
     let values = paginate_map_values(
         deps,
         &PROPOSAL_MODULES,
diff --git a/contracts/cwd-core/src/lib.rs b/contracts/cwd-core/src/lib.rs
index bbf4fedd..190d0033 100644
--- a/contracts/cwd-core/src/lib.rs
+++ b/contracts/cwd-core/src/lib.rs
@@ -1,17 +1,4 @@
-//! # cw-core
-//!
-//! This contract is the core module for all DAO DAO DAOs. It handles
-//! management of voting power and proposal modules, executes messages,
-//! and holds the DAO's treasury.
-//!
-//! For more information about how these modules fit together see
-//! [this](https://!github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-v1-Contracts-Design)
-//! wiki page.
-//!
-//! In additon to the wiki spec this contract may also pause. To do so a
-//! `Pause` message must by executed by a proposal module. Pausing the
-//! core module will stop all actions on the module for the duration of
-//! the pause.
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
 
 pub mod contract;
 mod error;
diff --git a/contracts/external/cw-admin-factory/src/lib.rs b/contracts/external/cw-admin-factory/src/lib.rs
index f07717bb..ea90e3aa 100644
--- a/contracts/external/cw-admin-factory/src/lib.rs
+++ b/contracts/external/cw-admin-factory/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
diff --git a/contracts/external/cw-token-swap/src/lib.rs b/contracts/external/cw-token-swap/src/lib.rs
index 8e58a2f1..d1800adb 100644
--- a/contracts/external/cw-token-swap/src/lib.rs
+++ b/contracts/external/cw-token-swap/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
diff --git a/contracts/pre-propose/README b/contracts/pre-propose/README.md
similarity index 100%
rename from contracts/pre-propose/README
rename to contracts/pre-propose/README.md
diff --git a/contracts/pre-propose/cwd-pre-propose-multiple/src/lib.rs b/contracts/pre-propose/cwd-pre-propose-multiple/src/lib.rs
index 75a1d11b..15ad11d1 100644
--- a/contracts/pre-propose/cwd-pre-propose-multiple/src/lib.rs
+++ b/contracts/pre-propose/cwd-pre-propose-multiple/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 
 #[cfg(test)]
diff --git a/contracts/pre-propose/cwd-pre-propose-single/src/lib.rs b/contracts/pre-propose/cwd-pre-propose-single/src/lib.rs
index 75a1d11b..15ad11d1 100644
--- a/contracts/pre-propose/cwd-pre-propose-single/src/lib.rs
+++ b/contracts/pre-propose/cwd-pre-propose-single/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 
 #[cfg(test)]
diff --git a/contracts/proposal/cwd-proposal-multiple/src/lib.rs b/contracts/proposal/cwd-proposal-multiple/src/lib.rs
index ff50ff85..9841649c 100644
--- a/contracts/proposal/cwd-proposal-multiple/src/lib.rs
+++ b/contracts/proposal/cwd-proposal-multiple/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
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/lib.rs b/contracts/proposal/cwd-proposal-single/src/lib.rs
index 72982c0f..c9076cf7 100644
--- a/contracts/proposal/cwd-proposal-single/src/lib.rs
+++ b/contracts/proposal/cwd-proposal-single/src/lib.rs
@@ -1,40 +1,4 @@
-//! # cw-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
-//! executed by the core module upon the proposal being passed and
-//! executed.
-//!
-//! For more information about how these modules fit together see
-//! [this](https://!github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-v1-Contracts-Design)
-//! wiki page.
-//!
-//! For information about how this module counts votes and handles passing
-//! thresholds see
-//! [this](https://!github.com/DA0-DA0/dao-contracts/wiki/A-brief-overview-of-DAO-DAO-voting#proposal-status)
-//! wiki page.
-//!
-//! ## Proposal deposits
-//!
-//! Proposal deposits for this module are handled by the
-//! [`cwd-pre-propose-single`](../../pre-propose/cwd-pre-propose-single)
-//! contract.
-//!
-//! ## Hooks
-//!
-//! This module supports hooks for voting and proposal status changes. One
-//! may register a contract to receive these hooks with the `AddVoteHook`
-//! and `AddProposalHook` methods. Upon registration the contract will
-//! receive messages whenever a vote is cast and a proposal's status
-//! changes (for example, when the proposal passes).
-//!
-//! The format for these hook messages can be located in the
-//! `proposal-hooks` and `vote-hooks` packages located in
-//! `packages/proposal-hooks` and `packages/vote-hooks` respectively.
-//!
-//! To stop an invalid hook receiver from locking the proposal module
-//! receivers will be removed from the hook list if they error when
-//! handling a hook.
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
 
 pub mod contract;
 mod error;
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-external-rewards/src/lib.rs b/contracts/staking/cw20-stake-external-rewards/src/lib.rs
index dfedc9dc..595daabe 100644
--- a/contracts/staking/cw20-stake-external-rewards/src/lib.rs
+++ b/contracts/staking/cw20-stake-external-rewards/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
diff --git a/contracts/staking/cw20-stake-reward-distributor/src/lib.rs b/contracts/staking/cw20-stake-reward-distributor/src/lib.rs
index 8e58a2f1..d1800adb 100644
--- a/contracts/staking/cw20-stake-reward-distributor/src/lib.rs
+++ b/contracts/staking/cw20-stake-reward-distributor/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
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..6688af2c 100644
--- a/contracts/staking/cw20-stake/src/lib.rs
+++ b/contracts/staking/cw20-stake/src/lib.rs
@@ -1,6 +1,9 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 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/lib.rs b/contracts/voting/cwd-voting-cw20-staked/src/lib.rs
index 8e58a2f1..d1800adb 100644
--- a/contracts/voting/cwd-voting-cw20-staked/src/lib.rs
+++ b/contracts/voting/cwd-voting-cw20-staked/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
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/contracts/voting/cwd-voting-cw4/src/lib.rs b/contracts/voting/cwd-voting-cw4/src/lib.rs
index 8e58a2f1..d1800adb 100644
--- a/contracts/voting/cwd-voting-cw4/src/lib.rs
+++ b/contracts/voting/cwd-voting-cw4/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
diff --git a/contracts/voting/cwd-voting-cw721-staked/src/lib.rs b/contracts/voting/cwd-voting-cw721-staked/src/lib.rs
index fccce995..51ae5c61 100644
--- a/contracts/voting/cwd-voting-cw721-staked/src/lib.rs
+++ b/contracts/voting/cwd-voting-cw721-staked/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod hooks;
diff --git a/contracts/voting/cwd-voting-native-staked/src/lib.rs b/contracts/voting/cwd-voting-native-staked/src/lib.rs
index 8e58a2f1..d1800adb 100644
--- a/contracts/voting/cwd-voting-native-staked/src/lib.rs
+++ b/contracts/voting/cwd-voting-native-staked/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
diff --git a/contracts/voting/cwd-voting-staking-denom-staked/src/lib.rs b/contracts/voting/cwd-voting-staking-denom-staked/src/lib.rs
index 8e58a2f1..d1800adb 100644
--- a/contracts/voting/cwd-voting-staking-denom-staked/src/lib.rs
+++ b/contracts/voting/cwd-voting-staking-denom-staked/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 mod error;
 pub mod msg;
diff --git a/packages/cw-denom/README b/packages/cw-denom/README.md
similarity index 71%
rename from packages/cw-denom/README
rename to packages/cw-denom/README.md
index 437cc548..1ce89ac6 100644
--- a/packages/cw-denom/README
+++ b/packages/cw-denom/README.md
@@ -5,12 +5,10 @@ denominations. It proves the types, `UncheckedDenom` and
 `CheckedDenom`. `UncheckedDenom` may be used in CosmWasm contract
 messages and checked via the `into_checked` method.
 
-To validate native denominations, this package uses the same rules
-as the SDK [1].
+To validate native denominations, this package uses the [same
+rules](https://github.com/cosmos/cosmos-sdk/blob/7728516abfab950dc7a9120caad4870f1f962df5/types/coin.go#L865-L867) as the SDK.
 
 To validate cw20 denominations this package ensures that the
 specified address is valid, that the specified address is a
 CosmWasm contract, and that the specified address responds
 correctly to cw20 `TokenInfo` queries.
-
-[1] https://github.com/cosmos/cosmos-sdk/blob/7728516abfab950dc7a9120caad4870f1f962df5/types/coin.go#L865-L867
\ No newline at end of file
diff --git a/packages/cw-denom/src/lib.rs b/packages/cw-denom/src/lib.rs
index fa44c1ca..f582d7fa 100644
--- a/packages/cw-denom/src/lib.rs
+++ b/packages/cw-denom/src/lib.rs
@@ -1,16 +1,4 @@
-//! This is a simple package for validating cw20 and Cosmos SDK native
-//! denominations. It proves the types, `UncheckedDenom` and
-//! `CheckedDenom`. `UncheckedDenom` may be used in CosmWasm contract
-//! messages and checked via the `into_checked` method.
-//!
-//! To validate native denominations, this package uses the [same
-//! rules](https://github.com/cosmos/cosmos-sdk/blob/7728516abfab950dc7a9120caad4870f1f962df5/types/coin.go#L865-L867)
-//! as the SDK.
-//!
-//! To validate cw20 denominations this package ensures that the
-//! specified address is valid, that the specified address is a
-//! CosmWasm contract, and that the specified address responds
-//! correctly to cw20 `TokenInfo` queries.
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
 
 #[cfg(test)]
 mod integration_tests;
diff --git a/packages/cw-paginate/README.md b/packages/cw-paginate/README.md
index ecf08f35..51a178a3 100644
--- a/packages/cw-paginate/README.md
+++ b/packages/cw-paginate/README.md
@@ -10,12 +10,20 @@ happy](https://twitter.com/larry0x/status/1530537243709939719).
 Given a map like:
 
 ```rust
+use cw_storage_plus::Map;
+
 pub const ITEMS: Map<String, String> = Map::new("items");
 ```
 
 You can use this package to write a query to list it's contents like:
 
 ```rust
+use cosmwasm_std::{Deps, Binary, to_binary, StdResult};
+use cw_storage_plus::Map;
+use cw_paginate::paginate_map;                         
+
+pub const ITEMS: Map<String, String> = Map::new("items");
+
 pub fn query_list_items(
     deps: Deps,
     start_after: Option<String>,
diff --git a/packages/cw-paginate/src/lib.rs b/packages/cw-paginate/src/lib.rs
index 1453c797..66a0d046 100644
--- a/packages/cw-paginate/src/lib.rs
+++ b/packages/cw-paginate/src/lib.rs
@@ -1,43 +1,4 @@
-//! # CosmWasm Map Pagination
-//
-//! This package provides generic convienence methods for paginating keys
-//! and values in a CosmWasm `Maop` or `SnapshotMap`. If you use these
-//! methods to paginate the maps in your contract you may [make larry0x
-//! happy](https://twitter.com/larry0x/status/1530537243709939719).
-//
-//! ## Example
-//
-//! Given a map like:
-//
-//! ```rust
-//! # use cw_storage_plus::Map;
-//!
-//! pub const ITEMS: Map<String, String> = Map::new("items");
-//! ```
-//
-//! You can use this package to write a query to list it's contents like:
-//
-//! ```rust
-//! # use cosmwasm_std::{Deps, Binary, to_binary, StdResult};
-//! # use cw_storage_plus::Map;
-//! # use cw_paginate::paginate_map;
-//!
-//! # pub const ITEMS: Map<String, String> = Map::new("items");
-//!
-//! pub fn query_list_items(
-//!     deps: Deps,
-//!     start_after: Option<String>,
-//!     limit: Option<u32>,
-//! ) -> StdResult<Binary> {
-//!     to_binary(&paginate_map(
-//!         deps,
-//!         &ITEMS,
-//!         start_after,
-//!         limit,
-//!         cosmwasm_std::Order::Descending,
-//!     )?)
-//! }
-//!  ```
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
 
 use cosmwasm_std::{Deps, Order, StdResult};
 
diff --git a/packages/cw721-controllers/src/lib.rs b/packages/cw721-controllers/src/lib.rs
index 89909830..d4f58091 100644
--- a/packages/cw721-controllers/src/lib.rs
+++ b/packages/cw721-controllers/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 mod nft_claim;
 
 pub use nft_claim::{NftClaim, NftClaims, NftClaimsResponse};
diff --git a/packages/cwd-hooks/src/lib.rs b/packages/cwd-hooks/src/lib.rs
index df9f90f8..14bff308 100644
--- a/packages/cwd-hooks/src/lib.rs
+++ b/packages/cwd-hooks/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 use thiserror::Error;
 
 use cosmwasm_schema::cw_serde;
diff --git a/packages/cwd-interface/src/lib.rs b/packages/cwd-interface/src/lib.rs
index 2e69b364..ec842abb 100644
--- a/packages/cwd-interface/src/lib.rs
+++ b/packages/cwd-interface/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 use cosmwasm_schema::cw_serde;
 use cosmwasm_std::{Addr, Binary, CosmosMsg, Empty, WasmMsg};
 
diff --git a/packages/cwd-macros/README.md b/packages/cwd-macros/README.md
index 5cd49cc9..c6761f12 100644
--- a/packages/cwd-macros/README.md
+++ b/packages/cwd-macros/README.md
@@ -5,9 +5,15 @@ derive DAO module interfaces on message enums. For example, to derive
 the voting module interface on an enum:
 
 ```rust
+use cosmwasm_schema::{cw_serde, QueryResponses};
+use cwd_macros::{token_query, voting_query, info_query};
+use cwd_interface::voting::TotalPowerAtHeightResponse;
+use cwd_interface::voting::VotingPowerAtHeightResponse;
+
 #[token_query]
 #[voting_query]
 #[info_query]
 #[cw_serde]
+#[derive(QueryResponses)]
 pub enum Query {}
 ```
diff --git a/packages/cwd-macros/src/lib.rs b/packages/cwd-macros/src/lib.rs
index 09906c17..161af767 100644
--- a/packages/cwd-macros/src/lib.rs
+++ b/packages/cwd-macros/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 use proc_macro::TokenStream;
 
 use quote::quote;
diff --git a/packages/cwd-pre-propose-base/src/lib.rs b/packages/cwd-pre-propose-base/src/lib.rs
index edd273f7..e95d8f60 100644
--- a/packages/cwd-pre-propose-base/src/lib.rs
+++ b/packages/cwd-pre-propose-base/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod error;
 pub mod execute;
 pub mod msg;
diff --git a/packages/cwd-proposal-hooks/src/lib.rs b/packages/cwd-proposal-hooks/src/lib.rs
index fad07bbe..55175742 100644
--- a/packages/cwd-proposal-hooks/src/lib.rs
+++ b/packages/cwd-proposal-hooks/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 use cosmwasm_schema::cw_serde;
 use cosmwasm_std::{to_binary, StdResult, Storage, SubMsg, WasmMsg};
 use cwd_hooks::Hooks;
diff --git a/packages/cwd-vote-hooks/src/lib.rs b/packages/cwd-vote-hooks/src/lib.rs
index 39d17169..168e163b 100644
--- a/packages/cwd-vote-hooks/src/lib.rs
+++ b/packages/cwd-vote-hooks/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 use cosmwasm_schema::cw_serde;
 use cosmwasm_std::{to_binary, StdResult, Storage, SubMsg, WasmMsg};
 use cwd_hooks::Hooks;
diff --git a/packages/cwd-voting/src/lib.rs b/packages/cwd-voting/src/lib.rs
index 8c90f894..208cfc46 100644
--- a/packages/cwd-voting/src/lib.rs
+++ b/packages/cwd-voting/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod deposit;
 pub mod error;
 pub mod multiple_choice;
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/packages/cwd-testing/src/lib.rs b/packages/cwd-testing/src/lib.rs
index f58bdf08..56891f63 100644
--- a/packages/cwd-testing/src/lib.rs
+++ b/packages/cwd-testing/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 #[cfg(not(target_arch = "wasm32"))]
 pub mod tests;
 
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,
         },