From 64bb6ebe4f6b94a63cbba1b266dddc037622c795 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 07:39:04 +0100 Subject: [PATCH 01/14] Copy original files from cw4-group --- contracts/tg4-group/.cargo/config | 5 + contracts/tg4-group/Cargo.toml | 40 ++ contracts/tg4-group/NOTICE | 14 + contracts/tg4-group/README.md | 51 +++ contracts/tg4-group/examples/schema.rs | 22 + contracts/tg4-group/src/contract.rs | 556 +++++++++++++++++++++++++ contracts/tg4-group/src/error.rs | 19 + contracts/tg4-group/src/helpers.rs | 43 ++ contracts/tg4-group/src/lib.rs | 7 + contracts/tg4-group/src/msg.rs | 51 +++ contracts/tg4-group/src/state.rs | 16 + 11 files changed, 824 insertions(+) create mode 100644 contracts/tg4-group/.cargo/config create mode 100644 contracts/tg4-group/Cargo.toml create mode 100644 contracts/tg4-group/NOTICE create mode 100644 contracts/tg4-group/README.md create mode 100644 contracts/tg4-group/examples/schema.rs create mode 100644 contracts/tg4-group/src/contract.rs create mode 100644 contracts/tg4-group/src/error.rs create mode 100644 contracts/tg4-group/src/helpers.rs create mode 100644 contracts/tg4-group/src/lib.rs create mode 100644 contracts/tg4-group/src/msg.rs create mode 100644 contracts/tg4-group/src/state.rs diff --git a/contracts/tg4-group/.cargo/config b/contracts/tg4-group/.cargo/config new file mode 100644 index 00000000..7d1a066c --- /dev/null +++ b/contracts/tg4-group/.cargo/config @@ -0,0 +1,5 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +wasm-debug = "build --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example schema" diff --git a/contracts/tg4-group/Cargo.toml b/contracts/tg4-group/Cargo.toml new file mode 100644 index 00000000..b2038f15 --- /dev/null +++ b/contracts/tg4-group/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "cw4-group" +version = "0.12.1" +authors = ["Ethan Frey "] +edition = "2018" +description = "Simple cw4 implementation of group membership controlled by admin " +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cw-plus" +homepage = "https://cosmwasm.com" +documentation = "https://docs.cosmwasm.com" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "artifacts/*", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cw-utils = { path = "../../packages/utils", version = "0.12.1" } +cw2 = { path = "../../packages/cw2", version = "0.12.1" } +cw4 = { path = "../../packages/cw4", version = "0.12.1" } +cw-controllers = { path = "../../packages/controllers", version = "0.12.1" } +cw-storage-plus = { path = "../../packages/storage-plus", version = "0.12.1" } +cosmwasm-std = { version = "1.0.0-beta5" } +schemars = "0.8.1" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +thiserror = { version = "1.0.23" } + +[dev-dependencies] +cosmwasm-schema = { version = "1.0.0-beta5" } diff --git a/contracts/tg4-group/NOTICE b/contracts/tg4-group/NOTICE new file mode 100644 index 00000000..d459f5e2 --- /dev/null +++ b/contracts/tg4-group/NOTICE @@ -0,0 +1,14 @@ +Cw4_group +Copyright (C) 2020 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/contracts/tg4-group/README.md b/contracts/tg4-group/README.md new file mode 100644 index 00000000..19dd6008 --- /dev/null +++ b/contracts/tg4-group/README.md @@ -0,0 +1,51 @@ +# CW4 Group + +This is a basic implementation of the [cw4 spec](../../packages/cw4/README.md). +It fulfills all elements of the spec, including the raw query lookups, +and it designed to be used as a backing storage for +[cw3 compliant contracts](../../packages/cw3/README.md). + +It stores a set of members along with an admin, and allows the admin to +update the state. Raw queries (intended for cross-contract queries) +can check a given member address and the total weight. Smart queries (designed +for client API) can do the same, and also query the admin address as well as +paginate over all members. + +## Init + +To create it, you must pass in a list of members, as well as an optional +`admin`, if you wish it to be mutable. + +```rust +pub struct InitMsg { + pub admin: Option, + pub members: Vec, +} + +pub struct Member { + pub addr: HumanAddr, + pub weight: u64, +} +``` + +Members are defined by an address and a weight. This is transformed +and stored under their `CanonicalAddr`, in a format defined in +[cw4 raw queries](../../packages/cw4/README.md#raw). + +Note that 0 *is an allowed weight*. This doesn't give any voting rights, but +it does define this address is part of the group. This could be used in +e.g. a KYC whitelist to say they are allowed, but cannot participate in +decision-making. + +## Messages + +Basic update messages, queries, and hooks are defined by the +[cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. + +`cw4-group` adds one message to control the group membership: + +`UpdateMembers{add, remove}` - takes a membership diff and adds/updates the +members, as well as removing any provided addresses. If an address is on both +lists, it will be removed. If it appears multiple times in `add`, only the +last occurrence will be used. + diff --git a/contracts/tg4-group/examples/schema.rs b/contracts/tg4-group/examples/schema.rs new file mode 100644 index 00000000..01cdd167 --- /dev/null +++ b/contracts/tg4-group/examples/schema.rs @@ -0,0 +1,22 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; + +pub use cw4::{AdminResponse, MemberListResponse, MemberResponse, TotalWeightResponse}; +pub use cw4_group::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema_with_title(&schema_for!(InstantiateMsg), &out_dir, "InstantiateMsg"); + export_schema_with_title(&schema_for!(ExecuteMsg), &out_dir, "ExecuteMsg"); + export_schema_with_title(&schema_for!(QueryMsg), &out_dir, "QueryMsg"); + export_schema(&schema_for!(AdminResponse), &out_dir); + export_schema(&schema_for!(MemberListResponse), &out_dir); + export_schema(&schema_for!(MemberResponse), &out_dir); + export_schema(&schema_for!(TotalWeightResponse), &out_dir); +} diff --git a/contracts/tg4-group/src/contract.rs b/contracts/tg4-group/src/contract.rs new file mode 100644 index 00000000..913d427f --- /dev/null +++ b/contracts/tg4-group/src/contract.rs @@ -0,0 +1,556 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + attr, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, + SubMsg, +}; +use cw2::set_contract_version; +use cw4::{ + Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, + TotalWeightResponse, +}; +use cw_storage_plus::Bound; +use cw_utils::maybe_addr; + +use crate::error::ContractError; +use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use crate::state::{ADMIN, HOOKS, MEMBERS, TOTAL}; + +// version info for migration info +const CONTRACT_NAME: &str = "crates.io:cw4-group"; +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +// Note, you can use StdResult in some functions where you do not +// make use of the custom errors +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + create(deps, msg.admin, msg.members, env.block.height)?; + Ok(Response::default()) +} + +// create is the instantiation logic with set_contract_version removed so it can more +// easily be imported in other contracts +pub fn create( + mut deps: DepsMut, + admin: Option, + members: Vec, + height: u64, +) -> Result<(), ContractError> { + let admin_addr = admin + .map(|admin| deps.api.addr_validate(&admin)) + .transpose()?; + ADMIN.set(deps.branch(), admin_addr)?; + + let mut total = 0u64; + for member in members.into_iter() { + total += member.weight; + let member_addr = deps.api.addr_validate(&member.addr)?; + MEMBERS.save(deps.storage, &member_addr, &member.weight, height)?; + } + TOTAL.save(deps.storage, &total)?; + + Ok(()) +} + +// And declare a custom Error variant for the ones where you will want to make use of it +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let api = deps.api; + match msg { + ExecuteMsg::UpdateAdmin { admin } => Ok(ADMIN.execute_update_admin( + deps, + info, + admin.map(|admin| api.addr_validate(&admin)).transpose()?, + )?), + ExecuteMsg::UpdateMembers { add, remove } => { + execute_update_members(deps, env, info, add, remove) + } + ExecuteMsg::AddHook { addr } => { + Ok(HOOKS.execute_add_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?) + } + ExecuteMsg::RemoveHook { addr } => { + Ok(HOOKS.execute_remove_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?) + } + } +} + +pub fn execute_update_members( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + add: Vec, + remove: Vec, +) -> Result { + let attributes = vec![ + attr("action", "update_members"), + attr("added", add.len().to_string()), + attr("removed", remove.len().to_string()), + attr("sender", &info.sender), + ]; + + // make the local update + let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; + // call all registered hooks + let messages = HOOKS.prepare_hooks(deps.storage, |h| { + diff.clone().into_cosmos_msg(h).map(SubMsg::new) + })?; + Ok(Response::new() + .add_submessages(messages) + .add_attributes(attributes)) +} + +// the logic from execute_update_members extracted for easier import +pub fn update_members( + deps: DepsMut, + height: u64, + sender: Addr, + to_add: Vec, + to_remove: Vec, +) -> Result { + ADMIN.assert_admin(deps.as_ref(), &sender)?; + + let mut total = TOTAL.load(deps.storage)?; + let mut diffs: Vec = vec![]; + + // add all new members and update total + for add in to_add.into_iter() { + let add_addr = deps.api.addr_validate(&add.addr)?; + MEMBERS.update(deps.storage, &add_addr, height, |old| -> StdResult<_> { + total -= old.unwrap_or_default(); + total += add.weight; + diffs.push(MemberDiff::new(add.addr, old, Some(add.weight))); + Ok(add.weight) + })?; + } + + for remove in to_remove.into_iter() { + let remove_addr = deps.api.addr_validate(&remove)?; + let old = MEMBERS.may_load(deps.storage, &remove_addr)?; + // Only process this if they were actually in the list before + if let Some(weight) = old { + diffs.push(MemberDiff::new(remove, Some(weight), None)); + total -= weight; + MEMBERS.remove(deps.storage, &remove_addr, height)?; + } + } + + TOTAL.save(deps.storage, &total)?; + Ok(MemberChangedHookMsg { diffs }) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Member { + addr, + at_height: height, + } => to_binary(&query_member(deps, addr, height)?), + QueryMsg::ListMembers { start_after, limit } => { + to_binary(&list_members(deps, start_after, limit)?) + } + QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?), + QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), + QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?), + } +} + +fn query_total_weight(deps: Deps) -> StdResult { + let weight = TOTAL.load(deps.storage)?; + Ok(TotalWeightResponse { weight }) +} + +fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { + let addr = deps.api.addr_validate(&addr)?; + let weight = match height { + Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h), + None => MEMBERS.may_load(deps.storage, &addr), + }?; + Ok(MemberResponse { weight }) +} + +// settings for pagination +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +fn list_members( + deps: Deps, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let addr = maybe_addr(deps.api, start_after)?; + let start = addr.as_ref().map(Bound::exclusive); + + let members = MEMBERS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + item.map(|(addr, weight)| Member { + addr: addr.into(), + weight, + }) + }) + .collect::>()?; + + Ok(MemberListResponse { members }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{from_slice, Api, OwnedDeps, Querier, Storage}; + use cw4::{member_key, TOTAL_KEY}; + use cw_controllers::{AdminError, HookError}; + + const INIT_ADMIN: &str = "juan"; + const USER1: &str = "somebody"; + const USER2: &str = "else"; + const USER3: &str = "funny"; + + fn do_instantiate(deps: DepsMut) { + let msg = InstantiateMsg { + admin: Some(INIT_ADMIN.into()), + members: vec![ + Member { + addr: USER1.into(), + weight: 11, + }, + Member { + addr: USER2.into(), + weight: 6, + }, + ], + }; + let info = mock_info("creator", &[]); + instantiate(deps, mock_env(), info, msg).unwrap(); + } + + #[test] + fn proper_instantiation() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // it worked, let's query the state + let res = ADMIN.query_admin(deps.as_ref()).unwrap(); + assert_eq!(Some(INIT_ADMIN.into()), res.admin); + + let res = query_total_weight(deps.as_ref()).unwrap(); + assert_eq!(17, res.weight); + } + + #[test] + fn try_member_queries() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap(); + assert_eq!(member1.weight, Some(11)); + + let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap(); + assert_eq!(member2.weight, Some(6)); + + let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap(); + assert_eq!(member3.weight, None); + + let members = list_members(deps.as_ref(), None, None).unwrap(); + assert_eq!(members.members.len(), 2); + // TODO: assert the set is proper + } + + fn assert_users( + deps: &OwnedDeps, + user1_weight: Option, + user2_weight: Option, + user3_weight: Option, + height: Option, + ) { + let member1 = query_member(deps.as_ref(), USER1.into(), height).unwrap(); + assert_eq!(member1.weight, user1_weight); + + let member2 = query_member(deps.as_ref(), USER2.into(), height).unwrap(); + assert_eq!(member2.weight, user2_weight); + + let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap(); + assert_eq!(member3.weight, user3_weight); + + // this is only valid if we are not doing a historical query + if height.is_none() { + // compute expected metrics + let weights = vec![user1_weight, user2_weight, user3_weight]; + let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum(); + let count = weights.iter().filter(|x| x.is_some()).count(); + + // TODO: more detailed compare? + let members = list_members(deps.as_ref(), None, None).unwrap(); + assert_eq!(count, members.members.len()); + + let total = query_total_weight(deps.as_ref()).unwrap(); + assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 + } + } + + #[test] + fn add_new_remove_old_member() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // add a new one and remove existing one + let add = vec![Member { + addr: USER3.into(), + weight: 15, + }]; + let remove = vec![USER1.into()]; + + // non-admin cannot update + let height = mock_env().block.height; + let err = update_members( + deps.as_mut(), + height + 5, + Addr::unchecked(USER1), + add.clone(), + remove.clone(), + ) + .unwrap_err(); + assert_eq!(err, AdminError::NotAdmin {}.into()); + + // Test the values from instantiate + assert_users(&deps, Some(11), Some(6), None, None); + // Note all values were set at height, the beginning of that block was all None + assert_users(&deps, None, None, None, Some(height)); + // This will get us the values at the start of the block after instantiate (expected initial values) + assert_users(&deps, Some(11), Some(6), None, Some(height + 1)); + + // admin updates properly + update_members( + deps.as_mut(), + height + 10, + Addr::unchecked(INIT_ADMIN), + add, + remove, + ) + .unwrap(); + + // updated properly + assert_users(&deps, None, Some(6), Some(15), None); + + // snapshot still shows old value + assert_users(&deps, Some(11), Some(6), None, Some(height + 1)); + } + + #[test] + fn add_old_remove_new_member() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // add a new one and remove existing one + let add = vec![Member { + addr: USER1.into(), + weight: 4, + }]; + let remove = vec![USER3.into()]; + + // admin updates properly + let height = mock_env().block.height; + update_members( + deps.as_mut(), + height, + Addr::unchecked(INIT_ADMIN), + add, + remove, + ) + .unwrap(); + assert_users(&deps, Some(4), Some(6), None, None); + } + + #[test] + fn add_and_remove_same_member() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // USER1 is updated and remove in the same call, we should remove this an add member3 + let add = vec![ + Member { + addr: USER1.into(), + weight: 20, + }, + Member { + addr: USER3.into(), + weight: 5, + }, + ]; + let remove = vec![USER1.into()]; + + // admin updates properly + let height = mock_env().block.height; + update_members( + deps.as_mut(), + height, + Addr::unchecked(INIT_ADMIN), + add, + remove, + ) + .unwrap(); + assert_users(&deps, None, Some(6), Some(5), None); + } + + #[test] + fn add_remove_hooks() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert!(hooks.hooks.is_empty()); + + let contract1 = String::from("hook1"); + let contract2 = String::from("hook2"); + + let add_msg = ExecuteMsg::AddHook { + addr: contract1.clone(), + }; + + // non-admin cannot add hook + let user_info = mock_info(USER1, &[]); + let err = execute( + deps.as_mut(), + mock_env(), + user_info.clone(), + add_msg.clone(), + ) + .unwrap_err(); + assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); + + // admin can add it, and it appears in the query + let admin_info = mock_info(INIT_ADMIN, &[]); + let _ = execute( + deps.as_mut(), + mock_env(), + admin_info.clone(), + add_msg.clone(), + ) + .unwrap(); + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract1.clone()]); + + // cannot remove a non-registered contract + let remove_msg = ExecuteMsg::RemoveHook { + addr: contract2.clone(), + }; + let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), remove_msg).unwrap_err(); + assert_eq!(err, HookError::HookNotRegistered {}.into()); + + // add second contract + let add_msg2 = ExecuteMsg::AddHook { + addr: contract2.clone(), + }; + let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); + + // cannot re-add an existing contract + let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err(); + assert_eq!(err, HookError::HookAlreadyRegistered {}.into()); + + // non-admin cannot remove + let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 }; + let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err(); + assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); + + // remove the original + let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap(); + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract2]); + } + + #[test] + fn hooks_fire() { + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); + assert!(hooks.hooks.is_empty()); + + let contract1 = String::from("hook1"); + let contract2 = String::from("hook2"); + + // register 2 hooks + let admin_info = mock_info(INIT_ADMIN, &[]); + let add_msg = ExecuteMsg::AddHook { + addr: contract1.clone(), + }; + let add_msg2 = ExecuteMsg::AddHook { + addr: contract2.clone(), + }; + for msg in vec![add_msg, add_msg2] { + let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap(); + } + + // make some changes - add 3, remove 2, and update 1 + // USER1 is updated and remove in the same call, we should remove this an add member3 + let add = vec![ + Member { + addr: USER1.into(), + weight: 20, + }, + Member { + addr: USER3.into(), + weight: 5, + }, + ]; + let remove = vec![USER2.into()]; + let msg = ExecuteMsg::UpdateMembers { remove, add }; + + // admin updates properly + assert_users(&deps, Some(11), Some(6), None, None); + let res = execute(deps.as_mut(), mock_env(), admin_info, msg).unwrap(); + assert_users(&deps, Some(20), None, Some(5), None); + + // ensure 2 messages for the 2 hooks + assert_eq!(res.messages.len(), 2); + // same order as in the message (adds first, then remove) + let diffs = vec![ + MemberDiff::new(USER1, Some(11), Some(20)), + MemberDiff::new(USER3, None, Some(5)), + MemberDiff::new(USER2, Some(6), None), + ]; + let hook_msg = MemberChangedHookMsg { diffs }; + let msg1 = SubMsg::new(hook_msg.clone().into_cosmos_msg(contract1).unwrap()); + let msg2 = SubMsg::new(hook_msg.into_cosmos_msg(contract2).unwrap()); + assert_eq!(res.messages, vec![msg1, msg2]); + } + + #[test] + fn raw_queries_work() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(); + do_instantiate(deps.as_mut()); + + // get total from raw key + let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap(); + let total: u64 = from_slice(&total_raw).unwrap(); + assert_eq!(17, total); + + // get member votes from raw key + let member2_raw = deps.storage.get(&member_key(USER2)).unwrap(); + let member2: u64 = from_slice(&member2_raw).unwrap(); + assert_eq!(6, member2); + + // and execute misses + let member3_raw = deps.storage.get(&member_key(USER3)); + assert_eq!(None, member3_raw); + } +} diff --git a/contracts/tg4-group/src/error.rs b/contracts/tg4-group/src/error.rs new file mode 100644 index 00000000..82a84fe8 --- /dev/null +++ b/contracts/tg4-group/src/error.rs @@ -0,0 +1,19 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +use cw_controllers::{AdminError, HookError}; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Hook(#[from] HookError), + + #[error("{0}")] + Admin(#[from] AdminError), + + #[error("Unauthorized")] + Unauthorized {}, +} diff --git a/contracts/tg4-group/src/helpers.rs b/contracts/tg4-group/src/helpers.rs new file mode 100644 index 00000000..add7451a --- /dev/null +++ b/contracts/tg4-group/src/helpers.rs @@ -0,0 +1,43 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; + +use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg}; +use cw4::{Cw4Contract, Member}; + +use crate::msg::ExecuteMsg; + +/// Cw4GroupContract is a wrapper around Cw4Contract that provides a lot of helpers +/// for working with cw4-group contracts. +/// +/// It extends Cw4Contract to add the extra calls from cw4-group. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Cw4GroupContract(pub Cw4Contract); + +impl Deref for Cw4GroupContract { + type Target = Cw4Contract; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Cw4GroupContract { + pub fn new(addr: Addr) -> Self { + Cw4GroupContract(Cw4Contract(addr)) + } + + fn encode_msg(&self, msg: ExecuteMsg) -> StdResult { + Ok(WasmMsg::Execute { + contract_addr: self.addr().into(), + msg: to_binary(&msg)?, + funds: vec![], + } + .into()) + } + + pub fn update_members(&self, remove: Vec, add: Vec) -> StdResult { + let msg = ExecuteMsg::UpdateMembers { remove, add }; + self.encode_msg(msg) + } +} diff --git a/contracts/tg4-group/src/lib.rs b/contracts/tg4-group/src/lib.rs new file mode 100644 index 00000000..98208b78 --- /dev/null +++ b/contracts/tg4-group/src/lib.rs @@ -0,0 +1,7 @@ +pub mod contract; +pub mod error; +pub mod helpers; +pub mod msg; +pub mod state; + +pub use crate::error::ContractError; diff --git a/contracts/tg4-group/src/msg.rs b/contracts/tg4-group/src/msg.rs new file mode 100644 index 00000000..e1759ac5 --- /dev/null +++ b/contracts/tg4-group/src/msg.rs @@ -0,0 +1,51 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cw4::Member; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub struct InstantiateMsg { + /// The admin is the only account that can update the group state. + /// Omit it to make the group immutable. + pub admin: Option, + pub members: Vec, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + /// Change the admin + UpdateAdmin { admin: Option }, + /// apply a diff to the existing members. + /// remove is applied after add, so if an address is in both, it is removed + UpdateMembers { + remove: Vec, + add: Vec, + }, + /// Add a new hook to be informed of all membership changes. Must be called by Admin + AddHook { addr: String }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: String }, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// Return AdminResponse + Admin {}, + /// Return TotalWeightResponse + TotalWeight {}, + /// Returns MembersListResponse + ListMembers { + start_after: Option, + limit: Option, + }, + /// Returns MemberResponse + Member { + addr: String, + at_height: Option, + }, + /// Shows all registered hooks. Returns HooksResponse. + Hooks {}, +} diff --git a/contracts/tg4-group/src/state.rs b/contracts/tg4-group/src/state.rs new file mode 100644 index 00000000..1b5003c9 --- /dev/null +++ b/contracts/tg4-group/src/state.rs @@ -0,0 +1,16 @@ +use cosmwasm_std::Addr; +use cw4::TOTAL_KEY; +use cw_controllers::{Admin, Hooks}; +use cw_storage_plus::{Item, SnapshotMap, Strategy}; + +pub const ADMIN: Admin = Admin::new("admin"); +pub const HOOKS: Hooks = Hooks::new("cw4-hooks"); + +pub const TOTAL: Item = Item::new(TOTAL_KEY); + +pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( + cw4::MEMBERS_KEY, + cw4::MEMBERS_CHECKPOINTS, + cw4::MEMBERS_CHANGELOG, + Strategy::EveryBlock, +); From d2cbb53068437bcc1046b08820f62d17b0ffedcd Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 07:41:19 +0100 Subject: [PATCH 02/14] Rename cw4 to tg4 in docs / cargo toml --- contracts/tg4-group/Cargo.toml | 18 +++++++++--------- contracts/tg4-group/NOTICE | 4 ++-- contracts/tg4-group/README.md | 18 +++++++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/contracts/tg4-group/Cargo.toml b/contracts/tg4-group/Cargo.toml index b2038f15..0cc1ba5b 100644 --- a/contracts/tg4-group/Cargo.toml +++ b/contracts/tg4-group/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "cw4-group" -version = "0.12.1" +name = "tg4-group" +version = "0.6.0" authors = ["Ethan Frey "] edition = "2018" -description = "Simple cw4 implementation of group membership controlled by admin " +description = "Simple tg4 implementation of group membership controlled by admin" license = "Apache-2.0" -repository = "https://github.com/CosmWasm/cw-plus" +repository = "https://github.com/CosmWasm/tfi" homepage = "https://cosmwasm.com" documentation = "https://docs.cosmwasm.com" @@ -26,11 +26,11 @@ backtraces = ["cosmwasm-std/backtraces"] library = [] [dependencies] -cw-utils = { path = "../../packages/utils", version = "0.12.1" } -cw2 = { path = "../../packages/cw2", version = "0.12.1" } -cw4 = { path = "../../packages/cw4", version = "0.12.1" } -cw-controllers = { path = "../../packages/controllers", version = "0.12.1" } -cw-storage-plus = { path = "../../packages/storage-plus", version = "0.12.1" } +cw-utils = "0.11.1" +cw2 = "0.11.1" +tg4 = "0.6.0" +cw-controllers = "0.11.1" +cw-storage-plus = "0.11.1" cosmwasm-std = { version = "1.0.0-beta5" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/tg4-group/NOTICE b/contracts/tg4-group/NOTICE index d459f5e2..05e27a88 100644 --- a/contracts/tg4-group/NOTICE +++ b/contracts/tg4-group/NOTICE @@ -1,5 +1,5 @@ -Cw4_group -Copyright (C) 2020 Confio OÜ +Tg4_group +Copyright (C) 2022 Confio Gmbh Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/contracts/tg4-group/README.md b/contracts/tg4-group/README.md index 19dd6008..c3f0e219 100644 --- a/contracts/tg4-group/README.md +++ b/contracts/tg4-group/README.md @@ -1,13 +1,13 @@ -# CW4 Group +# TG4 Group -This is a basic implementation of the [cw4 spec](../../packages/cw4/README.md). +This is a basic implementation of the [tg4 spec](../../packages/tg4/README.md). It fulfills all elements of the spec, including the raw query lookups, and it designed to be used as a backing storage for [cw3 compliant contracts](../../packages/cw3/README.md). It stores a set of members along with an admin, and allows the admin to update the state. Raw queries (intended for cross-contract queries) -can check a given member address and the total weight. Smart queries (designed +can check a given member address and the total points. Smart queries (designed for client API) can do the same, and also query the admin address as well as paginate over all members. @@ -24,15 +24,15 @@ pub struct InitMsg { pub struct Member { pub addr: HumanAddr, - pub weight: u64, + pub points: u64, } ``` -Members are defined by an address and a weight. This is transformed +Members are defined by an address and a number of points. This is transformed and stored under their `CanonicalAddr`, in a format defined in -[cw4 raw queries](../../packages/cw4/README.md#raw). +[tg4 raw queries](../../packages/tg4/README.md#raw). -Note that 0 *is an allowed weight*. This doesn't give any voting rights, but +Note that 0 *is an allowed number of points*. This doesn't give any voting rights, but it does define this address is part of the group. This could be used in e.g. a KYC whitelist to say they are allowed, but cannot participate in decision-making. @@ -40,9 +40,9 @@ decision-making. ## Messages Basic update messages, queries, and hooks are defined by the -[cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. +[tg4 spec](../../packages/tg4/README.md). Please refer to it for more info. -`cw4-group` adds one message to control the group membership: +`tg4-group` adds one message to control the group membership: `UpdateMembers{add, remove}` - takes a membership diff and adds/updates the members, as well as removing any provided addresses. If an address is on both From 2430186cb75ffc343e57be5cc2b78c11823831b0 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 07:49:22 +0100 Subject: [PATCH 03/14] Renamed [Cc]w4 to [Tt]g4 in source files --- contracts/tg4-group/src/contract.rs | 6 +++--- contracts/tg4-group/src/helpers.rs | 18 +++++++++--------- contracts/tg4-group/src/msg.rs | 2 +- contracts/tg4-group/src/state.rs | 10 +++++----- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contracts/tg4-group/src/contract.rs b/contracts/tg4-group/src/contract.rs index 913d427f..2b7f25af 100644 --- a/contracts/tg4-group/src/contract.rs +++ b/contracts/tg4-group/src/contract.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ SubMsg, }; use cw2::set_contract_version; -use cw4::{ +use tg4::{ Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, TotalWeightResponse, }; @@ -17,7 +17,7 @@ use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::state::{ADMIN, HOOKS, MEMBERS, TOTAL}; // version info for migration info -const CONTRACT_NAME: &str = "crates.io:cw4-group"; +const CONTRACT_NAME: &str = "crates.io:tg4-group"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); // Note, you can use StdResult in some functions where you do not @@ -211,7 +211,7 @@ mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{from_slice, Api, OwnedDeps, Querier, Storage}; - use cw4::{member_key, TOTAL_KEY}; + use tg4::{member_key, TOTAL_KEY}; use cw_controllers::{AdminError, HookError}; const INIT_ADMIN: &str = "juan"; diff --git a/contracts/tg4-group/src/helpers.rs b/contracts/tg4-group/src/helpers.rs index add7451a..da50b2e2 100644 --- a/contracts/tg4-group/src/helpers.rs +++ b/contracts/tg4-group/src/helpers.rs @@ -3,28 +3,28 @@ use serde::{Deserialize, Serialize}; use std::ops::Deref; use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg}; -use cw4::{Cw4Contract, Member}; +use tg4::{Tg4Contract, Member}; use crate::msg::ExecuteMsg; -/// Cw4GroupContract is a wrapper around Cw4Contract that provides a lot of helpers -/// for working with cw4-group contracts. +/// Tg4GroupContract is a wrapper around Tg4Contract that provides a lot of helpers +/// for working with tg4-group contracts. /// -/// It extends Cw4Contract to add the extra calls from cw4-group. +/// It extends Tg4Contract to add the extra calls from tg4-group. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Cw4GroupContract(pub Cw4Contract); +pub struct Tg4GroupContract(pub Tg4Contract); -impl Deref for Cw4GroupContract { - type Target = Cw4Contract; +impl Deref for Tg4GroupContract { + type Target = Tg4Contract; fn deref(&self) -> &Self::Target { &self.0 } } -impl Cw4GroupContract { +impl Tg4GroupContract { pub fn new(addr: Addr) -> Self { - Cw4GroupContract(Cw4Contract(addr)) + Tg4GroupContract(Tg4Contract(addr)) } fn encode_msg(&self, msg: ExecuteMsg) -> StdResult { diff --git a/contracts/tg4-group/src/msg.rs b/contracts/tg4-group/src/msg.rs index e1759ac5..060bd762 100644 --- a/contracts/tg4-group/src/msg.rs +++ b/contracts/tg4-group/src/msg.rs @@ -1,7 +1,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use cw4::Member; +use tg4::Member; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] diff --git a/contracts/tg4-group/src/state.rs b/contracts/tg4-group/src/state.rs index 1b5003c9..18738069 100644 --- a/contracts/tg4-group/src/state.rs +++ b/contracts/tg4-group/src/state.rs @@ -1,16 +1,16 @@ use cosmwasm_std::Addr; -use cw4::TOTAL_KEY; +use tg4::TOTAL_KEY; use cw_controllers::{Admin, Hooks}; use cw_storage_plus::{Item, SnapshotMap, Strategy}; pub const ADMIN: Admin = Admin::new("admin"); -pub const HOOKS: Hooks = Hooks::new("cw4-hooks"); +pub const HOOKS: Hooks = Hooks::new("tg4-hooks"); pub const TOTAL: Item = Item::new(TOTAL_KEY); pub const MEMBERS: SnapshotMap<&Addr, u64> = SnapshotMap::new( - cw4::MEMBERS_KEY, - cw4::MEMBERS_CHECKPOINTS, - cw4::MEMBERS_CHANGELOG, + tg4::MEMBERS_KEY, + tg4::MEMBERS_CHECKPOINTS, + tg4::MEMBERS_CHANGELOG, Strategy::EveryBlock, ); From 7371620894fe55e5a71010d8e21402caf261edd1 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 07:55:05 +0100 Subject: [PATCH 04/14] Rename [Ww]eight to [Pp]oints in source files --- contracts/tg4-group/src/contract.rs | 88 ++++++++++++++--------------- contracts/tg4-group/src/helpers.rs | 2 +- contracts/tg4-group/src/msg.rs | 4 +- contracts/tg4-group/src/state.rs | 2 +- 4 files changed, 48 insertions(+), 48 deletions(-) diff --git a/contracts/tg4-group/src/contract.rs b/contracts/tg4-group/src/contract.rs index 2b7f25af..260bea84 100644 --- a/contracts/tg4-group/src/contract.rs +++ b/contracts/tg4-group/src/contract.rs @@ -5,12 +5,12 @@ use cosmwasm_std::{ SubMsg, }; use cw2::set_contract_version; +use cw_storage_plus::Bound; +use cw_utils::maybe_addr; use tg4::{ Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, - TotalWeightResponse, + TotalPointsResponse, }; -use cw_storage_plus::Bound; -use cw_utils::maybe_addr; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; @@ -49,9 +49,9 @@ pub fn create( let mut total = 0u64; for member in members.into_iter() { - total += member.weight; + total += member.points; let member_addr = deps.api.addr_validate(&member.addr)?; - MEMBERS.save(deps.storage, &member_addr, &member.weight, height)?; + MEMBERS.save(deps.storage, &member_addr, &member.points, height)?; } TOTAL.save(deps.storage, &total)?; @@ -128,9 +128,9 @@ pub fn update_members( let add_addr = deps.api.addr_validate(&add.addr)?; MEMBERS.update(deps.storage, &add_addr, height, |old| -> StdResult<_> { total -= old.unwrap_or_default(); - total += add.weight; - diffs.push(MemberDiff::new(add.addr, old, Some(add.weight))); - Ok(add.weight) + total += add.points; + diffs.push(MemberDiff::new(add.addr, old, Some(add.points))); + Ok(add.points) })?; } @@ -138,9 +138,9 @@ pub fn update_members( let remove_addr = deps.api.addr_validate(&remove)?; let old = MEMBERS.may_load(deps.storage, &remove_addr)?; // Only process this if they were actually in the list before - if let Some(weight) = old { - diffs.push(MemberDiff::new(remove, Some(weight), None)); - total -= weight; + if let Some(points) = old { + diffs.push(MemberDiff::new(remove, Some(points), None)); + total -= points; MEMBERS.remove(deps.storage, &remove_addr, height)?; } } @@ -159,24 +159,24 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ListMembers { start_after, limit } => { to_binary(&list_members(deps, start_after, limit)?) } - QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?), + QueryMsg::TotalPoints {} => to_binary(&query_total_points(deps)?), QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?), } } -fn query_total_weight(deps: Deps) -> StdResult { - let weight = TOTAL.load(deps.storage)?; - Ok(TotalWeightResponse { weight }) +fn query_total_points(deps: Deps) -> StdResult { + let points = TOTAL.load(deps.storage)?; + Ok(TotalPointsResponse { points }) } fn query_member(deps: Deps, addr: String, height: Option) -> StdResult { let addr = deps.api.addr_validate(&addr)?; - let weight = match height { + let points = match height { Some(h) => MEMBERS.may_load_at_height(deps.storage, &addr, h), None => MEMBERS.may_load(deps.storage, &addr), }?; - Ok(MemberResponse { weight }) + Ok(MemberResponse { points }) } // settings for pagination @@ -196,9 +196,9 @@ fn list_members( .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|item| { - item.map(|(addr, weight)| Member { + item.map(|(addr, points)| Member { addr: addr.into(), - weight, + points, }) }) .collect::>()?; @@ -211,8 +211,8 @@ mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{from_slice, Api, OwnedDeps, Querier, Storage}; - use tg4::{member_key, TOTAL_KEY}; use cw_controllers::{AdminError, HookError}; + use tg4::{member_key, TOTAL_KEY}; const INIT_ADMIN: &str = "juan"; const USER1: &str = "somebody"; @@ -225,11 +225,11 @@ mod tests { members: vec![ Member { addr: USER1.into(), - weight: 11, + points: 11, }, Member { addr: USER2.into(), - weight: 6, + points: 6, }, ], }; @@ -246,8 +246,8 @@ mod tests { let res = ADMIN.query_admin(deps.as_ref()).unwrap(); assert_eq!(Some(INIT_ADMIN.into()), res.admin); - let res = query_total_weight(deps.as_ref()).unwrap(); - assert_eq!(17, res.weight); + let res = query_total_points(deps.as_ref()).unwrap(); + assert_eq!(17, res.points); } #[test] @@ -256,13 +256,13 @@ mod tests { do_instantiate(deps.as_mut()); let member1 = query_member(deps.as_ref(), USER1.into(), None).unwrap(); - assert_eq!(member1.weight, Some(11)); + assert_eq!(member1.points, Some(11)); let member2 = query_member(deps.as_ref(), USER2.into(), None).unwrap(); - assert_eq!(member2.weight, Some(6)); + assert_eq!(member2.points, Some(6)); let member3 = query_member(deps.as_ref(), USER3.into(), None).unwrap(); - assert_eq!(member3.weight, None); + assert_eq!(member3.points, None); let members = list_members(deps.as_ref(), None, None).unwrap(); assert_eq!(members.members.len(), 2); @@ -271,33 +271,33 @@ mod tests { fn assert_users( deps: &OwnedDeps, - user1_weight: Option, - user2_weight: Option, - user3_weight: Option, + user1_points: Option, + user2_points: Option, + user3_points: Option, height: Option, ) { let member1 = query_member(deps.as_ref(), USER1.into(), height).unwrap(); - assert_eq!(member1.weight, user1_weight); + assert_eq!(member1.points, user1_points); let member2 = query_member(deps.as_ref(), USER2.into(), height).unwrap(); - assert_eq!(member2.weight, user2_weight); + assert_eq!(member2.points, user2_points); let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap(); - assert_eq!(member3.weight, user3_weight); + assert_eq!(member3.points, user3_points); // this is only valid if we are not doing a historical query if height.is_none() { // compute expected metrics - let weights = vec![user1_weight, user2_weight, user3_weight]; - let sum: u64 = weights.iter().map(|x| x.unwrap_or_default()).sum(); - let count = weights.iter().filter(|x| x.is_some()).count(); + let points = vec![user1_points, user2_points, user3_points]; + let sum: u64 = points.iter().map(|x| x.unwrap_or_default()).sum(); + let count = points.iter().filter(|x| x.is_some()).count(); // TODO: more detailed compare? let members = list_members(deps.as_ref(), None, None).unwrap(); assert_eq!(count, members.members.len()); - let total = query_total_weight(deps.as_ref()).unwrap(); - assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 + let total = query_total_points(deps.as_ref()).unwrap(); + assert_eq!(sum, total.points); // 17 - 11 + 15 = 21 } } @@ -309,7 +309,7 @@ mod tests { // add a new one and remove existing one let add = vec![Member { addr: USER3.into(), - weight: 15, + points: 15, }]; let remove = vec![USER1.into()]; @@ -358,7 +358,7 @@ mod tests { // add a new one and remove existing one let add = vec![Member { addr: USER1.into(), - weight: 4, + points: 4, }]; let remove = vec![USER3.into()]; @@ -385,11 +385,11 @@ mod tests { let add = vec![ Member { addr: USER1.into(), - weight: 20, + points: 20, }, Member { addr: USER3.into(), - weight: 5, + points: 5, }, ]; let remove = vec![USER1.into()]; @@ -504,11 +504,11 @@ mod tests { let add = vec![ Member { addr: USER1.into(), - weight: 20, + points: 20, }, Member { addr: USER3.into(), - weight: 5, + points: 5, }, ]; let remove = vec![USER2.into()]; diff --git a/contracts/tg4-group/src/helpers.rs b/contracts/tg4-group/src/helpers.rs index da50b2e2..20e8332a 100644 --- a/contracts/tg4-group/src/helpers.rs +++ b/contracts/tg4-group/src/helpers.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use std::ops::Deref; use cosmwasm_std::{to_binary, Addr, CosmosMsg, StdResult, WasmMsg}; -use tg4::{Tg4Contract, Member}; +use tg4::{Member, Tg4Contract}; use crate::msg::ExecuteMsg; diff --git a/contracts/tg4-group/src/msg.rs b/contracts/tg4-group/src/msg.rs index 060bd762..9bb2877b 100644 --- a/contracts/tg4-group/src/msg.rs +++ b/contracts/tg4-group/src/msg.rs @@ -34,8 +34,8 @@ pub enum ExecuteMsg { pub enum QueryMsg { /// Return AdminResponse Admin {}, - /// Return TotalWeightResponse - TotalWeight {}, + /// Return TotalPointsResponse + TotalPoints {}, /// Returns MembersListResponse ListMembers { start_after: Option, diff --git a/contracts/tg4-group/src/state.rs b/contracts/tg4-group/src/state.rs index 18738069..de41754e 100644 --- a/contracts/tg4-group/src/state.rs +++ b/contracts/tg4-group/src/state.rs @@ -1,7 +1,7 @@ use cosmwasm_std::Addr; -use tg4::TOTAL_KEY; use cw_controllers::{Admin, Hooks}; use cw_storage_plus::{Item, SnapshotMap, Strategy}; +use tg4::TOTAL_KEY; pub const ADMIN: Admin = Admin::new("admin"); pub const HOOKS: Hooks = Hooks::new("tg4-hooks"); From d12bd96a8a210ed982c1c3e52411f072a32d54e8 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 07:56:50 +0100 Subject: [PATCH 05/14] Return to untyped bounds for cw-plus 0.11 compat --- contracts/tg4-group/src/contract.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tg4-group/src/contract.rs b/contracts/tg4-group/src/contract.rs index 260bea84..1492b861 100644 --- a/contracts/tg4-group/src/contract.rs +++ b/contracts/tg4-group/src/contract.rs @@ -190,7 +190,7 @@ fn list_members( ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let addr = maybe_addr(deps.api, start_after)?; - let start = addr.as_ref().map(Bound::exclusive); + let start = addr.map(|addr| Bound::exclusive(addr.as_ref())); let members = MEMBERS .range(deps.storage, start, None, Order::Ascending) From a24cbb3fc07d6bc6e0a4df75f5111b85f9aeeff1 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 08:45:01 +0100 Subject: [PATCH 06/14] Add tg-bindings/utils deps --- contracts/tg4-group/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/tg4-group/Cargo.toml b/contracts/tg4-group/Cargo.toml index 0cc1ba5b..97595121 100644 --- a/contracts/tg4-group/Cargo.toml +++ b/contracts/tg4-group/Cargo.toml @@ -28,7 +28,9 @@ library = [] [dependencies] cw-utils = "0.11.1" cw2 = "0.11.1" -tg4 = "0.6.0" +tg-bindings = { version = "0.6.0", path = "../../packages/bindings" } +tg-utils = { version = "0.6.0", path = "../../packages/utils" } +tg4 = { version = "0.6.0", path = "../../packages/tg4" } cw-controllers = "0.11.1" cw-storage-plus = "0.11.1" cosmwasm-std = { version = "1.0.0-beta5" } From 487a55e680787a03163ce53affc88ff309fd8e6b Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 08:46:10 +0100 Subject: [PATCH 07/14] Use tg-bindings TgradeMsg, and tg-utils Hooks --- contracts/tg4-group/src/contract.rs | 69 ++++++++++++++++++++++++----- contracts/tg4-group/src/error.rs | 3 +- contracts/tg4-group/src/state.rs | 3 +- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/contracts/tg4-group/src/contract.rs b/contracts/tg4-group/src/contract.rs index 1492b861..8e3326e7 100644 --- a/contracts/tg4-group/src/contract.rs +++ b/contracts/tg4-group/src/contract.rs @@ -1,21 +1,25 @@ +use crate::ContractError::Unauthorized; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - attr, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, StdResult, - SubMsg, + attr, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Order, StdResult, }; use cw2::set_contract_version; use cw_storage_plus::Bound; use cw_utils::maybe_addr; use tg4::{ - Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, + HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, TotalPointsResponse, }; +use tg_bindings::TgradeMsg; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; use crate::state::{ADMIN, HOOKS, MEMBERS, TOTAL}; +pub type Response = cosmwasm_std::Response; +pub type SubMsg = cosmwasm_std::SubMsg; + // version info for migration info const CONTRACT_NAME: &str = "crates.io:tg4-group"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -76,12 +80,8 @@ pub fn execute( ExecuteMsg::UpdateMembers { add, remove } => { execute_update_members(deps, env, info, add, remove) } - ExecuteMsg::AddHook { addr } => { - Ok(HOOKS.execute_add_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?) - } - ExecuteMsg::RemoveHook { addr } => { - Ok(HOOKS.execute_remove_hook(&ADMIN, deps, info, api.addr_validate(&addr)?)?) - } + ExecuteMsg::AddHook { addr } => execute_add_hook(deps, info, addr), + ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, info, addr), } } @@ -110,6 +110,52 @@ pub fn execute_update_members( .add_attributes(attributes)) } +pub fn execute_add_hook( + deps: DepsMut, + info: MessageInfo, + hook: String, +) -> Result { + // Same as cw4-group guard: being admin + if !ADMIN.is_admin(deps.as_ref(), &info.sender)? { + return Err(Unauthorized {}); + } + + // add the hook + HOOKS.add_hook(deps.storage, deps.api.addr_validate(&hook)?)?; + + // response + let res = Response::new() + .add_attribute("action", "add_hook") + .add_attribute("hook", hook) + .add_attribute("sender", info.sender); + Ok(res) +} + +pub fn execute_remove_hook( + deps: DepsMut, + info: MessageInfo, + hook: String, +) -> Result { + // custom guard: self-removal OR being admin + let hook_addr = deps.api.addr_validate(&hook)?; + if info.sender != hook_addr && !ADMIN.is_admin(deps.as_ref(), &info.sender)? { + // return Err(ContractError::Unauthorized( + // "Hook address is not same as sender's or sender is not an admin".to_owned(), + // )); + return Err(ContractError::Unauthorized {}); + } + + // remove the hook + HOOKS.remove_hook(deps.storage, hook_addr)?; + + // response + let resp = Response::new() + .add_attribute("action", "remove_hook") + .add_attribute("hook", hook) + .add_attribute("sender", info.sender); + Ok(resp) +} + // the logic from execute_update_members extracted for easier import pub fn update_members( deps: DepsMut, @@ -161,7 +207,10 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { } QueryMsg::TotalPoints {} => to_binary(&query_total_points(deps)?), QueryMsg::Admin {} => to_binary(&ADMIN.query_admin(deps)?), - QueryMsg::Hooks {} => to_binary(&HOOKS.query_hooks(deps)?), + QueryMsg::Hooks {} => { + let hooks = HOOKS.list_hooks(deps.storage)?; + to_binary(&HooksResponse { hooks }) + } } } diff --git a/contracts/tg4-group/src/error.rs b/contracts/tg4-group/src/error.rs index 82a84fe8..cb1812e2 100644 --- a/contracts/tg4-group/src/error.rs +++ b/contracts/tg4-group/src/error.rs @@ -1,7 +1,8 @@ use cosmwasm_std::StdError; use thiserror::Error; -use cw_controllers::{AdminError, HookError}; +use cw_controllers::AdminError; +use tg_utils::HookError; #[derive(Error, Debug, PartialEq)] pub enum ContractError { diff --git a/contracts/tg4-group/src/state.rs b/contracts/tg4-group/src/state.rs index de41754e..577178bc 100644 --- a/contracts/tg4-group/src/state.rs +++ b/contracts/tg4-group/src/state.rs @@ -1,7 +1,8 @@ use cosmwasm_std::Addr; -use cw_controllers::{Admin, Hooks}; +use cw_controllers::Admin; use cw_storage_plus::{Item, SnapshotMap, Strategy}; use tg4::TOTAL_KEY; +use tg_utils::Hooks; pub const ADMIN: Admin = Admin::new("admin"); pub const HOOKS: Hooks = Hooks::new("tg4-hooks"); From cb6d8ba97fc216391e58d8fe31a4d5733084f047 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 08:50:32 +0100 Subject: [PATCH 08/14] Update lock file --- Cargo.lock | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 98bbbc15..65930f47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1579,6 +1579,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tg4-group" +version = "0.6.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "cw-utils", + "cw2", + "schemars", + "serde", + "tg-bindings", + "tg-utils", + "tg4", + "thiserror", +] + [[package]] name = "tg4-mixer" version = "0.6.0" From f0831bb465e8e84314b321df11a901fa8d062fe4 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 09:20:44 +0100 Subject: [PATCH 09/14] Fix / adapt tg4-group unit tests --- contracts/tg4-group/src/contract.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/contracts/tg4-group/src/contract.rs b/contracts/tg4-group/src/contract.rs index 8e3326e7..360cbfa2 100644 --- a/contracts/tg4-group/src/contract.rs +++ b/contracts/tg4-group/src/contract.rs @@ -260,8 +260,9 @@ mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{from_slice, Api, OwnedDeps, Querier, Storage}; - use cw_controllers::{AdminError, HookError}; + use cw_controllers::AdminError; use tg4::{member_key, TOTAL_KEY}; + use tg_utils::HookError; const INIT_ADMIN: &str = "juan"; const USER1: &str = "somebody"; @@ -462,8 +463,8 @@ mod tests { let mut deps = mock_dependencies(); do_instantiate(deps.as_mut()); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert!(hooks.hooks.is_empty()); + let hooks = HOOKS.list_hooks(&deps.storage).unwrap(); + assert!(hooks.is_empty()); let contract1 = String::from("hook1"); let contract2 = String::from("hook2"); @@ -481,7 +482,7 @@ mod tests { add_msg.clone(), ) .unwrap_err(); - assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); + assert_eq!(err, ContractError::Unauthorized {}); // admin can add it, and it appears in the query let admin_info = mock_info(INIT_ADMIN, &[]); @@ -492,8 +493,8 @@ mod tests { add_msg.clone(), ) .unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract1.clone()]); + let hooks = HOOKS.list_hooks(&deps.storage).unwrap(); + assert_eq!(hooks, vec![contract1.clone()]); // cannot remove a non-registered contract let remove_msg = ExecuteMsg::RemoveHook { @@ -507,8 +508,8 @@ mod tests { addr: contract2.clone(), }; let _ = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); + let hooks = HOOKS.list_hooks(&deps.storage).unwrap(); + assert_eq!(hooks, vec![contract1.clone(), contract2.clone()]); // cannot re-add an existing contract let err = execute(deps.as_mut(), mock_env(), admin_info.clone(), add_msg).unwrap_err(); @@ -517,12 +518,12 @@ mod tests { // non-admin cannot remove let remove_msg = ExecuteMsg::RemoveHook { addr: contract1 }; let err = execute(deps.as_mut(), mock_env(), user_info, remove_msg.clone()).unwrap_err(); - assert_eq!(err, HookError::Admin(AdminError::NotAdmin {}).into()); + assert_eq!(err, ContractError::Unauthorized {}); // remove the original let _ = execute(deps.as_mut(), mock_env(), admin_info, remove_msg).unwrap(); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract2]); + let hooks = HOOKS.list_hooks(&deps.storage).unwrap(); + assert_eq!(hooks, vec![contract2]); } #[test] @@ -530,8 +531,8 @@ mod tests { let mut deps = mock_dependencies(); do_instantiate(deps.as_mut()); - let hooks = HOOKS.query_hooks(deps.as_ref()).unwrap(); - assert!(hooks.hooks.is_empty()); + let hooks = HOOKS.list_hooks(&deps.storage).unwrap(); + assert!(hooks.is_empty()); let contract1 = String::from("hook1"); let contract2 = String::from("hook2"); From 262308bf01c80d0883bcde1ee1e538a74e3e8f1a Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 09:21:56 +0100 Subject: [PATCH 10/14] Rename cw4 to tg4, Weight to Points in schema generator --- contracts/tg4-group/examples/schema.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/tg4-group/examples/schema.rs b/contracts/tg4-group/examples/schema.rs index 01cdd167..f5ed6fe2 100644 --- a/contracts/tg4-group/examples/schema.rs +++ b/contracts/tg4-group/examples/schema.rs @@ -3,8 +3,8 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; -pub use cw4::{AdminResponse, MemberListResponse, MemberResponse, TotalWeightResponse}; -pub use cw4_group::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +pub use tg4::{AdminResponse, MemberListResponse, MemberResponse, TotalPointsResponse}; +pub use tg4_group::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { let mut out_dir = current_dir().unwrap(); @@ -18,5 +18,5 @@ fn main() { export_schema(&schema_for!(AdminResponse), &out_dir); export_schema(&schema_for!(MemberListResponse), &out_dir); export_schema(&schema_for!(MemberResponse), &out_dir); - export_schema(&schema_for!(TotalWeightResponse), &out_dir); + export_schema(&schema_for!(TotalPointsResponse), &out_dir); } From 3b5a683bab2f6eba197a513c75576c44a18ee785 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 09:34:24 +0100 Subject: [PATCH 11/14] Add tg4-group to CI --- .circleci/config.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9c1dd7c9..63617756 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,7 @@ workflows: test: jobs: - contract_tg4_engagement + - contract_tg4_group - contract_tg4_mixer - contract_tg4_stake - contract_tgrade_community_pool @@ -65,6 +66,33 @@ jobs: - target key: cargocache-tg4-engagement-rust:1.58.1-{{ checksum "~/project/Cargo.lock" }} + contract_tg4_group: + docker: + - image: rust:1.58.1 + working_directory: ~/project/contracts/tg4-group + steps: + - checkout: + path: ~/project + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - cargocache-tg4-group-rust:1.58.1-{{ checksum "~/project/Cargo.lock" }} + - run: + name: Unit Tests + environment: + RUST_BACKTRACE: 1 + command: cargo unit-test --locked + - run: + name: Build and run schema generator + command: cargo schema --locked + - save_cache: + paths: + - /usr/local/cargo/registry + - target + key: cargocache-tg4-group-rust:1.58.1-{{ checksum "~/project/Cargo.lock" }} + contract_tg4_mixer: docker: - image: rust:1.58.1 From 4329a853e274172ddb2475cc32f048fb49698b5f Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 09:36:24 +0100 Subject: [PATCH 12/14] Add `tg4-group` to publish script --- scripts/publish.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/publish.sh b/scripts/publish.sh index e9c090c2..2dbe694f 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -5,7 +5,7 @@ command -v shellcheck >/dev/null && shellcheck "$0" # These are imported by other packages - wait 30 seconds between each as they have linear dependencies BASE_CRATES="packages/bindings packages/bindings-test packages/tg4 packages/utils contracts/tg4-engagement contracts/tg4-stake contracts/tg4-mixer packages/tg3 packages/voting-contract" -ALL_CRATES="packages/test-utils contracts/tgrade-community-pool contracts/tgrade-validator-voting contracts/tgrade-valset" +ALL_CRATES="packages/test-utils contracts/tgrade-community-pool contracts/tgrade-validator-voting contracts/tgrade-valset contracts/tg4-group" SLEEP_TIME=30 From 5400fbfded5a584ffee2f163ede67c3d224f07bf Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 09:55:28 +0100 Subject: [PATCH 13/14] Update author Co-authored-by: Jakub Bogucki --- contracts/tg4-group/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tg4-group/Cargo.toml b/contracts/tg4-group/Cargo.toml index 97595121..329df96d 100644 --- a/contracts/tg4-group/Cargo.toml +++ b/contracts/tg4-group/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tg4-group" version = "0.6.0" -authors = ["Ethan Frey "] +authors = ["Mauro Lacy "] edition = "2018" description = "Simple tg4 implementation of group membership controlled by admin" license = "Apache-2.0" From 397af92005552f0f681d0509d6260ce8d6afdb94 Mon Sep 17 00:00:00 2001 From: Mauro Lacy Date: Wed, 16 Feb 2022 09:55:51 +0100 Subject: [PATCH 14/14] Update repository link Co-authored-by: Jakub Bogucki --- contracts/tg4-group/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/tg4-group/Cargo.toml b/contracts/tg4-group/Cargo.toml index 329df96d..9473b97c 100644 --- a/contracts/tg4-group/Cargo.toml +++ b/contracts/tg4-group/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Mauro Lacy "] edition = "2018" description = "Simple tg4 implementation of group membership controlled by admin" license = "Apache-2.0" -repository = "https://github.com/CosmWasm/tfi" +repository = "https://github.com/confio/poe-contracts" homepage = "https://cosmwasm.com" documentation = "https://docs.cosmwasm.com"