Repository: DA0-DA0/dao-contracts
Pull request #183 (2022-02-27 13:27:28)

Automatically add cw20s to balance list
Addresses #176 

Allows DAOs/multisigs to automatically list CW20s sent to them.
- Both DAOs and multisigs can have a config flag to disable/enable this feature e.g. big DAOs may not want to be spammed with CW20s.
- Automatically adding to the tracked balances state.

Feedback appreciated!


Patch:
diff --git a/contracts/cw3-dao/src/contract.rs b/contracts/cw3-dao/src/contract.rs
index 7f420ce4..07fe23c7 100644
--- a/contracts/cw3-dao/src/contract.rs
+++ b/contracts/cw3-dao/src/contract.rs
@@ -20,7 +20,8 @@ use cosmwasm_std::{
 };
 use cw2::set_contract_version;
 use cw20::{
-    BalanceResponse, Cw20Coin, Cw20CoinVerified, Cw20Contract, Cw20QueryMsg, MinterResponse,
+    BalanceResponse, Cw20Coin, Cw20CoinVerified, Cw20Contract, Cw20QueryMsg, Cw20ReceiveMsg,
+    MinterResponse,
 };
 use cw3::{Status, Vote};
 use cw_storage_plus::Bound;
@@ -60,6 +61,7 @@ pub fn instantiate(
         refund_failed_proposals: msg.refund_failed_proposals,
         image_url: msg.image_url,
         only_members_execute: msg.only_members_execute,
+        automatically_add_cw20s: msg.automatically_add_cw20s,
     };
     CONFIG.save(deps.storage, &cfg)?;
 
@@ -183,6 +185,7 @@ pub fn execute(
         ExecuteMsg::UpdateStakingContract {
             new_staking_contract,
         } => execute_update_staking_contract(deps, env, info, new_staking_contract),
+        ExecuteMsg::Receive(rec) => execute_receive(deps, env, info, rec),
     }
 }
 
@@ -499,6 +502,24 @@ pub fn execute_update_cw20_token_list(
     Ok(Response::new().add_attribute("action", "update_cw20_token_list"))
 }
 
+pub fn execute_receive(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    _wrapped: Cw20ReceiveMsg,
+) -> Result<Response<Empty>, ContractError> {
+    let cfg = CONFIG.load(deps.storage)?;
+    if !cfg.automatically_add_cw20s {
+        return Ok(Response::new());
+    }
+
+    TREASURY_TOKENS.save(deps.storage, &info.sender, &Empty {})?;
+
+    Ok(Response::new()
+        .add_attribute("action", "receive")
+        .add_attribute("token", info.sender.to_string()))
+}
+
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
diff --git a/contracts/cw3-dao/src/msg.rs b/contracts/cw3-dao/src/msg.rs
index 05533c27..e16b70d1 100644
--- a/contracts/cw3-dao/src/msg.rs
+++ b/contracts/cw3-dao/src/msg.rs
@@ -2,7 +2,7 @@ use crate::error::ContractError;
 use crate::query::ThresholdResponse;
 use crate::state::Config;
 use cosmwasm_std::{Addr, CosmosMsg, Decimal, Empty, Uint128};
-use cw20::Cw20Coin;
+use cw20::{Cw20Coin, Cw20ReceiveMsg};
 use cw20_base::msg::InstantiateMarketingInfo;
 use cw3::Vote;
 use cw_utils::{Duration, Expiration};
@@ -28,6 +28,7 @@ pub struct InstantiateMsg {
     /// Optional Image URL that is used by the contract
     pub image_url: Option<String>,
     pub only_members_execute: bool,
+    pub automatically_add_cw20s: bool,
 }
 
 #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
@@ -168,6 +169,9 @@ pub enum ExecuteMsg {
     /// Update Staking Contract (can only be called by DAO contract)
     /// WARNING: this changes the contract controlling voting
     UpdateStakingContract { new_staking_contract: Addr },
+    /// Wrapper called for automatically adding cw20s
+    /// to our tracked balances
+    Receive(Cw20ReceiveMsg),
 }
 
 #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
