Repository: CosmWasm/sylvia
Pull request #39 (2022-09-08 12:39:12)

Cw1 Whitelist contracts done properly


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index a952572..e2a0bd5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,6 +8,12 @@ version = "1.0.57"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
 
+[[package]]
+name = "assert_matches"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9"
+
 [[package]]
 name = "base16ct"
 version = "0.1.1"
@@ -234,10 +240,12 @@ name = "cw1-whitelist"
 version = "0.1.2"
 dependencies = [
  "anyhow",
+ "assert_matches",
  "cosmwasm-std",
  "cw-multi-test",
- "cw-storage-plus 0.13.2",
+ "cw-storage-plus 0.14.0",
  "cw1",
+ "cw2",
  "schemars",
  "serde",
  "sylvia",
diff --git a/contracts/cw1-whitelist/Cargo.toml b/contracts/cw1-whitelist/Cargo.toml
index 4b6ffa3..7bb5d7c 100644
--- a/contracts/cw1-whitelist/Cargo.toml
+++ b/contracts/cw1-whitelist/Cargo.toml
@@ -8,18 +8,25 @@ license = "Apache-2.0"
 repository = "https://github.com/CosmWasm/sylvia"
 homepage = "https://cosmwasm.com"
 
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+library = []
+tests = ["library", "cw-multi-test"]
+
 [dependencies]
 cosmwasm-std = { version = "1.0.0", features = ["staking"] }
 schemars = "0.8.1"
 serde = { version = "1", default-features = false, features = ["derive"] }
-sylvia = { version = "0.1.2", path = "../../sylvia" }
-cw1 = { version = "0.1.2", path = "../cw1" }
-cw-storage-plus = "0.13.2"
+sylvia = { path = "../../sylvia" }
+cw1 = { path = "../cw1" }
+cw-storage-plus = "0.14.0"
 thiserror = { version = "1.0.23" }
-
-[lib]
-crate-type = ["cdylib", "rlib"]
+cw2 = "0.14.0"
+cw-multi-test = { version = "0.14.0", optional = true }
 
 [dev-dependencies]
 anyhow = "1"
 cw-multi-test = "0.14.0"
+assert_matches = "1.5.0"
diff --git a/contracts/cw1-whitelist/src/contract.rs b/contracts/cw1-whitelist/src/contract.rs
index 5e6fe4e..e37108e 100644
--- a/contracts/cw1-whitelist/src/contract.rs
+++ b/contracts/cw1-whitelist/src/contract.rs
@@ -1,243 +1,451 @@
 use crate::error::ContractError;
-use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response};
+use crate::responses::AdminListResponse;
+use cosmwasm_std::{Addr, Deps, DepsMut, Empty, Env, MessageInfo, Order, Response, StdResult};
 
-use cw1::{Cw1, FindMemberResponse};
-use cw_storage_plus::Map;
-use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+use cw2::set_contract_version;
+use cw_storage_plus::{Item, Map};
 use sylvia::contract;
 
-#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
-pub struct MembersCntResponse {
-    pub cnt: usize,
-}
+const CONTRACT_NAME: &str = env!("CARGO_PKG_NAME");
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
 
-pub struct Cw1WhitelistContract {
-    members: Map<'static, Addr, Empty>,
+pub struct Cw1WhitelistContract<'a> {
+    pub(crate) admins: Map<'static, &'a Addr, Empty>,
+    pub(crate) mutable: Item<'static, bool>,
 }
 
-impl Cw1 for Cw1WhitelistContract {
-    type Error = ContractError;
-    fn add_member(
-        &self,
-        (deps, _env, _info): (
-            cosmwasm_std::DepsMut,
-            cosmwasm_std::Env,
-            cosmwasm_std::MessageInfo,
-        ),
-        member: String,
-    ) -> Result<Response, Self::Error> {
-        self.members
-            .save(deps.storage, deps.api.addr_validate(&member)?, &Empty {})?;
-
-        Ok(Response::new())
-    }
-
-    fn find_member(
-        &self,
-        (deps, _env): (cosmwasm_std::Deps, cosmwasm_std::Env),
-        member: String,
-    ) -> Result<FindMemberResponse, Self::Error> {
-        let is_present = self
-            .members
-            .has(deps.storage, deps.api.addr_validate(&member)?);
-
-        Ok(FindMemberResponse { is_present })
-    }
-}
-
-#[contract(error=ContractError)]
+#[contract]
 #[messages(cw1 as Cw1)]
