Repository: DA0-DA0/dao-contracts
Pull request #227 (2022-03-28 19:47:33)

Add cw-governance functionality for managing cw20 and cw721 assets.
Adds:
    
1. Management of cw20 and cw721 token lists.
2. Automatic addition of received cw20 and cw721 assets to list.
3. Toggling on / off of automatic addition.
4. Associated queries for listing assets and listing asset balances.

After some thinking I've opted to implement this functionality inside
of the core contract instead of inside separate treasury
modules. Implementing treasury modules is nice because it makes us
flexible in the event that a new token / NFT / unknown asset standard
comes out and we would like to support it. It adds a fair amount of
complexity though:

1. Someone wanting to send tokens to a DAO must send those tokens to
   the appropriate treasury contract.
2. Queries on the frontend for asset lists require two additional
   queries (what are treasury contracts, what is address of treasury
   contract I care about).

We can always add new treasury contracts later down the line.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index d008de60..685b94c4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -192,6 +192,8 @@ dependencies = [
  "cw20 0.12.1",
  "cw20-balance-voting",
  "cw20-base 0.12.1",
+ "cw721",
+ "cw721-base",
  "schemars",
  "serde",
  "thiserror",
@@ -601,6 +603,34 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw721"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc5bb00baf493fc48bf05831edc3ed074731da7c1944c8521ead3df4ff08d9c7"
+dependencies = [
+ "cosmwasm-std",
+ "cw-utils 0.12.1",
+ "schemars",
+ "serde",
+]
+
+[[package]]
+name = "cw721-base"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da773ac93cae5a01a052772d65a2f377dd22933f583ef8f739d67f32ffce1b6b"
+dependencies = [
+ "cosmwasm-std",
+ "cw-storage-plus 0.12.1",
+ "cw-utils 0.12.1",
+ "cw2 0.12.1",
+ "cw721",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "der"
 version = "0.4.5"
diff --git a/contracts/cw-governance/Cargo.toml b/contracts/cw-governance/Cargo.toml
index 9e37343b..f177ced4 100644
--- a/contracts/cw-governance/Cargo.toml
+++ b/contracts/cw-governance/Cargo.toml
@@ -34,6 +34,8 @@ cosmwasm-storage = { version = "1.0.0-beta5" }
 cw-storage-plus = "0.12"
 cw2 = "0.12"
 cw-utils = "0.12"
+cw20 = "0.12"
+cw721 = "0.12"
 schemars = "0.8"
 serde = { version = "1.0", default-features = false, features = ["derive"] }
 thiserror = { version = "1.0" }
@@ -44,6 +46,6 @@ cw-governance-macros = { version = "0.1.0", path = "../../packages/cw-governance
 cosmwasm-schema = { version = "1.0.0-beta5" }
 cw-multi-test = "0.12"
 cw20-base = "0.12"
+cw721-base = "0.12"
 cw-govmod-sudo = { version = "0.1.0", path = "../cw-govmod-sudo"}
 cw20-balance-voting = { version = "0.1.0", path = "../cw20-balance-voting"}
-cw20 = "0.12"
diff --git a/contracts/cw-governance/src/contract.rs b/contracts/cw-governance/src/contract.rs
index 544e0bac..babf584b 100644
--- a/contracts/cw-governance/src/contract.rs
+++ b/contracts/cw-governance/src/contract.rs
@@ -7,16 +7,16 @@ use cosmwasm_std::{
     StdResult, SubMsg,
 };
 use cw2::{get_contract_version, set_contract_version};
-use cw_storage_plus::Bound;
+use cw_storage_plus::{Bound, Map};
 use cw_utils::parse_reply_instantiate_data;
 
 use cw_governance_interface::voting;
 
 use crate::error::ContractError;
 use crate::msg::{ExecuteMsg, InitialItemInfo, InstantiateMsg, ModuleInstantiateInfo, QueryMsg};
-use crate::query::{DumpStateResponse, GetItemResponse};
+use crate::query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse};
 use crate::state::{
-    Config, CONFIG, GOVERNANCE_MODULES, GOVERNANCE_MODULE_COUNT, ITEMS,
+    Config, CONFIG, CW20_LIST, CW721_LIST, GOVERNANCE_MODULES, GOVERNANCE_MODULE_COUNT, ITEMS,
     PENDING_ITEM_INSTANTIATION_NAMES, VOTING_MODULE,
 };
 
@@ -50,6 +50,8 @@ pub fn instantiate(
         name: msg.name,
         description: msg.description,
         image_url: msg.image_url,
+        automatically_add_cw20s: msg.automatically_add_cw20s,
+        automatically_add_cw721s: msg.automatically_add_cw721s,
     };
     CONFIG.save(deps.storage, &config)?;
 
@@ -127,39 +129,54 @@ pub fn execute(
     msg: ExecuteMsg,
 ) -> Result<Response, ContractError> {
     match msg {
-        ExecuteMsg::ExecuteProposalHook { .. } => {
-            if !GOVERNANCE_MODULES.has(deps.storage, info.sender.clone()) {
-                return Err(ContractError::Unauthorized {});
-            }
+        ExecuteMsg::ExecuteProposalHook { msgs } => {
+            execute_proposal_hook(deps.as_ref(), info.sender, msgs)
         }
-        _ => {
-            if info.sender != env.contract.address {
-                return Err(ContractError::Unauthorized {});
-            }
+        ExecuteMsg::UpdateConfig { config } => {
+            execute_update_config(deps, env, info.sender, config)
+        }
+        ExecuteMsg::UpdateVotingModule { module } => {
+            execute_update_voting_module(env, info.sender, module)
         }
-    }
-
-    let response = match msg {
-        ExecuteMsg::ExecuteProposalHook { msgs } => execute_proposal_hook(msgs),
-        ExecuteMsg::UpdateConfig { config } => execute_update_config(deps, config),
-        ExecuteMsg::UpdateVotingModule { module } => execute_update_voting_module(env, module),
         ExecuteMsg::UpdateGovernanceModules { to_add, to_remove } => {
-            execute_update_governance_modules(deps, env, to_add, to_remove)
+            execute_update_governance_modules(deps, env, info.sender, to_add, to_remove)
         }
-        ExecuteMsg::SetItem { key, addr } => execute_set_item(deps, key, addr),
-        ExecuteMsg::RemoveItem { key } => execute_remove_item(deps, key),
-    }?;
-
-    Ok(response.add_attribute("sender", info.sender))
+        ExecuteMsg::SetItem { key, addr } => execute_set_item(deps, env, info.sender, key, addr),
+        ExecuteMsg::RemoveItem { key } => execute_remove_item(deps, env, info.sender, key),
+        ExecuteMsg::Receive(_) => execute_receive_cw20(deps, info.sender),
+        ExecuteMsg::ReceiveNft(_) => execute_receive_cw721(deps, info.sender),
+        ExecuteMsg::UpdateCw20List { to_add, to_remove } => {
+            execute_update_cw20_list(deps, env, info.sender, to_add, to_remove)
+        }
+        ExecuteMsg::UpdateCw721List { to_add, to_remove } => {
+            execute_update_cw721_list(deps, env, info.sender, to_add, to_remove)
+        }
+    }
 }
 
-pub fn execute_proposal_hook(msgs: Vec<CosmosMsg<Empty>>) -> Result<Response, ContractError> {
+pub fn execute_proposal_hook(
+    deps: Deps,
+    sender: Addr,
+    msgs: Vec<CosmosMsg<Empty>>,
+) -> Result<Response, ContractError> {
+    if !GOVERNANCE_MODULES.has(deps.storage, sender) {
+        return Err(ContractError::Unauthorized {});
+    }
     Ok(Response::default()
         .add_attribute("action", "execute_proposal_hook")
         .add_messages(msgs))
 }
 
-pub fn execute_update_config(deps: DepsMut, config: Config) -> Result<Response, ContractError> {
+pub fn execute_update_config(
+    deps: DepsMut,
+    env: Env,
+    sender: Addr,
+    config: Config,
+) -> Result<Response, ContractError> {
+    if sender != env.contract.address {
+        return Err(ContractError::Unauthorized {});
+    }
+
     CONFIG.save(deps.storage, &config)?;
     // We incur some gas costs by having the config's fields in the
     // response. This has the benefit that it makes it reasonably
@@ -178,8 +195,13 @@ pub fn execute_update_config(deps: DepsMut, config: Config) -> Result<Response,
 
 pub fn execute_update_voting_module(
     env: Env,
+    sender: Addr,
     module: ModuleInstantiateInfo,
 ) -> Result<Response, ContractError> {
+    if env.contract.address != sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
     let wasm = module.into_wasm_msg(env.contract.address);
     let submessage = SubMsg::reply_on_success(wasm, VOTE_MODULE_UPDATE_REPLY_ID);
 
@@ -191,9 +213,14 @@ pub fn execute_update_voting_module(
 pub fn execute_update_governance_modules(
     deps: DepsMut,
     env: Env,
+    sender: Addr,
     to_add: Vec<ModuleInstantiateInfo>,
     to_remove: Vec<String>,
 ) -> Result<Response, ContractError> {
+    if env.contract.address != sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
     let module_count = GOVERNANCE_MODULE_COUNT.load(deps.storage)?;
 
     // Some safe maths.
@@ -223,11 +250,71 @@ pub fn execute_update_governance_modules(
         .add_submessages(to_add))
 }
 
+fn do_update_addr_list(
+    deps: DepsMut,
+    map: Map<Addr, Empty>,
+    to_add: Vec<String>,
+    to_remove: Vec<String>,
+) -> Result<(), ContractError> {
+    let to_add = to_add
+        .into_iter()
+        .map(|a| deps.api.addr_validate(&a))
+        .collect::<Result<Vec<_>, _>>()?;
+
+    let to_remove = to_remove
+        .into_iter()
+        .map(|a| deps.api.addr_validate(&a))
+        .collect::<Result<Vec<_>, _>>()?;
+
+    for addr in to_add {
+        map.save(deps.storage, addr, &Empty {})?;
+    }
+    for addr in to_remove {
+        map.remove(deps.storage, addr);
+    }
+
+    Ok(())
+}
+
+pub fn execute_update_cw20_list(
+    deps: DepsMut,
+    env: Env,
+    sender: Addr,
+    to_add: Vec<String>,
+    to_remove: Vec<String>,
+) -> Result<Response, ContractError> {
+    if env.contract.address != sender {
+        return Err(ContractError::Unauthorized {});
+    }
+    do_update_addr_list(deps, CW20_LIST, to_add, to_remove)?;
+    Ok(Response::default().add_attribute("action", "update_cw20_list"))
+}
+
+pub fn execute_update_cw721_list(
+    deps: DepsMut,
+    env: Env,
+    sender: Addr,
+    to_add: Vec<String>,
+    to_remove: Vec<String>,
+) -> Result<Response, ContractError> {
+    if env.contract.address != sender {
+        return Err(ContractError::Unauthorized {});
+    }
+    do_update_addr_list(deps, CW721_LIST, to_add, to_remove)?;
+    Ok(Response::default().add_attribute("action", "update_cw721_list"))
+}
+
 pub fn execute_set_item(
     deps: DepsMut,
+    env: Env,
+    sender: Addr,
     key: String,
     addr: String,
 ) -> Result<Response, ContractError> {
+    if env.contract.address != sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
     let addr = deps.api.addr_validate(&addr)?;
     ITEMS.save(deps.storage, key.clone(), &addr)?;
     Ok(Response::default()
@@ -236,15 +323,48 @@ pub fn execute_set_item(
         .add_attribute("addr", addr))
 }
 
-pub fn execute_remove_item(deps: DepsMut, key: String) -> Result<Response, ContractError> {
+pub fn execute_remove_item(
+    deps: DepsMut,
+    env: Env,
+    sender: Addr,
+    key: String,
+) -> Result<Response, ContractError> {
+    if env.contract.address != sender {
+        return Err(ContractError::Unauthorized {});
+    }
+
     ITEMS.remove(deps.storage, key.clone());
     Ok(Response::default()
         .add_attribute("action", "execute_remove_item")
         .add_attribute("key", key))
 }
 
+pub fn execute_receive_cw20(deps: DepsMut, sender: Addr) -> Result<Response, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+    if !config.automatically_add_cw20s {
+        Ok(Response::new())
+    } else {
+        CW20_LIST.save(deps.storage, sender.clone(), &Empty {})?;
+        Ok(Response::new()
+            .add_attribute("action", "receive_cw20")
+            .add_attribute("token", sender))
+    }
+}
+
+pub fn execute_receive_cw721(deps: DepsMut, sender: Addr) -> Result<Response, ContractError> {
+    let config = CONFIG.load(deps.storage)?;
+    if !config.automatically_add_cw721s {
+        Ok(Response::new())
+    } else {
+        CW721_LIST.save(deps.storage, sender.clone(), &Empty {})?;
+        Ok(Response::new()
+            .add_attribute("action", "receive_cw721")
+            .add_attribute("token", sender))
+    }
+}
+
 #[cfg_attr(not(feature = "library"), entry_point)]
-pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
         QueryMsg::Config {} => query_config(deps),
         QueryMsg::VotingModule {} => query_voting_module(deps),
@@ -259,6 +379,11 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::GetItem { key } => query_get_item(deps, key),
         QueryMsg::ListItems { start_at, limit } => query_list_items(deps, start_at, limit),
         QueryMsg::Info {} => query_info(deps),
+        QueryMsg::Cw20TokenList { start_at, limit } => query_cw20_list(deps, start_at, limit),
+        QueryMsg::Cw721TokenList { start_at, limit } => query_cw721_list(deps, start_at, limit),
+        QueryMsg::Cw20Balances { start_at, limit } => {
+            query_cw20_balances(deps, env, start_at, limit)
+        }
     }
 }
 
@@ -347,6 +472,11 @@ pub fn query_get_item(deps: Deps, item: String) -> StdResult<Binary> {
     to_binary(&GetItemResponse { item })
 }
 
+pub fn query_info(deps: Deps) -> StdResult<Binary> {
+    let info = cw2::get_contract_version(deps.storage)?;
+    to_binary(&cw_governance_interface::voting::InfoResponse { info })
+}
+
 pub fn query_list_items(
     deps: Deps,
     start_at: Option<String>,
@@ -358,18 +488,96 @@ pub fn query_list_items(
         None,
         cosmwasm_std::Order::Descending,
     );
-    let items: Vec<String> = match limit {
+    let items = match limit {
         Some(limit) => items
             .take(limit as usize)
             .collect::<Result<Vec<String>, _>>()?,
         None => items.collect::<Result<Vec<String>, _>>()?,
     };
+
     to_binary(&items)
 }
 
-pub fn query_info(deps: Deps) -> StdResult<Binary> {
-    let info = cw2::get_contract_version(deps.storage)?;
-    to_binary(&cw_governance_interface::voting::InfoResponse { info })
+// Can't be generic over key type. Otherwise, we could use this in
+// `query_list_items` as well.
+// <https://github.com/CosmWasm/cw-plus/issues/691>
+fn list_addr_keys<V>(
+    deps: Deps,
+    map: Map<Addr, V>,
+    start_at: Option<Addr>,
+    limit: Option<u64>,
+) -> StdResult<Vec<Addr>>
+where
+    V: serde::de::DeserializeOwned + serde::Serialize,
+{
+    let items = map.keys(
+        deps.storage,
+        start_at.map(Bound::inclusive),
+        None,
+        cosmwasm_std::Order::Descending,
+    );
+    match limit {
+        Some(limit) => Ok(items
+            .take(limit as usize)
+            .collect::<Result<Vec<Addr>, _>>()?),
+        None => Ok(items.collect::<Result<Vec<Addr>, _>>()?),
+    }
+}
+
+pub fn query_cw20_list(
+    deps: Deps,
+    start_at: Option<String>,
+    limit: Option<u64>,
+) -> StdResult<Binary> {
+    to_binary(&list_addr_keys(
+        deps,
+        CW20_LIST,
+        start_at.map(|s| deps.api.addr_validate(&s)).transpose()?,
+        limit,
+    )?)
+}
+
+pub fn query_cw721_list(
+    deps: Deps,
+    start_at: Option<String>,
+    limit: Option<u64>,
+) -> StdResult<Binary> {
+    to_binary(&list_addr_keys(
+        deps,
+        CW721_LIST,
+        start_at.map(|s| deps.api.addr_validate(&s)).transpose()?,
+        limit,
+    )?)
+}
+
+pub fn query_cw20_balances(
+    deps: Deps,
+    env: Env,
+    start_at: Option<String>,
+    limit: Option<u64>,
+) -> StdResult<Binary> {
+    let addrs = list_addr_keys(
+        deps,
+        CW20_LIST,
+        start_at.map(|a| deps.api.addr_validate(&a)).transpose()?,
+        limit,
+    )?;
+    let balances = addrs
+        .into_iter()
+        .map(|addr| {
+            let balance: cw20::BalanceResponse = deps.querier.query_wasm_smart(
+                addr.clone(),
+                &cw20::Cw20QueryMsg::Balance {
+                    address: env.contract.address.to_string(),
+                },
+            )?;
+            Ok(Cw20BalanceResponse {
+                addr,
+                balance: balance.balance,
+            })
+        })
+        .collect::<StdResult<Vec<_>>>()?;
+    to_binary(&balances)
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
diff --git a/contracts/cw-governance/src/msg.rs b/contracts/cw-governance/src/msg.rs
index 7a607626..fb0960c2 100644
--- a/contracts/cw-governance/src/msg.rs
+++ b/contracts/cw-governance/src/msg.rs
@@ -57,6 +57,13 @@ pub struct InstantiateMsg {
     /// An image URL to describe the governance module contract.
     pub image_url: Option<String>,
 
+    /// If true the contract will automatically add received cw20
+    /// tokens to its treasury.
+    pub automatically_add_cw20s: bool,
+    /// If true the contract will automatically add received cw721
+    /// tokens to its treasury.
+    pub automatically_add_cw721s: bool,
+
     /// Instantiate information for the governance contract's voting
     /// power module.
     pub voting_module_instantiate_info: ModuleInstantiateInfo,
@@ -97,6 +104,24 @@ pub enum ExecuteMsg {
     SetItem { key: String, addr: String },
     /// Removes an item from the governance contract's item map.
     RemoveItem { key: String },
+    /// Executed when the contract receives a cw20 token. Depending on
+    /// the contract's configuration the contract will automatically
+    /// add the token to its treasury.
+    Receive(cw20::Cw20ReceiveMsg),
+    /// Executed when the contract receives a cw721 token. Depending
+    /// on the contract's configuration the contract will
+    /// automatically add the token to its treasury.
+    ReceiveNft(cw721::Cw721ReceiveMsg),
+    /// Updates the list of cw20 tokens this contract has registered.
+    UpdateCw20List {
+        to_add: Vec<String>,
+        to_remove: Vec<String>,
+    },
+    /// Updates the list of cw721 tokens this contract has registered.
+    UpdateCw721List {
+        to_add: Vec<String>,
+        to_remove: Vec<String>,
+    },
 }
 
 #[voting_query]
@@ -118,11 +143,31 @@ pub enum QueryMsg {
     /// limited by network times than compute times. Returns
     /// `DumpStateResponse`.
     DumpState {},
-    GetItem {
-        key: String,
-    },
+    /// Gets the address associated with an item key.
+    GetItem { key: String },
+    /// Lists all of the item keys associted with the contract. For
+    /// example, given the items `{ "group": "...", "subdao": "..."}`
+    /// this query would return `["group", "subdao"]`.
     ListItems {
         start_at: Option<String>,
         limit: Option<u64>,
     },
+    /// Lists the addresses of the cw20 tokens in this contract's
+    /// treasury.
+    Cw20TokenList {
+        start_at: Option<String>,
+        limit: Option<u64>,
+    },
+    /// Lists the addresses of the cw721 tokens in this contract's
+    /// treasury.
+    Cw721TokenList {
+        start_at: Option<String>,
+        limit: Option<u64>,
+    },
+    /// Gets the token balance for each cw20 registered with the
+    /// contract.
+    Cw20Balances {
+        start_at: Option<String>,
+        limit: Option<u64>,
+    },
 }
diff --git a/contracts/cw-governance/src/query.rs b/contracts/cw-governance/src/query.rs
index b3cbf7ac..45b2d7b4 100644
--- a/contracts/cw-governance/src/query.rs
+++ b/contracts/cw-governance/src/query.rs
@@ -1,4 +1,4 @@
-use cosmwasm_std::Addr;
+use cosmwasm_std::{Addr, Uint128};
 use cw2::ContractVersion;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -27,3 +27,12 @@ pub struct GetItemResponse {
     /// otherwise.
     pub item: Option<Addr>,
 }
+
+/// Returned by Cw20Balances query.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub struct Cw20BalanceResponse {
+    /// The address of the token.
+    pub addr: Addr,
+    /// The contract's balance.
+    pub balance: Uint128,
+}
diff --git a/contracts/cw-governance/src/state.rs b/contracts/cw-governance/src/state.rs
index 4c3e5fbd..43c355f1 100644
--- a/contracts/cw-governance/src/state.rs
+++ b/contracts/cw-governance/src/state.rs
@@ -6,9 +6,19 @@ use cw_storage_plus::{Item, Map};
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 pub struct Config {
+    /// The name of the contract.
     pub name: String,
+    /// A description of the contract.
     pub description: String,
+    /// An optional image URL for displaying alongside the contract.
     pub image_url: Option<String>,
+
+    /// If true the contract will automatically add received cw20
+    /// tokens to its treasury.
+    pub automatically_add_cw20s: bool,
+    /// If true the contract will automatically add received cw721
+    /// tokens to its treasury.
+    pub automatically_add_cw721s: bool,
 }
 
 pub const CONFIG: Item<Config> = Item::new("config");
@@ -18,6 +28,13 @@ pub const ITEMS: Map<String, Addr> = Map::new("items");
 pub const PENDING_ITEM_INSTANTIATION_NAMES: Map<u64, String> =
     Map::new("pending_item_instantiations");
 
+/// Set of cw20 tokens that have been registered with this contract's
+/// treasury.
+pub const CW20_LIST: Map<Addr, Empty> = Map::new("cw20s");
+/// Set of cw721 tokens that have been registered with this contract's
+/// treasury.
+pub const CW721_LIST: Map<Addr, Empty> = Map::new("cw721s");
+
 /// Stores the number of governance modules present in the governance
 /// contract. This information is avaliable from the governance
 /// modules map but finding it requires a full traversal of the


Test patch:
diff --git a/contracts/cw-governance/src/tests.rs b/contracts/cw-governance/src/tests.rs
index 974f18a2..a272f013 100644
--- a/contracts/cw-governance/src/tests.rs
+++ b/contracts/cw-governance/src/tests.rs
@@ -8,7 +8,7 @@ use crate::{
         Admin, ExecuteMsg, InitialItem, InitialItemInfo, InstantiateMsg, ModuleInstantiateInfo,
         QueryMsg,
     },
-    query::{DumpStateResponse, GetItemResponse},
+    query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse},
     state::Config,
     ContractError,
 };
@@ -24,6 +24,15 @@ fn cw20_contract() -> Box<dyn Contract<Empty>> {
     Box::new(contract)
 }
 
+fn cw721_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw721_base::entry::execute,
+        cw721_base::entry::instantiate,
+        cw721_base::entry::query,
+    );
+    Box::new(contract)
+}
+
 fn sudo_govmod_contract() -> Box<dyn Contract<Empty>> {
     let contract = ContractWrapper::new(
         cw_govmod_sudo::contract::execute,
@@ -82,6 +91,8 @@ fn test_instantiate_with_n_gov_modules(n: usize) {
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs.".to_string(),
         image_url: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
         voting_module_instantiate_info: ModuleInstantiateInfo {
             code_id: cw20_id,
             msg: to_binary(&cw20_instantiate).unwrap(),
@@ -111,6 +122,8 @@ fn test_instantiate_with_n_gov_modules(n: usize) {
             name: "DAO DAO".to_string(),
             description: "A DAO that builds DAOs.".to_string(),
             image_url: None,
+            automatically_add_cw20s: true,
+            automatically_add_cw721s: true,
         }
     );
 
@@ -183,6 +196,8 @@ makes wickedness."
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs.".to_string(),
         image_url: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
         voting_module_instantiate_info: ModuleInstantiateInfo {
             code_id: cw20_id,
             msg: to_binary(&cw20_instantiate).unwrap(),
@@ -209,6 +224,8 @@ fn test_update_config() {
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs.".to_string(),
         image_url: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
         voting_module_instantiate_info: ModuleInstantiateInfo {
             code_id: govmod_id,
             msg: to_binary(&govmod_instantiate).unwrap(),
@@ -252,6 +269,8 @@ fn test_update_config() {
         name: "Root DAO".to_string(),
         description: "We love trees and sudo.".to_string(),
         image_url: Some("https://moonphase.is/image.svg".to_string()),
+        automatically_add_cw20s: false,
+        automatically_add_cw721s: true,
     };
 
     app.execute_contract(
@@ -293,6 +312,8 @@ fn test_swap_governance(swaps: Vec<(u64, u64)>) {
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs.".to_string(),
         image_url: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
         voting_module_instantiate_info: ModuleInstantiateInfo {
             code_id: govmod_id,
             msg: to_binary(&govmod_instantiate).unwrap(),
@@ -421,6 +442,8 @@ fn test_swap_voting_module() {
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs.".to_string(),
         image_url: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
         voting_module_instantiate_info: ModuleInstantiateInfo {
             code_id: govmod_id,
             msg: to_binary(&govmod_instantiate).unwrap(),
@@ -533,6 +556,8 @@ fn test_permissions() {
             label: "governance module".to_string(),
         }],
         initial_items: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
     };
 
     let gov_addr = app
@@ -576,13 +601,14 @@ fn test_permissions() {
                 name: "Evil config.".to_string(),
                 description: "👿".to_string(),
                 image_url: None,
+                automatically_add_cw20s: true,
+                automatically_add_cw721s: true,
             },
         },
     );
 }
 
-#[test]
-fn test_passthrough_voting_queries() {
+fn do_standard_instantiate(auto_add: bool) -> (Addr, App) {
     let mut app = App::default();
     let govmod_id = app.store_code(sudo_govmod_contract());
     let voting_id = app.store_code(cw20_balances_voting());
@@ -611,6 +637,8 @@ fn test_passthrough_voting_queries() {
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs.".to_string(),
         image_url: None,
+        automatically_add_cw20s: auto_add,
+        automatically_add_cw721s: auto_add,
         voting_module_instantiate_info: ModuleInstantiateInfo {
             code_id: voting_id,
             msg: to_binary(&voting_instantiate).unwrap(),
@@ -637,6 +665,13 @@ fn test_passthrough_voting_queries() {
         )
         .unwrap();
 
+    (gov_addr, app)
+}
+
+#[test]
+fn test_passthrough_voting_queries() {
+    let (gov_addr, app) = do_standard_instantiate(true);
+
     let creator_voting_power: VotingPowerAtHeightResponse = app
         .wrap()
         .query_wasm_smart(
@@ -696,59 +731,8 @@ fn list_items(
 
 #[test]
 fn test_add_remove_get() {
-    let mut app = App::default();
-    let govmod_id = app.store_code(sudo_govmod_contract());
-    let voting_id = app.store_code(cw20_balances_voting());
-    let gov_id = app.store_code(cw_gov_contract());
-    let cw20_id = app.store_code(cw20_contract());
-
-    let govmod_instantiate = cw_govmod_sudo::msg::InstantiateMsg {
-        root: CREATOR_ADDR.to_string(),
-    };
-    let voting_instantiate = cw20_balance_voting::msg::InstantiateMsg {
-        token_info: cw20_balance_voting::msg::TokenInfo::New {
-            code_id: cw20_id,
-            label: "DAO DAO voting".to_string(),
-            name: "DAO DAO".to_string(),
-            symbol: "DAO".to_string(),
-            decimals: 6,
-            initial_balances: vec![cw20::Cw20Coin {
-                address: CREATOR_ADDR.to_string(),
-                amount: Uint128::from(2u64),
-            }],
-            marketing: None,
-        },
-    };
-
-    let gov_instantiate = InstantiateMsg {
-        name: "DAO DAO".to_string(),
-        description: "A DAO that builds DAOs.".to_string(),
-        image_url: None,
-        voting_module_instantiate_info: ModuleInstantiateInfo {
-            code_id: voting_id,
-            msg: to_binary(&voting_instantiate).unwrap(),
-            admin: Admin::GovernanceContract {},
-            label: "voting module".to_string(),
-        },
-        governance_modules_instantiate_info: vec![ModuleInstantiateInfo {
-            code_id: govmod_id,
-            msg: to_binary(&govmod_instantiate).unwrap(),
-            admin: Admin::GovernanceContract {},
-            label: "governance module".to_string(),
-        }],
-        initial_items: None,
-    };
+    let (gov_addr, mut app) = do_standard_instantiate(true);
 
-    let gov_addr = app
-        .instantiate_contract(
-            gov_id,
-            Addr::unchecked(CREATOR_ADDR),
-            &gov_instantiate,
-            &[],
-            "cw-governance",
-            None,
-        )
-        .unwrap();
     let a = get_item(&mut app, gov_addr.clone(), "aaaaa".to_string());
     assert_eq!(a, GetItemResponse { item: None });
 
@@ -803,6 +787,8 @@ fn test_list_items() {
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs.".to_string(),
         image_url: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
         voting_module_instantiate_info: ModuleInstantiateInfo {
             code_id: voting_id,
             msg: to_binary(&voting_instantiate).unwrap(),
@@ -886,6 +872,8 @@ fn test_instantiate_with_items() {
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs.".to_string(),
         image_url: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
         voting_module_instantiate_info: ModuleInstantiateInfo {
             code_id: voting_id,
             msg: to_binary(&voting_instantiate).unwrap(),
@@ -962,3 +950,336 @@ fn test_instantiate_with_items() {
     // Tests instantiate contracts with addresses in the form "Contract #{index}"
     assert!(item1.to_string().starts_with("Contract #"));
 }
+
+#[test]
+fn test_cw20_receive_auto_add() {
+    let (gov_addr, mut app) = do_standard_instantiate(true);
+
+    let voting_module: Addr = app
+        .wrap()
+        .query_wasm_smart(gov_addr.clone(), &QueryMsg::VotingModule {})
+        .unwrap();
+    let gov_token: Addr = app
+        .wrap()
+        .query_wasm_smart(
+            voting_module,
+            &cw_governance_interface::voting::Query::TokenContract {},
+        )
+        .unwrap();
+
+    // Check that the balances query works with no tokens.
+    let cw20_balances: Vec<Cw20BalanceResponse> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr.clone(),
+            &QueryMsg::Cw20Balances {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw20_balances, vec![]);
+
+    // Send a gov token to the governance contract.
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        gov_token.clone(),
+        &cw20::Cw20ExecuteMsg::Send {
+            contract: gov_addr.to_string(),
+            amount: Uint128::new(1),
+            msg: to_binary(&"").unwrap(),
+        },
+        &[],
+    )
+    .unwrap();
+
+    let cw20_list: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr.clone(),
+            &QueryMsg::Cw20TokenList {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw20_list, vec![gov_token.clone()]);
+
+    let cw20_balances: Vec<Cw20BalanceResponse> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr.clone(),
+            &QueryMsg::Cw20Balances {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(
+        cw20_balances,
+        vec![Cw20BalanceResponse {
+            addr: gov_token.clone(),
+            balance: Uint128::new(1),
+        }]
+    );
+
+    // Test removing and adding some new ones.
+    app.execute_contract(
+        Addr::unchecked(gov_addr.clone()),
+        gov_addr.clone(),
+        &ExecuteMsg::UpdateCw20List {
+            to_add: vec!["new".to_string()],
+            to_remove: vec![gov_token.to_string()],
+        },
+        &[],
+    )
+    .unwrap();
+
+    let cw20_list: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr,
+            &QueryMsg::Cw20TokenList {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw20_list, vec![Addr::unchecked("new")]);
+}
+
+#[test]
+fn test_cw20_receive_no_auto_add() {
+    let (gov_addr, mut app) = do_standard_instantiate(false);
+
+    let voting_module: Addr = app
+        .wrap()
+        .query_wasm_smart(gov_addr.clone(), &QueryMsg::VotingModule {})
+        .unwrap();
+    let gov_token: Addr = app
+        .wrap()
+        .query_wasm_smart(
+            voting_module,
+            &cw_governance_interface::voting::Query::TokenContract {},
+        )
+        .unwrap();
+
+    // Send a gov token to the governance contract. Should not be
+    // added becasue auto add is turned off.
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        gov_token.clone(),
+        &cw20::Cw20ExecuteMsg::Send {
+            contract: gov_addr.to_string(),
+            amount: Uint128::new(1),
+            msg: to_binary(&"").unwrap(),
+        },
+        &[],
+    )
+    .unwrap();
+
+    let cw20_list: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr.clone(),
+            &QueryMsg::Cw20TokenList {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw20_list, Vec::<Addr>::new());
+
+    app.execute_contract(
+        Addr::unchecked(gov_addr.clone()),
+        gov_addr.clone(),
+        &ExecuteMsg::UpdateCw20List {
+            to_add: vec!["new".to_string(), gov_token.to_string()],
+            to_remove: vec!["ok to remove non existent".to_string()],
+        },
+        &[],
+    )
+    .unwrap();
+
+    let cw20_list: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr,
+            &QueryMsg::Cw20TokenList {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw20_list, vec![Addr::unchecked("new"), gov_token]);
+}
+
+#[test]
+fn test_cw721_receive() {
+    let (gov_addr, mut app) = do_standard_instantiate(true);
+
+    let cw721_id = app.store_code(cw721_contract());
+
+    let cw721_addr = app
+        .instantiate_contract(
+            cw721_id,
+            Addr::unchecked(CREATOR_ADDR),
+            &cw721_base::msg::InstantiateMsg {
+                name: "ekez".to_string(),
+                symbol: "ekez".to_string(),
+                minter: CREATOR_ADDR.to_string(),
+            },
+            &[],
+            "cw721",
+            None,
+        )
+        .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        cw721_addr.clone(),
+        &cw721_base::msg::ExecuteMsg::Mint(cw721_base::msg::MintMsg::<Option<Empty>> {
+            token_id: "ekez".to_string(),
+            owner: CREATOR_ADDR.to_string(),
+            token_uri: None,
+            extension: None,
+        }),
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        cw721_addr.clone(),
+        &cw721_base::msg::ExecuteMsg::<Option<Empty>>::SendNft {
+            contract: gov_addr.to_string(),
+            token_id: "ekez".to_string(),
+            msg: to_binary("").unwrap(),
+        },
+        &[],
+    )
+    .unwrap();
+
+    let cw721_list: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr.clone(),
+            &QueryMsg::Cw721TokenList {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw721_list, vec![cw721_addr.clone()]);
+
+    // OK to add already added. Remove happens after add.
+    app.execute_contract(
+        Addr::unchecked(gov_addr.clone()),
+        gov_addr.clone(),
+        &ExecuteMsg::UpdateCw721List {
+            to_add: vec!["new".to_string(), cw721_addr.to_string()],
+            to_remove: vec![cw721_addr.to_string()],
+        },
+        &[],
+    )
+    .unwrap();
+
+    let cw20_list: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr,
+            &QueryMsg::Cw721TokenList {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw20_list, vec![Addr::unchecked("new")]);
+}
+
+#[test]
+fn test_cw721_receive_no_auto_add() {
+    let (gov_addr, mut app) = do_standard_instantiate(false);
+
+    let cw721_id = app.store_code(cw721_contract());
+
+    let cw721_addr = app
+        .instantiate_contract(
+            cw721_id,
+            Addr::unchecked(CREATOR_ADDR),
+            &cw721_base::msg::InstantiateMsg {
+                name: "ekez".to_string(),
+                symbol: "ekez".to_string(),
+                minter: CREATOR_ADDR.to_string(),
+            },
+            &[],
+            "cw721",
+            None,
+        )
+        .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        cw721_addr.clone(),
+        &cw721_base::msg::ExecuteMsg::Mint(cw721_base::msg::MintMsg::<Option<Empty>> {
+            token_id: "ekez".to_string(),
+            owner: CREATOR_ADDR.to_string(),
+            token_uri: None,
+            extension: None,
+        }),
+        &[],
+    )
+    .unwrap();
+
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        cw721_addr.clone(),
+        &cw721_base::msg::ExecuteMsg::<Option<Empty>>::SendNft {
+            contract: gov_addr.to_string(),
+            token_id: "ekez".to_string(),
+            msg: to_binary("").unwrap(),
+        },
+        &[],
+    )
+    .unwrap();
+
+    let cw721_list: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr.clone(),
+            &QueryMsg::Cw721TokenList {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw721_list, Vec::<Addr>::new());
+
+    // Duplicates OK. Just adds one.
+    app.execute_contract(
+        Addr::unchecked(gov_addr.clone()),
+        gov_addr.clone(),
+        &ExecuteMsg::UpdateCw721List {
+            to_add: vec![
+                "new".to_string(),
+                cw721_addr.to_string(),
+                cw721_addr.to_string(),
+            ],
+            to_remove: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    let cw20_list: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            gov_addr,
+            &QueryMsg::Cw721TokenList {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(cw20_list, vec![Addr::unchecked("new"), cw721_addr]);
+}
diff --git a/contracts/cw-govmod-single/src/tests.rs b/contracts/cw-govmod-single/src/tests.rs
index 46b37338..a901b675 100644
--- a/contracts/cw-govmod-single/src/tests.rs
+++ b/contracts/cw-govmod-single/src/tests.rs
@@ -91,6 +91,8 @@ fn instantiate_with_default_governance(
         name: "DAO DAO".to_string(),
         description: "A DAO that builds DAOs".to_string(),
         image_url: None,
+        automatically_add_cw20s: true,
+        automatically_add_cw721s: true,
         voting_module_instantiate_info: cw_governance::msg::ModuleInstantiateInfo {
             code_id: votemod_id,
             msg: to_binary(&cw20_balance_voting::msg::InstantiateMsg {