diff --git a/contracts/cw3-dao/src/state.rs b/contracts/cw3-dao/src/state.rs
index 6af78d06..9d4dc818 100644
--- a/contracts/cw3-dao/src/state.rs
+++ b/contracts/cw3-dao/src/state.rs
@@ -23,6 +23,7 @@ pub struct Config {
     pub refund_failed_proposals: Option<bool>,
     pub image_url: Option<String>,
     pub only_members_execute: bool,
+    pub automatically_add_cw20s: bool,
 }
 
 #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
diff --git a/contracts/cw3-multisig/src/contract.rs b/contracts/cw3-multisig/src/contract.rs
index 68688dee..c1042a50 100644
--- a/contracts/cw3-multisig/src/contract.rs
+++ b/contracts/cw3-multisig/src/contract.rs
@@ -9,7 +9,7 @@ use cosmwasm_std::{
 };
 
 use cw2::set_contract_version;
-use cw20::{BalanceResponse, Cw20CoinVerified, Cw20QueryMsg};
+use cw20::{BalanceResponse, Cw20CoinVerified, Cw20QueryMsg, Cw20ReceiveMsg};
 use cw3::{
     Status, Vote, VoteInfo, VoteListResponse, VoteResponse, VoterDetail, VoterListResponse,
     VoterResponse,
@@ -58,6 +58,7 @@ pub fn instantiate(
         max_voting_period: msg.max_voting_period,
         image_url: msg.image_url,
         only_members_execute: msg.only_members_execute,
+        automatically_add_cw20s: msg.automatically_add_cw20s,
     };
     CONFIG.save(deps.storage, &cfg)?;
 
@@ -135,6 +136,7 @@ pub fn execute(
         ExecuteMsg::UpdateCw20TokenList { to_add, to_remove } => {
             execute_update_cw20_token_list(deps, env, info, to_add, to_remove)
         }
+        ExecuteMsg::Receive(rec) => execute_receive(deps, env, info, rec),
     }
 }
 
@@ -390,6 +392,24 @@ pub fn execute_update_cw20_token_list(
     Ok(Response::new().add_attribute("action", "update_cw20_token_list"))
 }
 
+pub fn execute_receive(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    _wrapped: Cw20ReceiveMsg,
+) -> Result<Response<Empty>, ContractError> {
+    let cfg = CONFIG.load(deps.storage)?;
+    if !cfg.automatically_add_cw20s {
+        return Ok(Response::new());
+    }
+
+    TREASURY_TOKENS.save(deps.storage, &info.sender, &Empty {})?;
+
+    Ok(Response::new()
+        .add_attribute("action", "receive")
+        .add_attribute("token", info.sender.to_string()))
+}
+
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
diff --git a/contracts/cw3-multisig/src/msg.rs b/contracts/cw3-multisig/src/msg.rs
index c742dc11..b4cb12bb 100644
--- a/contracts/cw3-multisig/src/msg.rs
+++ b/contracts/cw3-multisig/src/msg.rs
@@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize};
 
 use crate::{error::ContractError, state::Config};
 use cosmwasm_std::{Addr, CosmosMsg, Decimal, Empty};
+use cw20::Cw20ReceiveMsg;
 use cw3::Vote;
 use cw4::{Member, MemberChangedHookMsg};
 use cw_utils::{Duration, Expiration, ThresholdResponse};
@@ -19,6 +20,7 @@ pub struct InstantiateMsg {
     pub max_voting_period: Duration,
     pub image_url: Option<String>,
     pub only_members_execute: bool,
+    pub automatically_add_cw20s: bool,
 }
 
 #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
@@ -160,6 +162,9 @@ pub enum ExecuteMsg {
         to_add: Vec<Addr>,
         to_remove: Vec<Addr>,
     },
+    /// Wrapper called for automatically adding cw20s
+    /// to our tracked balances
+    Receive(Cw20ReceiveMsg),
 }
 
 // We can also add this as a cw3 extension
diff --git a/contracts/cw3-multisig/src/state.rs b/contracts/cw3-multisig/src/state.rs
index db60e242..8d069ee3 100644
--- a/contracts/cw3-multisig/src/state.rs
+++ b/contracts/cw3-multisig/src/state.rs
@@ -28,6 +28,7 @@ pub struct Config {
     /// Optional Image URL that is used by the contract
     pub image_url: Option<String>,
     pub only_members_execute: bool,
+    pub automatically_add_cw20s: bool,
 }
 
 #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]