-impl Cw1WhitelistContract {
+impl Cw1WhitelistContract<'_> {
     pub const fn new() -> Self {
         Self {
-            members: Map::new("members"),
+            admins: Map::new("admins"),
+            mutable: Item::new("mutable"),
         }
     }
 
     #[msg(instantiate)]
     pub fn instantiate(
         &self,
-        (deps, _env, _msg): (DepsMut, Env, MessageInfo),
-        members: Vec<String>,
+        ctx: (DepsMut, Env, MessageInfo),
+        admins: Vec<String>,
+        mutable: bool,
     ) -> Result<Response, ContractError> {
-        for addr in members.into_iter() {
-            self.members
-                .save(deps.storage, deps.api.addr_validate(&addr)?, &Empty {})?;
+        let (deps, _, _) = ctx;
+        set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
+
+        for admin in admins {
+            let admin = deps.api.addr_validate(&admin)?;
+            self.admins.save(deps.storage, &admin, &Empty {})?;
         }
 
+        self.mutable.save(deps.storage, &mutable)?;
+
         Ok(Response::new())
     }
 
     #[msg(exec)]
-    fn remove_member(
+    pub fn freeze(&self, ctx: (DepsMut, Env, MessageInfo)) -> Result<Response, ContractError> {
+        let (deps, _, info) = ctx;
+
+        if !self.is_admin(deps.as_ref(), &info.sender) {
+            return Err(ContractError::Unauthorized {});
+        }
+
+        self.mutable.save(deps.storage, &false)?;
+
+        let resp = Response::new().add_attribute("action", "freeze");
+        Ok(resp)
+    }
+
+    #[msg(exec)]
+    pub fn update_admins(
         &self,
-        (deps, _env, msg): (DepsMut, Env, MessageInfo),
-        member: String,
+        ctx: (DepsMut, Env, MessageInfo),
+        mut admins: Vec<String>,
     ) -> Result<Response, ContractError> {
-        self.members
-            .remove(deps.storage, deps.api.addr_validate(&member)?);
+        let (deps, _, info) = ctx;
+
+        if !self.is_admin(deps.as_ref(), &info.sender) {
+            return Err(ContractError::Unauthorized {});
+        }
+
+        if !self.mutable.load(deps.storage)? {
+            return Err(ContractError::ContractFrozen {});
+        }
+
+        admins.sort_unstable();
+        let mut low_idx = 0;
+
+        let to_remove: Vec<_> = self
+            .admins
+            .keys(deps.storage, None, None, Order::Ascending)
+            .filter(|addr| {
+                // This is a bit of optimization basing on the fact that both `admins` and queried
+                // keys range are sorted. Binary search would always return the index which is at
+                // most as big as searched item, so for next item there is no point in looking at
+                // lower indices. On the other hand - if we reached and of the sequence, we want to
+                // remove all following keys.
+                addr.as_ref()
+                    .map(|addr| {
+                        if low_idx >= admins.len() {
+                            return true;
+                        }
+
+                        match admins[low_idx..].binary_search(&addr.into()) {
+                            Ok(idx) => {
+                                low_idx = idx;
+                                false
+                            }
+                            Err(idx) => {
+                                low_idx = idx;
+                                true
+                            }
+                        }
+                    })
+                    .unwrap_or(true)
+            })
+            .collect::<Result<_, _>>()?;
+
+        for addr in to_remove {
+            self.admins.remove(deps.storage, &addr);
+        }
 
-        Ok(Response::new()
-            .add_attribute("action", "remove_member")
-            .add_attribute("sender", msg.sender))
+        for admin in admins {
+            let admin = deps.api.addr_validate(&admin)?;
+            self.admins.save(deps.storage, &admin, &Empty {})?;
+        }
+
+        let resp = Response::new().add_attribute("action", "update_admins");
+        Ok(resp)
     }
 
     #[msg(query)]
-    fn members_cnt(&self, (deps, _env): (Deps, Env)) -> Result<MembersCntResponse, ContractError> {
-        let cnt = self
-            .members
+    pub fn admin_list(&self, ctx: (Deps, Env)) -> StdResult<AdminListResponse> {
+        let (deps, _) = ctx;
+
+        let admins: Result<_, _> = self
+            .admins
             .keys(deps.storage, None, None, Order::Ascending)
-            .count();
-        Ok(MembersCntResponse { cnt })
+            .map(|addr| addr.map(String::from))
+            .collect();
+
+        Ok(AdminListResponse {
+            admins: admins?,
+            mutable: self.mutable.load(deps.storage)?,
+        })
+    }
+
+    pub fn is_admin(&self, deps: Deps, addr: &Addr) -> bool {
+        self.admins.has(deps.storage, addr)
     }
 }
 
 #[cfg(test)]
 mod tests {
-    use cosmwasm_std::{
-        from_binary, from_slice,
-        testing::{mock_dependencies, mock_env, mock_info},
-        to_binary,
-    };
-
-    use crate::contract::InstantiateMsg;
-
     use super::*;
+    use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
+    use cosmwasm_std::{coin, coins, to_binary, BankMsg, CosmosMsg, StakingMsg, SubMsg, WasmMsg};
+    use cw1::Cw1;
 
     #[test]
-    fn binary_serialize_instantiate() {
-        let original_msg = InstantiateMsg {
-            members: vec!["member1".to_owned(), "member2".to_owned()],
-        };
+    fn instantiate_and_modify_config() {
+        let mut deps = mock_dependencies();
 
-        let serialized_msg = to_binary(&original_msg).unwrap();
-        let serialized_msg: InstantiateMsg = from_binary(&serialized_msg).unwrap();
+        let alice = "alice";
+        let bob = "bob";
+        let carl = "carl";
 
-        assert_eq!(serialized_msg, original_msg);
-    }
+        let anyone = "anyone";
 
-    #[test]
-    fn slice_deserialize_instantiate() {
-        let deserialized: InstantiateMsg =
-            from_slice(br#"{"members": ["member", "some_member"]}"#).unwrap();
+        let contract = Cw1WhitelistContract::new();
+
+        // instantiate the contract
+        let info = mock_info(anyone, &[]);
+        contract
+            .instantiate(
+                (deps.as_mut(), mock_env(), info),
+                vec![alice.to_string(), bob.to_string(), carl.to_string()],
+                true,
+            )
+            .unwrap();
+
+        // ensure expected config
+        let expected = AdminListResponse {
+            admins: vec![alice.to_string(), bob.to_string(), carl.to_string()],
+            mutable: true,
+        };
         assert_eq!(
-            deserialized,
-            InstantiateMsg {
-                members: vec!["member".to_owned(), "some_member".to_owned()]
-            }
+            contract.admin_list((deps.as_ref(), mock_env())).unwrap(),
+            expected
         );
-    }
 
-    #[test]
-    fn binary_serialize_exec() {
-        let original_msg = ExecMsg::Cw1WhitelistContract(ImplExecMsg::RemoveMember {
-            member: "some_member".to_owned(),
-        });
+        // anyone cannot modify the contract
+        let info = mock_info(anyone, &[]);
+        let err = contract
+            .update_admins((deps.as_mut(), mock_env(), info), vec![anyone.to_string()])
+            .unwrap_err();
+        assert_eq!(err, ContractError::Unauthorized {});
 
-        let serialized_msg = to_binary(&original_msg).unwrap();
-        let serialized_msg: ExecMsg = from_binary(&serialized_msg).unwrap();
+        // but alice can kick out carl
+        let info = mock_info(alice, &[]);
+        contract
+            .update_admins(
+                (deps.as_mut(), mock_env(), info),
+                vec![alice.to_string(), bob.to_string()],
+            )
+            .unwrap();
 
-        assert_eq!(serialized_msg, original_msg);
-    }
+        // ensure expected config
+        let expected = AdminListResponse {
+            admins: vec![alice.to_string(), bob.to_string()],
+            mutable: true,
+        };
+        assert_eq!(
+            contract.admin_list((deps.as_ref(), mock_env())).unwrap(),
+            expected
+        );
 
-    #[test]
-    fn slice_deserialize_exec() {
-        let deserialized: ExecMsg =
-            from_slice(br#"{"remove_member": {"member": "some_member"}}"#).unwrap();
+        // carl cannot freeze it
+        let info = mock_info(carl, &[]);
+        let err = contract
+            .freeze((deps.as_mut(), mock_env(), info))
+            .unwrap_err();
+        assert_eq!(err, ContractError::Unauthorized {});
+
+        // but bob can
+        let info = mock_info(bob, &[]);
+        contract.freeze((deps.as_mut(), mock_env(), info)).unwrap();
+        let expected = AdminListResponse {
+            admins: vec![alice.to_string(), bob.to_string()],
+            mutable: false,
+        };
         assert_eq!(
-            deserialized,
-            ExecMsg::Cw1WhitelistContract(ImplExecMsg::RemoveMember {
-                member: "some_member".to_owned()
-            })
+            contract.admin_list((deps.as_ref(), mock_env())).unwrap(),
+            expected
         );
+
+        // and now alice cannot change it again
+        let info = mock_info(alice, &[]);
+        let err = contract
+            .update_admins((deps.as_mut(), mock_env(), info), vec![alice.to_string()])
+            .unwrap_err();
+        assert_eq!(err, ContractError::ContractFrozen {});
     }
 
     #[test]
-    fn binary_serialize_query() {
-        let original_msg = QueryMsg::Cw1WhitelistContract(ImplQueryMsg::MembersCnt {});
+    fn execute_messages_has_proper_permissions() {
+        let mut deps = mock_dependencies();
 
-        let serialized_msg = to_binary(&original_msg).unwrap();
-        let serialized_msg: QueryMsg = from_binary(&serialized_msg).unwrap();
+        let alice = "alice";
+        let bob = "bob";
+        let carl = "carl";
 
-        assert_eq!(serialized_msg, original_msg);
-    }
+        let contract = Cw1WhitelistContract::new();
 
-    #[test]
-    fn slice_deserialize_query() {
-        let deserialized: QueryMsg = from_slice(br#"{"members_cnt": {}}"#).unwrap();
+        // instantiate the contract
+        let info = mock_info(bob, &[]);
+        contract
+            .instantiate(
+                (deps.as_mut(), mock_env(), info),
+                vec![alice.to_string(), carl.to_string()],
+                false,
+            )
+            .unwrap();
+
+        let freeze = ImplExecMsg::Freeze {};
+        let msgs = vec![
+            BankMsg::Send {
+                to_address: bob.to_string(),
+                amount: coins(10000, "DAI"),
+            }
+            .into(),
+            WasmMsg::Execute {
+                contract_addr: "some contract".into(),
+                msg: to_binary(&freeze).unwrap(),
+                funds: vec![],
+            }
+            .into(),
+        ];
+
+        // bob cannot execute them
+        let info = mock_info(bob, &[]);
+        let err = contract
+            .execute((deps.as_mut(), mock_env(), info), msgs.clone())
+            .unwrap_err();
+        assert_eq!(err, ContractError::Unauthorized {});
+
+        // but carl can
+        let info = mock_info(carl, &[]);
+        let res = contract
+            .execute((deps.as_mut(), mock_env(), info), msgs.clone())
+            .unwrap();
         assert_eq!(
-            deserialized,
-            QueryMsg::Cw1WhitelistContract(ImplQueryMsg::MembersCnt {})
+            res.messages,
+            msgs.into_iter().map(SubMsg::new).collect::<Vec<_>>()
         );
+        assert_eq!(res.attributes, [("action", "execute")]);
     }
 
     #[test]
-    fn instantiate_empty() {
-        let contract = Cw1WhitelistContract::new();
+    fn can_execute_query_works() {
         let mut deps = mock_dependencies();
-        let info = mock_info("anyone", &[]);
 
-        contract
-            .instantiate((deps.as_mut(), mock_env(), info), vec![])
-            .unwrap();
-    }
+        let alice = "alice";
+        let bob = "bob";
+
+        let anyone = "anyone";
 
-    #[test]
-    fn instantiate() {
         let contract = Cw1WhitelistContract::new();
-        let mut deps = mock_dependencies();
-        let members = vec!["alice".to_owned(), "brian".to_owned(), "carol".to_owned()];
-        let info = mock_info("anyone", &[]);
 
+        // instantiate the contract
+        let info = mock_info(anyone, &[]);
         contract
-            .instantiate((deps.as_mut(), mock_env(), info), members)
+            .instantiate(
+                (deps.as_mut(), mock_env(), info),
+                vec![alice.to_string(), bob.to_string()],
+                false,
+            )
             .unwrap();
 
-        let resp = contract
-            .find_member((deps.as_ref(), mock_env()), "brian".to_owned())
+        // let us make some queries... different msg types by owner and by other
+        let send_msg = CosmosMsg::Bank(BankMsg::Send {
+            to_address: anyone.to_string(),
+            amount: coins(12345, "ushell"),
+        });
+        let staking_msg = CosmosMsg::Staking(StakingMsg::Delegate {
+            validator: anyone.to_string(),
+            amount: coin(70000, "ureef"),
+        });
+
+        // owner can send
+        let res = contract
+            .can_execute(
+                (deps.as_ref(), mock_env()),
+                alice.to_string(),
+                send_msg.clone(),
+            )
+            .unwrap();
+        assert!(res.can_execute);
+
+        // owner can stake
+        let res = contract
+            .can_execute(
+                (deps.as_ref(), mock_env()),
+                bob.to_string(),
+                staking_msg.clone(),
+            )
             .unwrap();
+        assert!(res.can_execute);
 
-        assert!(resp.is_present);
+        // anyone cannot send
+        let res = contract
+            .can_execute((deps.as_ref(), mock_env()), anyone.to_string(), send_msg)
+            .unwrap();
+        assert!(!res.can_execute);
 
-        let resp = contract
-            .find_member((deps.as_ref(), mock_env()), "alice".to_owned())
+        // anyone cannot stake
+        let res = contract
+            .can_execute((deps.as_ref(), mock_env()), anyone.to_string(), staking_msg)
             .unwrap();
+        assert!(!res.can_execute);
+    }
 
-        assert!(resp.is_present);
+    mod msgs {
+        use cosmwasm_std::{from_binary, from_slice, to_binary, BankMsg};
 
-        let resp = contract
-            .find_member((deps.as_ref(), mock_env()), "carol".to_owned())
-            .unwrap();
+        use crate::contract::{ExecMsg, ImplExecMsg, ImplQueryMsg, QueryMsg};
 
-        assert!(resp.is_present);
+        #[test]
+        fn freeze() {
+            let original = ImplExecMsg::Freeze {};
+            let serialized = to_binary(&original).unwrap();
+            let deserialized = from_binary(&serialized).unwrap();
 
-        let resp = contract
-            .find_member((deps.as_ref(), mock_env()), "someone".to_owned())
-            .unwrap();
+            assert_eq!(ExecMsg::Cw1WhitelistContract(original), deserialized);
 
-        assert!(!resp.is_present);
-    }
+            let json = br#"{
+                "freeze": {}
+            }"#;
+            let deserialized = from_slice(json).unwrap();
 
-    #[test]
-    fn add_member() {
-        let contract = Cw1WhitelistContract::new();
-        let mut deps = mock_dependencies();
-        let members = vec!["alice".to_owned(), "brian".to_owned(), "carol".to_owned()];
-        let info = mock_info("anyone", &[]);
+            assert_eq!(
+                ExecMsg::Cw1WhitelistContract(ImplExecMsg::Freeze {}),
+                deserialized
+            );
+        }
 
-        contract
-            .instantiate((deps.as_mut(), mock_env(), info.clone()), members)
-            .unwrap();
+        #[test]
+        fn update_admins() {
+            let original = ImplExecMsg::UpdateAdmins {
+                admins: vec!["admin1".to_owned(), "admin2".to_owned()],
+            };
+            let serialized = to_binary(&original).unwrap();
+            let deserialized = from_binary(&serialized).unwrap();
+
+            assert_eq!(ExecMsg::Cw1WhitelistContract(original), deserialized);
+
+            let json = br#"{
+                "update_admins": {
+                    "admins": ["admin1", "admin3"]
+                }
+            }"#;
+            let deserialized = from_slice(json).unwrap();
+
+            assert_eq!(
+                ExecMsg::Cw1WhitelistContract(ImplExecMsg::UpdateAdmins {
+                    admins: vec!["admin1".to_owned(), "admin3".to_owned()]
+                }),
+                deserialized
+            );
+        }
 
-        contract
-            .add_member((deps.as_mut(), mock_env(), info), "denis".to_owned())
-            .unwrap();
+        #[test]
+        fn admin_list() {
+            let original = ImplQueryMsg::AdminList {};
+            let serialized = to_binary(&original).unwrap();
+            let deserialized = from_binary(&serialized).unwrap();
 
-        let resp = contract
-            .find_member((deps.as_ref(), mock_env()), "denis".to_owned())
-            .unwrap();
+            assert_eq!(QueryMsg::Cw1WhitelistContract(original), deserialized);
 
-        assert!(resp.is_present);
+            let json = br#"{
+                "admin_list": {}
+            }"#;
+            let deserialized = from_slice(json).unwrap();
+
+            assert_eq!(
+                QueryMsg::Cw1WhitelistContract(ImplQueryMsg::AdminList {}),
+                deserialized
+            );
+        }
+
+        #[test]
+        fn execute() {
+            let original = cw1::ExecMsg::Execute {
+                msgs: vec![BankMsg::Send {
+                    to_address: "admin1".to_owned(),
+                    amount: vec![],
+                }
+                .into()],
+            };
+            let serialized = to_binary(&original).unwrap();
+            let deserialized = from_binary(&serialized).unwrap();
+            assert_eq!(ExecMsg::Cw1(original), deserialized);
+        }
+
+        #[test]
+        fn can_execute() {
+            let original = cw1::QueryMsg::CanExecute {
+                sender: "admin".to_owned(),
+                msg: BankMsg::Send {
+                    to_address: "admin1".to_owned(),
+                    amount: vec![],
+                }
+                .into(),
+            };
+            let serialized = to_binary(&original).unwrap();
+            let deserialized = from_binary(&serialized).unwrap();
+            assert_eq!(QueryMsg::Cw1(original), deserialized);
+        }
     }
 }
diff --git a/contracts/cw1-whitelist/src/cw1.rs b/contracts/cw1-whitelist/src/cw1.rs
new file mode 100644
index 0000000..2b25fee
--- /dev/null
+++ b/contracts/cw1-whitelist/src/cw1.rs
@@ -0,0 +1,41 @@
+use cosmwasm_std::{Addr, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response};
+use cw1::{CanExecuteResp, Cw1};
+
+use crate::contract::Cw1WhitelistContract;
+use crate::error::ContractError;
+
+impl Cw1 for Cw1WhitelistContract<'_> {
+    type Error = ContractError;
+
+    fn execute(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        msgs: Vec<CosmosMsg>,
+    ) -> Result<Response, ContractError> {
+        let (deps, _, info) = ctx;
+
+        if !self.is_admin(deps.as_ref(), &info.sender) {
+            return Err(ContractError::Unauthorized {});
+        }
+
+        let resp = Response::new()
+            .add_messages(msgs)
+            .add_attribute("action", "execute");
+        Ok(resp)
+    }
+
+    fn can_execute(
+        &self,
+        ctx: (Deps, Env),
+        sender: String,
+        _msg: CosmosMsg,
+    ) -> Result<cw1::CanExecuteResp, Self::Error> {
+        let (deps, _) = ctx;
+
+        let resp = CanExecuteResp {
+            can_execute: self.is_admin(deps, &Addr::unchecked(&sender)),
+        };
+
+        Ok(resp)
+    }
+}
diff --git a/contracts/cw1-whitelist/src/error.rs b/contracts/cw1-whitelist/src/error.rs
index 40dab9f..6fc9ba9 100644
--- a/contracts/cw1-whitelist/src/error.rs
+++ b/contracts/cw1-whitelist/src/error.rs
@@ -1,8 +1,14 @@
 use cosmwasm_std::StdError;
 use thiserror::Error;
 
-#[derive(Error, Debug)]
+#[derive(Error, Debug, PartialEq)]
 pub enum ContractError {
     #[error("{0}")]
     Std(#[from] StdError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("Contract is frozen")]
+    ContractFrozen {},
 }
diff --git a/contracts/cw1-whitelist/src/lib.rs b/contracts/cw1-whitelist/src/lib.rs
index 8b1bfe5..74803fe 100644
--- a/contracts/cw1-whitelist/src/lib.rs
+++ b/contracts/cw1-whitelist/src/lib.rs
@@ -1,9 +1,12 @@
-mod contract;
-mod error;
-mod multitest;
+pub mod contract;
+mod cw1;
+pub mod error;
+#[cfg(any(test, feature = "tests"))]
+pub mod multitest;
+pub mod responses;
 
 #[cfg(not(feature = "library"))]
-pub mod entry_points {
+mod entry_points {
     use cosmwasm_std::{
         entry_point, from_slice, Binary, Deps, DepsMut, Env, MessageInfo, Response,
     };
@@ -38,3 +41,6 @@ pub mod entry_points {
         from_slice::<QueryMsg>(&msg)?.dispatch(&CONTRACT, (deps, env))
     }
 }
+
+#[cfg(not(feature = "library"))]
+pub use crate::entry_points::*;
diff --git a/contracts/cw1-whitelist/src/responses.rs b/contracts/cw1-whitelist/src/responses.rs
new file mode 100644
index 0000000..8ff5438
--- /dev/null
+++ b/contracts/cw1-whitelist/src/responses.rs
@@ -0,0 +1,41 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
+pub struct AdminListResponse {
+    pub admins: Vec<String>,
+    pub mutable: bool,
+}
+
+#[cfg(any(test, feature = "test-utils"))]
+impl AdminListResponse {
+    /// Utility function forconverting message to its canonical form, so two messages with
+    /// different representation but same semantical meaning can be easly compared.
+    ///
+    /// It could be encapsulated in custom `PartialEq` implementation, but `PartialEq` is expected
+    /// to be quickly, so it seems to be reasonable to keep it as representation-equality, and
+    /// canonicalize message only when it is needed
+    ///
+    /// Example:
+    ///
+    /// ```
+    /// # use cw1_whitelist::msg::AdminListResponse;
+    ///
+    /// let resp1 = AdminListResponse {
+    ///   admins: vec!["admin1".to_owned(), "admin2".to_owned()],
+    ///   mutable: true,
+    /// };
+    ///
+    /// let resp2 = AdminListResponse {
+    ///   admins: vec!["admin2".to_owned(), "admin1".to_owned(), "admin2".to_owned()],
+    ///   mutable: true,
+    /// };
+    ///
+    /// assert_eq!(resp1.canonical(), resp2.canonical());
+    /// ```
+    pub fn canonical(mut self) -> Self {
+        self.admins.sort();
+        self.admins.dedup();
+        self
+    }
+}
diff --git a/contracts/cw1/src/lib.rs b/contracts/cw1/src/lib.rs
index 5d7c757..5f7c458 100644
--- a/contracts/cw1/src/lib.rs
+++ b/contracts/cw1/src/lib.rs
@@ -1,94 +1,115 @@
-use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response, StdError};
-
+use cosmwasm_std::{CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdError};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use sylvia::interface;
 
-#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
-pub struct FindMemberResponse {
-    pub is_present: bool,
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
+pub struct CanExecuteResp {
+    pub can_execute: bool,
 }
 
 #[interface]
 pub trait Cw1 {
     type Error: From<StdError>;
 
+    /// Execute requests the contract to re-dispatch all these messages with the
+    /// contract's address as sender. Every implementation has it's own logic to
+    /// determine in
     #[msg(exec)]
-    fn add_member(
+    fn execute(
         &self,
         ctx: (DepsMut, Env, MessageInfo),
-        member: String,
+        msgs: Vec<CosmosMsg>,
     ) -> Result<Response, Self::Error>;
 
+    /// Checks permissions of the caller on this proxy.
+    /// If CanExecute returns true then a call to `Execute` with the same message,
+    /// from the given sender, before any further state changes, should also succeed.
     #[msg(query)]
-    fn find_member(
+    fn can_execute(
         &self,
         ctx: (Deps, Env),
-        member: String,
-    ) -> Result<FindMemberResponse, Self::Error>;
+        sender: String,
+        msg: CosmosMsg,
+    ) -> Result<CanExecuteResp, Self::Error>;
 }
 
 #[cfg(test)]
 mod tests {
-    use cosmwasm_std::{from_binary, from_slice, to_binary};
+    use cosmwasm_std::{coins, from_binary, from_slice, to_binary, BankMsg};
 
     use super::*;
 
     #[test]
     fn execute() {
-        let original_msg = ExecMsg::AddMember {
-            member: "member_name".to_owned(),
+        let original = ExecMsg::Execute {
+            msgs: vec![BankMsg::Send {
+                to_address: "receiver".to_owned(),
+                amount: coins(10, "atom"),
+            }
+            .into()],
         };
 
-        let serialized_msg = to_binary(&original_msg).unwrap();
-        let serialized_msg: ExecMsg = from_binary(&serialized_msg).unwrap();
+        let serialized = to_binary(&original).unwrap();
+        let deserialized = from_binary(&serialized).unwrap();
 
-        assert_eq!(serialized_msg, original_msg);
+        assert_eq!(original, deserialized);
     }
 
     #[test]
-    fn query() {
-        let original_msg = QueryMsg::FindMember {
-            member: "member_name".to_owned(),
-        };
-
-        let serialized_msg = to_binary(&original_msg).unwrap();
-        let serialized_msg: QueryMsg = from_binary(&serialized_msg).unwrap();
-
-        assert_eq!(serialized_msg, original_msg);
+    fn execute_from_slice() {
+        let deserialized = from_slice(br#"{"execute": { "msgs": [] }}"#).unwrap();
+        assert_eq!(ExecMsg::Execute { msgs: vec![] }, deserialized);
     }
 
     #[test]
-    fn execute_from_slice() {
-        let deserialized: ExecMsg =
-            from_slice(br#"{"add_member": {"member": "some_member"}}"#).unwrap();
-        assert_eq!(
-            deserialized,
-            ExecMsg::AddMember {
-                member: "some_member".to_owned()
+    fn query() {
+        let original = QueryMsg::CanExecute {
+            sender: "sender".to_owned(),
+            msg: BankMsg::Send {
+                to_address: "receiver".to_owned(),
+                amount: coins(10, "atom"),
             }
-        );
+            .into(),
+        };
+
+        let serialized = to_binary(&original).unwrap();
+        let deserialized = from_binary(&serialized).unwrap();
+
+        assert_eq!(original, deserialized);
     }
 
     #[test]
     fn query_from_slice() {
-        let deserialized: QueryMsg =
-            from_slice(br#"{"find_member": {"member": "some_member"}}"#).unwrap();
+        let deserialized = from_slice(
+            br#"{"can_execute": {
+                "sender": "address",
+                "msg": {
+                    "bank": {
+                        "send": {
+                            "to_address": "receiver",
+                            "amount": [
+                                {
+                                    "amount": "10",
+                                    "denom": "atom"
+                                }
+                            ]
+                        }
+                    }
+                }
+            }}"#,
+        )
+        .unwrap();
         assert_eq!(
-            deserialized,
-            QueryMsg::FindMember {
-                member: "some_member".to_owned()
-            }
+            QueryMsg::CanExecute {
+                sender: "address".to_owned(),
+                msg: BankMsg::Send {
+                    to_address: "receiver".to_owned(),
+                    amount: coins(10, "atom"),
+                }
+                .into()
+            },
+            deserialized
         );
     }
-
-    #[test]
-    fn query_msgs() {
-        assert_eq!(QueryMsg::messages(), ["find_member"]);
-    }
-
-    #[test]
-    fn exec_msgs() {
-        assert_eq!(ExecMsg::messages(), ["add_member"]);
-    }
 }


Test patch:
diff --git a/contracts/cw1-whitelist/src/multitest.rs b/contracts/cw1-whitelist/src/multitest.rs
index c119b39..87aeb76 100644
--- a/contracts/cw1-whitelist/src/multitest.rs
+++ b/contracts/cw1-whitelist/src/multitest.rs
@@ -1,136 +1,128 @@
+use anyhow::{bail, Result as AnyResult};
+use cosmwasm_std::{from_slice, Binary, DepsMut, Empty, Env, MessageInfo, Reply, Response};
+use cw_multi_test::Contract;
+
+use crate::contract::{Cw1WhitelistContract, ExecMsg, InstantiateMsg, QueryMsg};
+
+impl Contract<Empty> for Cw1WhitelistContract<'_> {
+    fn instantiate(
+        &self,
+        deps: DepsMut,
+        env: Env,
+        info: MessageInfo,
+        msg: Vec<u8>,
+    ) -> AnyResult<Response> {
+        from_slice::<InstantiateMsg>(&msg)?
+            .dispatch(self, (deps, env, info))
+            .map_err(Into::into)
+    }
+
+    fn execute(
+        &self,
+        deps: DepsMut,
+        env: Env,
+        info: MessageInfo,
+        msg: Vec<u8>,
+    ) -> AnyResult<Response<Empty>> {
+        from_slice::<ExecMsg>(&msg)?
+            .dispatch(self, (deps, env, info))
+            .map_err(Into::into)
+    }
+
+    fn query(&self, deps: cosmwasm_std::Deps, env: Env, msg: Vec<u8>) -> AnyResult<Binary> {
+        from_slice::<QueryMsg>(&msg)?
+            .dispatch(self, (deps, env))
+            .map_err(Into::into)
+    }
+
+    fn sudo(&self, _deps: DepsMut, _env: Env, _msg: Vec<u8>) -> AnyResult<Response<Empty>> {
+        bail!("sudo not implemented for contract")
+    }
+
+    fn reply(&self, _deps: DepsMut, _env: Env, _msg: Reply) -> AnyResult<Response<Empty>> {
+        bail!("reply not implemented for contract")
+    }
+
+    fn migrate(&self, _deps: DepsMut, _env: Env, _msg: Vec<u8>) -> AnyResult<Response<Empty>> {
+        bail!("migrate not implemented for contract")
+    }
+}
+
 #[cfg(test)]
 mod test {
-    use crate::contract::{Cw1WhitelistContract, ExecMsg, ImplExecMsg, InstantiateMsg};
-    use anyhow::{bail, Result as AnyResult};
-    use cosmwasm_std::{
-        from_slice, Addr, Binary, DepsMut, Empty, Env, MessageInfo, Reply, Response,
-    };
-    use cw1::{FindMemberResponse, QueryMsg};
-    use cw_multi_test::{App, Contract, Executor};
-
-    impl Contract<Empty> for Cw1WhitelistContract {
-        fn instantiate(
-            &self,
-            deps: DepsMut,
-            env: Env,
-            info: MessageInfo,
-            msg: Vec<u8>,
-        ) -> AnyResult<Response<Empty>> {
-            from_slice::<InstantiateMsg>(&msg)?
-                .dispatch(self, (deps, env, info))
-                .map_err(Into::into)
-        }
-
-        fn execute(
-            &self,
-            deps: DepsMut,
-            env: Env,
-            info: MessageInfo,
-            msg: Vec<u8>,
-        ) -> AnyResult<Response<Empty>> {
-            from_slice::<ExecMsg>(&msg)?
-                .dispatch(self, (deps, env, info))
-                .map_err(Into::into)
-        }
-
-        fn query(&self, deps: cosmwasm_std::Deps, env: Env, msg: Vec<u8>) -> AnyResult<Binary> {
-            from_slice::<QueryMsg>(&msg)?
-                .dispatch(self, (deps, env))
-                .map_err(Into::into)
-        }
-
-        fn sudo(&self, _deps: DepsMut, _env: Env, _msg: Vec<u8>) -> AnyResult<Response<Empty>> {
-            bail!("sudo not implemented for contract")
-        }
-
-        fn reply(&self, _deps: DepsMut, _env: Env, _msg: Reply) -> AnyResult<Response<Empty>> {
-            bail!("reply not implemented for contract")
-        }
-
-        fn migrate(&self, _deps: DepsMut, _env: Env, _msg: Vec<u8>) -> AnyResult<Response<Empty>> {
-            bail!("migrate not implemented for contract")
-        }
-    }
+    use cosmwasm_std::{to_binary, Addr, WasmMsg};
+    use cw_multi_test::{App, Executor};
+
+    use crate::contract::{ImplExecMsg, ImplQueryMsg};
+    use crate::responses::AdminListResponse;
+    use assert_matches::assert_matches;
+
+    use super::*;
 
     #[test]
-    fn entry_points() {
+    fn proxy_freeze_message() {
         let mut app = App::default();
-        let contract_id = app.store_code(Box::new(Cw1WhitelistContract::new()));
 
-        let contract = app
+        let owner = Addr::unchecked("owner");
+
+        let code_id = app.store_code(Box::new(Cw1WhitelistContract::new()));
+
+        let first_contract = app
             .instantiate_contract(
-                contract_id,
-                Addr::unchecked("Owner"),
+                code_id,
+                owner.clone(),
                 &InstantiateMsg {
-                    members: vec!["member".to_owned()],
+                    admins: vec![owner.to_string()],
+                    mutable: true,
                 },
                 &[],
-                "contract",
+                "First contract",
                 None,
             )
             .unwrap();
 
-        app.execute_contract(
-            Addr::unchecked("owner"),
-            contract.clone(),
-            &cw1::ExecMsg::AddMember {
-                member: "other_member".to_owned(),
-            },
-            &[],
-        )
-        .unwrap();
-
-        let resp: FindMemberResponse = app
-            .wrap()
-            .query_wasm_smart(
-                contract,
-                &QueryMsg::FindMember {
-                    member: "other_member".to_owned(),
-                },
-            )
-            .unwrap();
-
-        assert_eq!(resp, FindMemberResponse { is_present: true });
-    }
-
-    #[test]
-    fn contract_exec() {
-        let mut app = App::default();
-        let contract_id = app.store_code(Box::new(Cw1WhitelistContract::new()));
-
-        let contract = app
+        let second_contract = app
             .instantiate_contract(
-                contract_id,
-                Addr::unchecked("Owner"),
+                code_id,
+                owner.clone(),
                 &InstantiateMsg {
-                    members: vec!["member".to_owned(), "other_member".to_owned()],
+                    admins: vec![first_contract.to_string()],
+                    mutable: true,
                 },
                 &[],
-                "contract",
+                "Second contract",
                 None,
             )
             .unwrap();
-
+        assert_ne!(second_contract, first_contract);
+
+        let freeze = ImplExecMsg::Freeze {};
+        let freeze = WasmMsg::Execute {
+            contract_addr: second_contract.to_string(),
+            msg: to_binary(&freeze).unwrap(),
+            funds: vec![],
+        };
         app.execute_contract(
-            Addr::unchecked("owner"),
-            contract.clone(),
-            &ImplExecMsg::RemoveMember {
-                member: "other_member".to_owned(),
+            owner,
+            first_contract,
+            &cw1::ExecMsg::Execute {
+                msgs: vec![freeze.into()],
             },
             &[],
         )
         .unwrap();
 
-        let resp: FindMemberResponse = app
+        let resp = app
             .wrap()
-            .query_wasm_smart(
-                contract,
-                &QueryMsg::FindMember {
-                    member: "other_member".to_owned(),
-                },
-            )
+            .query_wasm_smart(second_contract, &ImplQueryMsg::AdminList {})
             .unwrap();
 
-        assert_eq!(resp, FindMemberResponse { is_present: false });
+        assert_matches!(
+            resp,
+            AdminListResponse {
+                mutable,
+                ..
+            } if !mutable
+        );
     }
 }