Test patch:
diff --git a/contracts/cw3-dao/src/tests.rs b/contracts/cw3-dao/src/tests.rs
index a5b8e9da..4e38a2f6 100644
--- a/contracts/cw3-dao/src/tests.rs
+++ b/contracts/cw3-dao/src/tests.rs
@@ -114,6 +114,7 @@ fn instantiate_dao(
         refund_failed_proposals,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     app.instantiate_contract(
         dao_code_id,
@@ -275,6 +276,7 @@ fn test_instantiate_works() {
         refund_failed_proposals: None,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     let err = app
         .instantiate_contract(
@@ -336,6 +338,7 @@ fn instantiate_new_gov_token() {
         refund_failed_proposals: None,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     let res = app.instantiate_contract(
         dao_code_id,
@@ -396,6 +399,7 @@ fn instantiate_new_gov_token() {
         refund_failed_proposals: None,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     let res = app.instantiate_contract(
         dao_code_id,
@@ -536,6 +540,7 @@ fn instantiate_new_gov_token() {
         refund_failed_proposals: None,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     let res = app.instantiate_contract(
         dao_code_id,
@@ -1318,6 +1323,7 @@ fn test_execute_works_for_anyone() {
         refund_failed_proposals: Some(false),
         image_url: None,
         only_members_execute: false,
+        automatically_add_cw20s: true,
     });
 
     let res = app.execute_contract(dao_addr.clone(), dao_addr.clone(), &update_config_msg, &[]);
@@ -1564,6 +1570,7 @@ fn test_close_works_with_refund() {
         refund_failed_proposals: Some(false),
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     });
 
     let res = app.execute_contract(dao_addr.clone(), dao_addr.clone(), &update_config_msg, &[]);
@@ -1930,6 +1937,7 @@ fn test_update_config() {
         refund_failed_proposals: None,
         image_url: Some("https://imghostingwebsite.com/fqfpw.jpg".to_string()),
         only_members_execute: true,
+        automatically_add_cw20s: true,
     });
     let res = app.execute_contract(
         Addr::unchecked(VOTER1),
@@ -1995,7 +2003,8 @@ fn test_update_config() {
                 proposal_deposit: new_proposal_deposit_amount,
                 refund_failed_proposals: None,
                 image_url: Some("https://imghostingwebsite.com/fqfpw.jpg".to_string()),
-                only_members_execute: true
+                only_members_execute: true,
+                automatically_add_cw20s: true,
             },
             gov_token: cw20_addr,
             staking_contract: staking_addr,
@@ -2088,7 +2097,8 @@ fn test_update_staking_contract() {
                 proposal_deposit: Uint128::zero(),
                 refund_failed_proposals: None,
                 image_url: None,
-                only_members_execute: true
+                only_members_execute: true,
+                automatically_add_cw20s: true,
             },
             gov_token: cw20_addr,
             staking_contract: Addr::unchecked("Better_Staking_Contract"),
@@ -2268,7 +2278,8 @@ fn test_config_query() {
                 proposal_deposit: Uint128::zero(),
                 refund_failed_proposals: None,
                 image_url: None,
-                only_members_execute: true
+                only_members_execute: true,
+                automatically_add_cw20s: true,
             },
             gov_token: cw20_addr,
             staking_contract: staking_addr,
@@ -2315,6 +2326,7 @@ fn test_proposal_deposit_works() {
         refund_failed_proposals: None,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     });
     let res = app.execute_contract(dao_addr.clone(), dao_addr.clone(), &update_config_msg, &[]);
     assert!(res.is_ok());
@@ -2390,6 +2402,148 @@ fn test_proposal_deposit_works() {
     assert_eq!(balance, initial_owner_cw20_balance);
 }
 
+#[test]
+fn test_receive() {
+    let mut app = mock_app();
+    let voting_period = Duration::Time(2000000);
+    let threshold = Threshold::AbsolutePercentage {
+        percentage: Decimal::percent(20),
+    };
+    let (dao_addr, cw20_addr, _) = setup_test_case(
+        &mut app,
+        threshold.clone(),
+        voting_period,
+        coins(10, NATIVE_TOKEN_DENOM),
+        None,
+        None,
+    );
+
+    // Make a new token to send to the DAO
+    let cw20_id = app.store_code(contract_cw20_gov());
+    let msg = cw20_base::msg::InstantiateMsg {
+        name: String::from("NewCoin"),
+        symbol: String::from("COIN"),
+        decimals: 6,
+        initial_balances: vec![
+            Cw20Coin {
+                address: OWNER.to_string(),
+                amount: Uint128::new(INITIAL_BALANCE * 5),
+            },
+            Cw20Coin {
+                address: VOTER1.to_string(),
+                amount: Uint128::new(INITIAL_BALANCE * 5),
+            },
+        ],
+        mint: None,
+        marketing: None,
+    };
+    let res = app.instantiate_contract(cw20_id, Addr::unchecked(OWNER), &msg, &[], "cw20", None);
+    assert!(res.is_ok());
+    let other_cw20_addr = res.unwrap();
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: dao_addr.to_string(),
+        amount: Uint128::new(200u128),
+        msg: Default::default(),
+    };
+    let res = app.execute_contract(Addr::unchecked(VOTER1), other_cw20_addr.clone(), &msg, &[]);
+    assert!(res.is_ok());
+
+    // Query balances, expecting to see the other cw20 already added
+    let cw20_token_balances_msg = QueryMsg::Cw20Balances {
+        start_after: None,
+        limit: None,
+    };
+    let all_balances: Cw20BalancesResponse = app
+        .wrap()
+        .query_wasm_smart(&dao_addr, &cw20_token_balances_msg)
+        .unwrap();
+    assert_eq!(
+        all_balances.cw20_balances,
+        vec![
+            Cw20CoinVerified {
+                address: cw20_addr.clone(),
+                amount: Uint128::zero(),
+            },
+            Cw20CoinVerified {
+                address: other_cw20_addr.clone(),
+                amount: Uint128::new(200u128)
+            }
+        ]
+    );
+
+    // Update to stop auto adding cw20s
+    let update_config_msg = ExecuteMsg::UpdateConfig(Config {
+        name: "dao-dao".to_string(),
+        description: "a great DAO!".to_string(),
+        threshold,
+        max_voting_period: voting_period,
+        proposal_deposit: Uint128::new(0),
+        refund_failed_proposals: None,
+        image_url: None,
+        only_members_execute: true,
+        automatically_add_cw20s: false,
+    });
+    let res = app.execute_contract(dao_addr.clone(), dao_addr.clone(), &update_config_msg, &[]);
+    assert!(res.is_ok());
+
+    // Make another new token with initial balance, this time it won't be added automatically when sent
+    let cw20_id = app.store_code(contract_cw20_gov());
+    let msg = cw20_base::msg::InstantiateMsg {
+        name: String::from("NotAdded"),
+        symbol: String::from("NOT"),
+        decimals: 6,
+        initial_balances: vec![
+            Cw20Coin {
+                address: OWNER.to_string(),
+                amount: Uint128::new(INITIAL_BALANCE * 5),
+            },
+            Cw20Coin {
+                address: VOTER1.to_string(),
+                amount: Uint128::new(INITIAL_BALANCE * 5),
+            },
+        ],
+        mint: None,
+        marketing: None,
+    };
+    let res = app.instantiate_contract(cw20_id, Addr::unchecked(OWNER), &msg, &[], "cw20", None);
+    assert!(res.is_ok());
+    let not_added_cw20_addr = res.unwrap();
+
+    // Send some cw20s that will not be automatically tracked
+    let msg = Cw20ExecuteMsg::Send {
+        contract: dao_addr.to_string(),
+        amount: Uint128::new(200u128),
+        msg: Default::default(),
+    };
+    let res = app.execute_contract(Addr::unchecked(VOTER1), not_added_cw20_addr, &msg, &[]);
+    assert!(res.is_ok());
+
+    // Query all balances, this time will still only have 2
+    // As we have disabled automatic listing of cw20s
+    let cw20_token_balances_msg = QueryMsg::Cw20Balances {
+        start_after: None,
+        limit: None,
+    };
+    let all_balances: Cw20BalancesResponse = app
+        .wrap()
+        .query_wasm_smart(&dao_addr, &cw20_token_balances_msg)
+        .unwrap();
+    assert_eq!(
+        all_balances.cw20_balances,
+        vec![
+            Cw20CoinVerified {
+                address: cw20_addr,
+                amount: Uint128::zero(),
+            },
+            Cw20CoinVerified {
+                address: other_cw20_addr,
+                amount: Uint128::new(200u128)
+            }
+        ]
+    );
+}
+
 #[test]
 fn treasury_queries() {
     let mut app = mock_app();
diff --git a/contracts/cw3-multisig/src/tests.rs b/contracts/cw3-multisig/src/tests.rs
index 7eeae2c0..2a1a9f01 100644
--- a/contracts/cw3-multisig/src/tests.rs
+++ b/contracts/cw3-multisig/src/tests.rs
@@ -10,7 +10,7 @@ use cosmwasm_std::{
     Uint128, WasmMsg,
 };
 use cw2::{query_contract_info, ContractVersion};
-use cw20::Cw20Coin;
+use cw20::{Cw20Coin, Cw20CoinVerified, Cw20ExecuteMsg};
 use cw3::{
     Status, Vote, VoteInfo, VoteListResponse, VoteResponse, VoterDetail, VoterListResponse,
     VoterResponse,
@@ -106,6 +106,7 @@ fn instantiate_multisig(
         max_voting_period,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     app.instantiate_contract(
         multisig_id,
@@ -245,6 +246,7 @@ fn test_instantiate_works() {
         max_voting_period,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     let err = app
         .instantiate_contract(
@@ -269,6 +271,7 @@ fn test_instantiate_works() {
         max_voting_period,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     let err = app
         .instantiate_contract(
@@ -293,6 +296,7 @@ fn test_instantiate_works() {
         max_voting_period,
         image_url: None,
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     let multisig_addr = app
         .instantiate_contract(
@@ -330,7 +334,8 @@ fn test_instantiate_works() {
                 threshold: Threshold::AbsoluteCount { weight: 1 },
                 max_voting_period,
                 image_url: None,
-                only_members_execute: true
+                only_members_execute: true,
+                automatically_add_cw20s: true,
             },
             group_address: Cw4Contract::new(Addr::unchecked(group_addr)),
         }
@@ -370,6 +375,7 @@ fn test_instantiate_works() {
         max_voting_period,
         image_url: Some("https://imgur.com/someElmo.png".to_string()),
         only_members_execute: true,
+        automatically_add_cw20s: true,
     };
     let res = app.instantiate_contract(
         multisig_id,
@@ -479,6 +485,7 @@ fn test_update_config() {
             max_voting_period: voting_period,
             image_url: Some("https://someUrl.com/image.png".to_string()),
             only_members_execute: true,
+            automatically_add_cw20s: true,
         },
         multisig_addr.to_string(),
     );
@@ -519,7 +526,8 @@ fn test_update_config() {
                 },
                 max_voting_period: voting_period,
                 image_url: Some("https://someUrl.com/image.png".to_string()),
-                only_members_execute: true
+                only_members_execute: true,
+                automatically_add_cw20s: true,
             },
             group_address: Cw4Contract::new(Addr::unchecked(group_addr)),
         }
@@ -534,6 +542,7 @@ fn test_update_config() {
             max_voting_period: voting_period,
             image_url: None,
             only_members_execute: true,
+            automatically_add_cw20s: true,
         },
         multisig_addr.to_string(),
     );
@@ -1349,6 +1358,7 @@ fn test_execute_works_for_anyone() {
             max_voting_period: voting_period,
             image_url: Some("https://someUrl.com/image.png".to_string()),
             only_members_execute: false,
+            automatically_add_cw20s: true,
         },
         multisig_addr.to_string(),
     );
@@ -2096,6 +2106,138 @@ fn quorum_enforced_even_if_absolute_threshold_met() {
     assert_eq!(prop_status(&app), Status::Passed);
 }
 
+#[test]
+fn test_receive() {
+    let init_funds = coins(10, "BTC");
+    let mut app = mock_app(&init_funds);
+
+    let voting_period = Duration::Time(2000000);
+    let threshold = Threshold::AbsoluteCount { weight: 1 };
+    let (multisig_addr, _) = setup_test_case(
+        &mut app,
+        threshold.clone(),
+        voting_period,
+        init_funds,
+        false,
+    );
+
+    // Make a new token to send to the multisig
+    let cw20_id = app.store_code(contract_cw20_gov());
+    let msg = cw20_base::msg::InstantiateMsg {
+        name: String::from("NewCoin"),
+        symbol: String::from("COIN"),
+        decimals: 6,
+        initial_balances: vec![
+            Cw20Coin {
+                address: OWNER.to_string(),
+                amount: Uint128::new(INITIAL_BALANCE * 5),
+            },
+            Cw20Coin {
+                address: VOTER1.to_string(),
+                amount: Uint128::new(INITIAL_BALANCE * 5),
+            },
+        ],
+        mint: None,
+        marketing: None,
+    };
+    let res = app.instantiate_contract(cw20_id, Addr::unchecked(OWNER), &msg, &[], "cw20", None);
+    assert!(res.is_ok());
+    let other_cw20_addr = res.unwrap();
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: multisig_addr.to_string(),
+        amount: Uint128::new(200u128),
+        msg: Default::default(),
+    };
+    let res = app.execute_contract(Addr::unchecked(VOTER1), other_cw20_addr.clone(), &msg, &[]);
+    assert!(res.is_ok());
+
+    // Query balances, expecting to see the other cw20 already added
+    let cw20_token_balances_msg = QueryMsg::Cw20Balances {
+        start_after: None,
+        limit: None,
+    };
+    let all_balances: Cw20BalancesResponse = app
+        .wrap()
+        .query_wasm_smart(&multisig_addr, &cw20_token_balances_msg)
+        .unwrap();
+    assert_eq!(
+        all_balances.cw20_balances,
+        vec![Cw20CoinVerified {
+            address: other_cw20_addr.clone(),
+            amount: Uint128::new(200u128)
+        }]
+    );
+
+    // Update to stop auto adding cw20s
+    let update_config_msg = ExecuteMsg::UpdateConfig(Config {
+        name: "some-multisig".to_string(),
+        description: "multisig".to_string(),
+        threshold,
+        max_voting_period: voting_period,
+        image_url: None,
+        only_members_execute: false,
+        automatically_add_cw20s: false,
+    });
+    let res = app.execute_contract(
+        multisig_addr.clone(),
+        multisig_addr.clone(),
+        &update_config_msg,
+        &[],
+    );
+    assert!(res.is_ok());
+
+    // Make another new token with initial balance, this time it won't be added automatically when sent
+    let cw20_id = app.store_code(contract_cw20_gov());
+    let msg = cw20_base::msg::InstantiateMsg {
+        name: String::from("NotAdded"),
+        symbol: String::from("NOT"),
+        decimals: 6,
+        initial_balances: vec![
+            Cw20Coin {
+                address: OWNER.to_string(),
+                amount: Uint128::new(INITIAL_BALANCE * 5),
+            },
+            Cw20Coin {
+                address: VOTER1.to_string(),
+                amount: Uint128::new(INITIAL_BALANCE * 5),
+            },
+        ],
+        mint: None,
+        marketing: None,
+    };
+    let res = app.instantiate_contract(cw20_id, Addr::unchecked(OWNER), &msg, &[], "cw20", None);
+    assert!(res.is_ok());
+    let not_added_cw20_addr = res.unwrap();
+
+    // Send some cw20s that will not be automatically tracked
+    let msg = Cw20ExecuteMsg::Send {
+        contract: multisig_addr.to_string(),
+        amount: Uint128::new(200u128),
+        msg: Default::default(),
+    };
+    let res = app.execute_contract(Addr::unchecked(VOTER1), not_added_cw20_addr, &msg, &[]);
+    assert!(res.is_ok());
+
+    // Query all balances, this time will still only have 1
+    // As we have disabled automatic listing of cw20s
+    let cw20_token_balances_msg = QueryMsg::Cw20Balances {
+        start_after: None,
+        limit: None,
+    };
+    let all_balances: Cw20BalancesResponse = app
+        .wrap()
+        .query_wasm_smart(&multisig_addr, &cw20_token_balances_msg)
+        .unwrap();
+    assert_eq!(
+        all_balances.cw20_balances,
+        vec![Cw20CoinVerified {
+            address: other_cw20_addr,
+            amount: Uint128::new(200u128)
+        }]
+    );
+}
+
 #[test]
 fn treasury_queries() {
     let init_funds = coins(10, "BTC");