From 18d972df92bb286518ba20fe11b55ed8507d392a Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 10 Dec 2020 22:50:10 +0100 Subject: [PATCH 01/22] Add cw4-stake as copy of cw4-group --- .circleci/config.yml | 36 + Cargo.lock | 15 + Cargo.toml | 4 + contracts/cw4-group/Cargo.toml | 5 + contracts/cw4-stake/.cargo/config | 5 + contracts/cw4-stake/Cargo.toml | 39 + contracts/cw4-stake/NOTICE | 14 + contracts/cw4-stake/README.md | 55 ++ contracts/cw4-stake/examples/schema.rs | 22 + .../cw4-stake/schema/admin_response.json | 22 + contracts/cw4-stake/schema/handle_msg.json | 123 ++++ contracts/cw4-stake/schema/init_msg.json | 50 ++ .../schema/member_list_response.json | 39 + .../cw4-stake/schema/member_response.json | 15 + contracts/cw4-stake/schema/query_msg.json | 107 +++ .../schema/total_weight_response.json | 15 + contracts/cw4-stake/src/contract.rs | 664 ++++++++++++++++++ contracts/cw4-stake/src/error.rs | 11 + contracts/cw4-stake/src/lib.rs | 11 + contracts/cw4-stake/src/msg.rs | 7 + contracts/cw4-stake/src/state.rs | 14 + scripts/publish.sh | 4 +- 22 files changed, 1275 insertions(+), 2 deletions(-) create mode 100644 contracts/cw4-stake/.cargo/config create mode 100644 contracts/cw4-stake/Cargo.toml create mode 100644 contracts/cw4-stake/NOTICE create mode 100644 contracts/cw4-stake/README.md create mode 100644 contracts/cw4-stake/examples/schema.rs create mode 100644 contracts/cw4-stake/schema/admin_response.json create mode 100644 contracts/cw4-stake/schema/handle_msg.json create mode 100644 contracts/cw4-stake/schema/init_msg.json create mode 100644 contracts/cw4-stake/schema/member_list_response.json create mode 100644 contracts/cw4-stake/schema/member_response.json create mode 100644 contracts/cw4-stake/schema/query_msg.json create mode 100644 contracts/cw4-stake/schema/total_weight_response.json create mode 100644 contracts/cw4-stake/src/contract.rs create mode 100644 contracts/cw4-stake/src/error.rs create mode 100644 contracts/cw4-stake/src/lib.rs create mode 100644 contracts/cw4-stake/src/msg.rs create mode 100644 contracts/cw4-stake/src/state.rs diff --git a/.circleci/config.yml b/.circleci/config.yml index 527f3b9e8..b5f74f2f3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,6 +8,7 @@ workflows: - contract_cw3_fixed_multisig - contract_cw3_flex_multisig - contract_cw4_group + - contract_cw4_stake - contract_cw20_atomic_swap - contract_cw20_base - contract_cw20_escrow @@ -247,6 +248,41 @@ jobs: - target key: cargocache-cw4-group-rust:1.47.0-{{ checksum "~/project/Cargo.lock" }} + contract_cw4_stake: + docker: + - image: rust:1.47.0 + working_directory: ~/project/contracts/cw4-stake + steps: + - checkout: + path: ~/project + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version + - restore_cache: + keys: + - cargocache-cw4-stake-rust:1.47.0-{{ checksum "~/project/Cargo.lock" }} + - run: + name: Unit Tests + env: RUST_BACKTRACE=1 + command: cargo unit-test --locked + - run: + name: Build and run schema generator + command: cargo schema --locked + - run: + name: Ensure checked-in schemas are up-to-date + command: | + CHANGES_IN_REPO=$(git status --porcelain) + if [[ -n "$CHANGES_IN_REPO" ]]; then + echo "Repository is dirty. Showing 'git status' and 'git --no-pager diff' for debugging now:" + git status && git --no-pager diff + exit 1 + fi + - save_cache: + paths: + - /usr/local/cargo/registry + - target + key: cargocache-cw4-stake-rust:1.47.0-{{ checksum "~/project/Cargo.lock" }} + contract_cw20_base: docker: - image: rust:1.47.0 diff --git a/Cargo.lock b/Cargo.lock index e9e7abad3..64893f06f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,6 +296,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw4-stake" +version = "0.3.2" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw0", + "cw2", + "cw4", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw721" version = "0.3.2" diff --git a/Cargo.toml b/Cargo.toml index 3afff2397..d25f75b31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,10 @@ incremental = false codegen-units = 1 incremental = false +[profile.release.package.cw4-stake] +codegen-units = 1 +incremental = false + [profile.release.package.cw20-atomic-swap] codegen-units = 1 incremental = false diff --git a/contracts/cw4-group/Cargo.toml b/contracts/cw4-group/Cargo.toml index 024cc6559..905931077 100644 --- a/contracts/cw4-group/Cargo.toml +++ b/contracts/cw4-group/Cargo.toml @@ -3,6 +3,11 @@ name = "cw4-group" version = "0.3.2" authors = ["Ethan Frey "] edition = "2018" +description = "Simple cw4 implementation of group membership controlled by admin " +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cosmwasm-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. diff --git a/contracts/cw4-stake/.cargo/config b/contracts/cw4-stake/.cargo/config new file mode 100644 index 000000000..7d1a066c8 --- /dev/null +++ b/contracts/cw4-stake/.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/cw4-stake/Cargo.toml b/contracts/cw4-stake/Cargo.toml new file mode 100644 index 000000000..a422bc119 --- /dev/null +++ b/contracts/cw4-stake/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "cw4-stake" +version = "0.3.2" +authors = ["Ethan Frey "] +edition = "2018" +description = "CW4 implementation of group based on staked tokens" +license = "Apache-2.0" +repository = "https://github.com/CosmWasm/cosmwasm-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 init/handle/query exports +library = [] + +[dependencies] +cw0 = { path = "../../packages/cw0", version = "0.3.2" } +cw2 = { path = "../../packages/cw2", version = "0.3.2" } +cw4 = { path = "../../packages/cw4", version = "0.3.2" } +cw-storage-plus = { path = "../../packages/storage-plus", version = "0.3.2", features = ["iterator"] } +cosmwasm-std = { version = "0.12.0" } +schemars = "0.7" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +thiserror = { version = "1.0.21" } + +[dev-dependencies] +cosmwasm-schema = { version = "0.12.0" } diff --git a/contracts/cw4-stake/NOTICE b/contracts/cw4-stake/NOTICE new file mode 100644 index 000000000..4a0722a3c --- /dev/null +++ b/contracts/cw4-stake/NOTICE @@ -0,0 +1,14 @@ +Cw4-Stake: implementation of group based on staked tokens +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/cw4-stake/README.md b/contracts/cw4-stake/README.md new file mode 100644 index 000000000..524f489a1 --- /dev/null +++ b/contracts/cw4-stake/README.md @@ -0,0 +1,55 @@ +# CW4 Stake + +This is a second implementation of the [cw4 spec](../../packages/cw4/README.md). +It fufills 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 provides a similar API to [`cw4-group`] (which handles elected membership), +but rather than appointing members (by admin or multisig), their +membership and weight is based on the number of tokens they have staked. +This is similar to many DAOs. + +Only one denom can be bonded with both `min_bond` as the minimum amount +that must be sent by one address to enter, as well as `tokens_per_weight`, +which can be used to normalize the weight (eg. if the token is uatom +and you want 1 weight per ATOM, you can set `tokens_per_weight = 1_000_000`). + +There is also an unbonding period (`Duration`) which sets how long the +tokens are frozen before being released. These frozen tokens can neither +be used for voting, nor claimed by the original owner. Only after the period +can you get your tokens back. This liquidity loss is the "skin in the game" +provided by staking to this contract. + +## Init + +**TODO** + +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 +eg. a KYC whitelist to say they are allowed, but cannot participate in +decision-making. + +## Messages + +Update messages and queries are defined by the +[cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. \ No newline at end of file diff --git a/contracts/cw4-stake/examples/schema.rs b/contracts/cw4-stake/examples/schema.rs new file mode 100644 index 000000000..42277092e --- /dev/null +++ b/contracts/cw4-stake/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_stake::msg::{HandleMsg, InitMsg, 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(&mut schema_for!(InitMsg), &out_dir, "InitMsg"); + export_schema_with_title(&mut schema_for!(HandleMsg), &out_dir, "HandleMsg"); + export_schema_with_title(&mut 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/cw4-stake/schema/admin_response.json b/contracts/cw4-stake/schema/admin_response.json new file mode 100644 index 000000000..71edb382e --- /dev/null +++ b/contracts/cw4-stake/schema/admin_response.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "type": "object", + "properties": { + "admin": { + "anyOf": [ + { + "$ref": "#/definitions/HumanAddr" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "HumanAddr": { + "type": "string" + } + } +} diff --git a/contracts/cw4-stake/schema/handle_msg.json b/contracts/cw4-stake/schema/handle_msg.json new file mode 100644 index 000000000..bb66c7771 --- /dev/null +++ b/contracts/cw4-stake/schema/handle_msg.json @@ -0,0 +1,123 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HandleMsg", + "anyOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "properties": { + "admin": { + "anyOf": [ + { + "$ref": "#/definitions/HumanAddr" + }, + { + "type": "null" + } + ] + } + } + } + } + }, + { + "description": "apply a diff to the existing members. remove is applied after add, so if an address is in both, it is removed", + "type": "object", + "required": [ + "update_members" + ], + "properties": { + "update_members": { + "type": "object", + "required": [ + "add", + "remove" + ], + "properties": { + "add": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + }, + "remove": { + "type": "array", + "items": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + } + }, + { + "description": "Add a new hook to be informed of all membership changes. Must be called by Admin", + "type": "object", + "required": [ + "add_hook" + ], + "properties": { + "add_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + }, + { + "description": "Remove a hook. Must be called by Admin", + "type": "object", + "required": [ + "remove_hook" + ], + "properties": { + "remove_hook": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + } + ], + "definitions": { + "HumanAddr": { + "type": "string" + }, + "Member": { + "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", + "type": "object", + "required": [ + "addr", + "weight" + ], + "properties": { + "addr": { + "$ref": "#/definitions/HumanAddr" + }, + "weight": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + } +} diff --git a/contracts/cw4-stake/schema/init_msg.json b/contracts/cw4-stake/schema/init_msg.json new file mode 100644 index 000000000..f52208750 --- /dev/null +++ b/contracts/cw4-stake/schema/init_msg.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InitMsg", + "type": "object", + "required": [ + "members" + ], + "properties": { + "admin": { + "description": "The admin is the only account that can update the group state. Omit it to make the group immutable.", + "anyOf": [ + { + "$ref": "#/definitions/HumanAddr" + }, + { + "type": "null" + } + ] + }, + "members": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + } + }, + "definitions": { + "HumanAddr": { + "type": "string" + }, + "Member": { + "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", + "type": "object", + "required": [ + "addr", + "weight" + ], + "properties": { + "addr": { + "$ref": "#/definitions/HumanAddr" + }, + "weight": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + } +} diff --git a/contracts/cw4-stake/schema/member_list_response.json b/contracts/cw4-stake/schema/member_list_response.json new file mode 100644 index 000000000..cad6cf104 --- /dev/null +++ b/contracts/cw4-stake/schema/member_list_response.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MemberListResponse", + "type": "object", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "$ref": "#/definitions/Member" + } + } + }, + "definitions": { + "HumanAddr": { + "type": "string" + }, + "Member": { + "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", + "type": "object", + "required": [ + "addr", + "weight" + ], + "properties": { + "addr": { + "$ref": "#/definitions/HumanAddr" + }, + "weight": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + } +} diff --git a/contracts/cw4-stake/schema/member_response.json b/contracts/cw4-stake/schema/member_response.json new file mode 100644 index 000000000..c78ed8623 --- /dev/null +++ b/contracts/cw4-stake/schema/member_response.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MemberResponse", + "type": "object", + "properties": { + "weight": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } +} diff --git a/contracts/cw4-stake/schema/query_msg.json b/contracts/cw4-stake/schema/query_msg.json new file mode 100644 index 000000000..f8461d9a3 --- /dev/null +++ b/contracts/cw4-stake/schema/query_msg.json @@ -0,0 +1,107 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "anyOf": [ + { + "description": "Return AdminResponse", + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object" + } + } + }, + { + "description": "Return TotalWeightResponse", + "type": "object", + "required": [ + "total_weight" + ], + "properties": { + "total_weight": { + "type": "object" + } + } + }, + { + "description": "Returns MembersListResponse", + "type": "object", + "required": [ + "list_members" + ], + "properties": { + "list_members": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "anyOf": [ + { + "$ref": "#/definitions/HumanAddr" + }, + { + "type": "null" + } + ] + } + } + } + } + }, + { + "description": "Returns MemberResponse", + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "type": "object", + "required": [ + "addr" + ], + "properties": { + "addr": { + "$ref": "#/definitions/HumanAddr" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + } + } + } + }, + { + "description": "Shows all registered hooks. Returns HooksResponse.", + "type": "object", + "required": [ + "hooks" + ], + "properties": { + "hooks": { + "type": "object" + } + } + } + ], + "definitions": { + "HumanAddr": { + "type": "string" + } + } +} diff --git a/contracts/cw4-stake/schema/total_weight_response.json b/contracts/cw4-stake/schema/total_weight_response.json new file mode 100644 index 000000000..61db7a998 --- /dev/null +++ b/contracts/cw4-stake/schema/total_weight_response.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "TotalWeightResponse", + "type": "object", + "required": [ + "weight" + ], + "properties": { + "weight": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } +} diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs new file mode 100644 index 000000000..d19686863 --- /dev/null +++ b/contracts/cw4-stake/src/contract.rs @@ -0,0 +1,664 @@ +use cosmwasm_std::{ + to_binary, Api, Binary, CanonicalAddr, Deps, DepsMut, Env, HandleResponse, HumanAddr, + InitResponse, MessageInfo, Order, StdResult, +}; +use cw0::{ + hooks::{add_hook, prepare_hooks, remove_hook, HOOKS}, + maybe_canonical, +}; +use cw2::set_contract_version; +use cw4::{ + AdminResponse, HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, + MemberResponse, TotalWeightResponse, +}; +use cw_storage_plus::Bound; + +use crate::error::ContractError; +use crate::msg::{HandleMsg, InitMsg, QueryMsg}; +use crate::state::{ADMIN, 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 +pub fn init(deps: DepsMut, env: Env, _info: MessageInfo, msg: InitMsg) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + create(deps, msg.admin, msg.members, env.block.height)?; + Ok(InitResponse::default()) +} + +// create is the init logic with set_contract_version removed so it can more +// easily be imported in other contracts +pub fn create( + deps: DepsMut, + admin: Option, + members: Vec, + height: u64, +) -> StdResult<()> { + let admin_raw = maybe_canonical(deps.api, admin)?; + ADMIN.save(deps.storage, &admin_raw)?; + + let mut total = 0u64; + for member in members.into_iter() { + total += member.weight; + let raw = deps.api.canonical_address(&member.addr)?; + MEMBERS.save(deps.storage, &raw, &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 +pub fn handle( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: HandleMsg, +) -> Result { + match msg { + HandleMsg::UpdateAdmin { admin } => handle_update_admin(deps, info, admin), + HandleMsg::UpdateMembers { add, remove } => { + handle_update_members(deps, env, info, add, remove) + } + HandleMsg::AddHook { addr } => handle_add_hook(deps, info, addr), + HandleMsg::RemoveHook { addr } => handle_remove_hook(deps, info, addr), + } +} + +pub fn handle_update_admin( + deps: DepsMut, + info: MessageInfo, + new_admin: Option, +) -> Result { + update_admin(deps, info.sender, new_admin)?; + Ok(HandleResponse::default()) +} + +// the logic from handle_update_admin extracted for easier import +pub fn update_admin( + deps: DepsMut, + sender: HumanAddr, + new_admin: Option, +) -> Result, ContractError> { + let api = deps.api; + ADMIN.update(deps.storage, |state| -> Result<_, ContractError> { + assert_admin(api, sender, state)?; + let new_admin = maybe_canonical(api, new_admin)?; + Ok(new_admin) + }) +} + +pub fn handle_update_members( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + add: Vec, + remove: Vec, +) -> Result { + // make the local update + let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; + // call all registered hooks + let messages = prepare_hooks(deps.storage, |h| diff.clone().into_cosmos_msg(h))?; + Ok(HandleResponse { + messages, + attributes: vec![], + data: None, + }) +} + +// the logic from handle_update_admin extracted for easier import +pub fn update_members( + deps: DepsMut, + height: u64, + sender: HumanAddr, + to_add: Vec, + to_remove: Vec, +) -> Result { + let admin = ADMIN.load(deps.storage)?; + assert_admin(deps.api, sender, admin)?; + + 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 raw = deps.api.canonical_address(&add.addr)?; + MEMBERS.update(deps.storage, &raw, 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 raw = deps.api.canonical_address(&remove)?; + let old = MEMBERS.may_load(deps.storage, &raw)?; + // 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, &raw, height)?; + } + } + + TOTAL.save(deps.storage, &total)?; + Ok(MemberChangedHookMsg { diffs }) +} + +fn assert_admin( + api: &dyn Api, + sender: HumanAddr, + admin: Option, +) -> Result<(), ContractError> { + let owner = match admin { + Some(x) => x, + None => return Err(ContractError::Unauthorized {}), + }; + if api.canonical_address(&sender)? != owner { + Err(ContractError::Unauthorized {}) + } else { + Ok(()) + } +} + +pub fn handle_add_hook( + deps: DepsMut, + info: MessageInfo, + addr: HumanAddr, +) -> Result { + let admin = ADMIN.load(deps.storage)?; + assert_admin(deps.api, info.sender, admin)?; + add_hook(deps.storage, addr)?; + Ok(HandleResponse::default()) +} + +pub fn handle_remove_hook( + deps: DepsMut, + info: MessageInfo, + addr: HumanAddr, +) -> Result { + let admin = ADMIN.load(deps.storage)?; + assert_admin(deps.api, info.sender, admin)?; + remove_hook(deps.storage, addr)?; + Ok(HandleResponse::default()) +} + +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::Admin {} => to_binary(&query_admin(deps)?), + QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?), + QueryMsg::Hooks {} => to_binary(&query_hooks(deps)?), + } +} + +fn query_admin(deps: Deps) -> StdResult { + let canon = ADMIN.load(deps.storage)?; + let admin = canon.map(|c| deps.api.human_address(&c)).transpose()?; + Ok(AdminResponse { admin }) +} + +fn query_hooks(deps: Deps) -> StdResult { + let hooks = HOOKS.may_load(deps.storage)?.unwrap_or_default(); + Ok(HooksResponse { hooks }) +} + +fn query_total_weight(deps: Deps) -> StdResult { + let weight = TOTAL.load(deps.storage)?; + Ok(TotalWeightResponse { weight }) +} + +fn query_member(deps: Deps, addr: HumanAddr, height: Option) -> StdResult { + let raw = deps.api.canonical_address(&addr)?; + let weight = match height { + Some(h) => MEMBERS.may_load_at_height(deps.storage, &raw, h), + None => MEMBERS.may_load(deps.storage, &raw), + }?; + 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 canon = maybe_canonical(deps.api, start_after)?; + let start = canon.map(Bound::exclusive); + + let api = &deps.api; + let members: StdResult> = MEMBERS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|item| { + let (key, weight) = item?; + Ok(Member { + addr: api.human_address(&CanonicalAddr::from(key))?, + weight, + }) + }) + .collect(); + + Ok(MemberListResponse { members: members? }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + use cosmwasm_std::{from_slice, OwnedDeps, Querier, StdError, Storage}; + use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; + use cw4::{member_key, TOTAL_KEY}; + + const ADMIN: &str = "juan"; + const USER1: &str = "somebody"; + const USER2: &str = "else"; + const USER3: &str = "funny"; + + fn do_init(deps: DepsMut) { + let msg = InitMsg { + admin: Some(ADMIN.into()), + members: vec![ + Member { + addr: USER1.into(), + weight: 11, + }, + Member { + addr: USER2.into(), + weight: 6, + }, + ], + }; + let info = mock_info("creator", &[]); + init(deps, mock_env(), info, msg).unwrap(); + } + + #[test] + fn proper_initialization() { + let mut deps = mock_dependencies(&[]); + do_init(deps.as_mut()); + + // it worked, let's query the state + let res = query_admin(deps.as_ref()).unwrap(); + assert_eq!(Some(HumanAddr::from(ADMIN)), res.admin); + + let res = query_total_weight(deps.as_ref()).unwrap(); + assert_eq!(17, res.weight); + } + + #[test] + fn try_update_admin() { + let mut deps = mock_dependencies(&[]); + do_init(deps.as_mut()); + + // a member cannot update admin + let err = update_admin(deps.as_mut(), USER1.into(), Some(USER3.into())).unwrap_err(); + match err { + ContractError::Unauthorized {} => {} + e => panic!("Unexpected error: {}", e), + } + + // admin can change it + update_admin(deps.as_mut(), ADMIN.into(), Some(USER3.into())).unwrap(); + assert_eq!( + query_admin(deps.as_ref()).unwrap().admin, + Some(USER3.into()) + ); + + // and unset it + update_admin(deps.as_mut(), USER3.into(), None).unwrap(); + assert_eq!(query_admin(deps.as_ref()).unwrap().admin, None); + + // no one can change it now + let err = update_admin(deps.as_mut(), USER3.into(), Some(USER1.into())).unwrap_err(); + match err { + ContractError::Unauthorized {} => {} + e => panic!("Unexpected error: {}", e), + } + } + + #[test] + fn try_member_queries() { + let mut deps = mock_dependencies(&[]); + do_init(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_init(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, + USER1.into(), + add.clone(), + remove.clone(), + ) + .unwrap_err(); + match err { + ContractError::Unauthorized {} => {} + e => panic!("Unexpected error: {}", e), + } + + // Test the values from init + 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 init (expected initial values) + assert_users(&deps, Some(11), Some(6), None, Some(height + 1)); + + // admin updates properly + update_members(deps.as_mut(), height + 10, ADMIN.into(), 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_init(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, ADMIN.into(), 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_init(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, ADMIN.into(), 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_init(deps.as_mut()); + + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert!(hooks.hooks.is_empty()); + + let contract1 = HumanAddr::from("hook1"); + let contract2 = HumanAddr::from("hook2"); + + let add_msg = HandleMsg::AddHook { + addr: contract1.clone(), + }; + + // non-admin cannot add hook + let user_info = mock_info(USER1, &[]); + let err = handle( + deps.as_mut(), + mock_env(), + user_info.clone(), + add_msg.clone(), + ) + .unwrap_err(); + match err { + ContractError::Unauthorized {} => {} + e => panic!("Unexpected error: {}", e), + } + + // admin can add it, and it appears in the query + let admin_info = mock_info(ADMIN, &[]); + let _ = handle( + deps.as_mut(), + mock_env(), + admin_info.clone(), + add_msg.clone(), + ) + .unwrap(); + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract1.clone()]); + + // cannot remove a non-registered contract + let remove_msg = HandleMsg::RemoveHook { + addr: contract2.clone(), + }; + let err = handle( + deps.as_mut(), + mock_env(), + admin_info.clone(), + remove_msg.clone(), + ) + .unwrap_err(); + + match err { + ContractError::Std(StdError::GenericErr { msg, .. }) => { + assert_eq!(msg, HOOK_NOT_REGISTERED) + } + e => panic!("Unexpected error: {}", e), + } + + // add second contract + let add_msg2 = HandleMsg::AddHook { + addr: contract2.clone(), + }; + let _ = handle(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); + + // cannot re-add an existing contract + let err = handle( + deps.as_mut(), + mock_env(), + admin_info.clone(), + add_msg.clone(), + ) + .unwrap_err(); + match err { + ContractError::Std(StdError::GenericErr { msg, .. }) => { + assert_eq!(msg, HOOK_ALREADY_REGISTERED) + } + e => panic!("Unexpected error: {}", e), + } + + // non-admin cannot remove + let remove_msg = HandleMsg::RemoveHook { + addr: contract1.clone(), + }; + let err = handle( + deps.as_mut(), + mock_env(), + user_info.clone(), + remove_msg.clone(), + ) + .unwrap_err(); + match err { + ContractError::Unauthorized {} => {} + e => panic!("Unexpected error: {}", e), + } + + // remove the original + let _ = handle( + deps.as_mut(), + mock_env(), + admin_info.clone(), + remove_msg.clone(), + ) + .unwrap(); + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract2.clone()]); + } + + #[test] + fn hooks_fire() { + let mut deps = mock_dependencies(&[]); + do_init(deps.as_mut()); + + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert!(hooks.hooks.is_empty()); + + let contract1 = HumanAddr::from("hook1"); + let contract2 = HumanAddr::from("hook2"); + + // register 2 hooks + let admin_info = mock_info(ADMIN, &[]); + let add_msg = HandleMsg::AddHook { + addr: contract1.clone(), + }; + let add_msg2 = HandleMsg::AddHook { + addr: contract2.clone(), + }; + for msg in vec![add_msg, add_msg2] { + let _ = handle(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 = HandleMsg::UpdateMembers { remove, add }; + + // admin updates properly + assert_users(&deps, Some(11), Some(6), None, None); + let res = handle(deps.as_mut(), mock_env(), admin_info.clone(), 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 = hook_msg.clone().into_cosmos_msg(contract1).unwrap(); + let msg2 = 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_init(deps.as_mut()); + + // get total from raw key + let total_raw = deps.storage.get(TOTAL_KEY).unwrap(); + let total: u64 = from_slice(&total_raw).unwrap(); + assert_eq!(17, total); + + // get member votes from raw key + let member2_canon = deps.api.canonical_address(&USER2.into()).unwrap(); + let member2_raw = deps.storage.get(&member_key(&member2_canon)).unwrap(); + let member2: u64 = from_slice(&member2_raw).unwrap(); + assert_eq!(6, member2); + + // and handle misses + let member3_canon = deps.api.canonical_address(&USER3.into()).unwrap(); + let member3_raw = deps.storage.get(&member_key(&member3_canon)); + assert_eq!(None, member3_raw); + } +} diff --git a/contracts/cw4-stake/src/error.rs b/contracts/cw4-stake/src/error.rs new file mode 100644 index 000000000..dc19f1033 --- /dev/null +++ b/contracts/cw4-stake/src/error.rs @@ -0,0 +1,11 @@ +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, +} diff --git a/contracts/cw4-stake/src/lib.rs b/contracts/cw4-stake/src/lib.rs new file mode 100644 index 000000000..0a2bfd93b --- /dev/null +++ b/contracts/cw4-stake/src/lib.rs @@ -0,0 +1,11 @@ +pub mod contract; +pub mod error; +pub mod msg; +pub mod state; + +// comment this out and use the lower form if the contract supports migrations +#[cfg(all(target_arch = "wasm32", not(feature = "library")))] +cosmwasm_std::create_entry_points!(contract); + +// #[cfg(all(target_arch = "wasm32", not(feature = "library")))] +// cosmwasm_std::create_entry_points_with_migration!(contract); diff --git a/contracts/cw4-stake/src/msg.rs b/contracts/cw4-stake/src/msg.rs new file mode 100644 index 000000000..cfbb52f38 --- /dev/null +++ b/contracts/cw4-stake/src/msg.rs @@ -0,0 +1,7 @@ +use cw4::{Cw4HandleMsg, Cw4InitMsg, Cw4QueryMsg}; + +pub type InitMsg = Cw4InitMsg; + +pub type HandleMsg = Cw4HandleMsg; + +pub type QueryMsg = Cw4QueryMsg; diff --git a/contracts/cw4-stake/src/state.rs b/contracts/cw4-stake/src/state.rs new file mode 100644 index 000000000..c2bcf66f1 --- /dev/null +++ b/contracts/cw4-stake/src/state.rs @@ -0,0 +1,14 @@ +use cosmwasm_std::{CanonicalAddr, HumanAddr}; +use cw4::TOTAL_KEY; +use cw_storage_plus::{snapshot_names, Item, SnapshotMap, SnapshotNamespaces, Strategy}; + +pub const ADMIN: Item> = Item::new(b"admin"); +pub const TOTAL: Item = Item::new(TOTAL_KEY); + +// Note: this must be same as cw4::MEMBERS_KEY but macro needs literal, not const +pub const MEMBERS: SnapshotMap<&[u8], u64> = + SnapshotMap::new(snapshot_names!("members"), Strategy::EveryBlock); + +// store all hook addresses in one item. We cannot have many of them before the contract +// becomes unusable +pub const HOOKS: Item> = Item::new(b"hooks"); diff --git a/scripts/publish.sh b/scripts/publish.sh index c411efe4c..1f723e4b2 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -7,8 +7,8 @@ BASE_PACKAGES="cw0 storage-plus" ALL_PACKAGES="cw1 cw2 cw3 cw4 cw20 cw721" # these are imported by other contracts -BASE_CONTRACTS="cw1-whitelist cw4-group cw20-base cw721-base" -ALL_CONTRACTS="cw1-subkeys cw3-fixed-multisig cw3-flex-multisig cw20-atomic-swap cw20-escrow cw20-staking" +BASE_CONTRACTS="cw1-whitelist cw20-base cw721-base" +ALL_CONTRACTS="cw1-subkeys cw3-fixed-multisig cw3-flex-multisig cw4-group cw4-stake cw20-atomic-swap cw20-escrow cw20-staking" SLEEP_TIME=30 From 4ac284f88606ee2e7e138ef8b20f8bedf564406c Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 10 Dec 2020 23:19:20 +0100 Subject: [PATCH 02/22] Pull out hooks from cw4-group into cw0 --- Cargo.lock | 1 + contracts/cw4-group/src/contract.rs | 51 +++++++++++++---------------- contracts/cw4-group/src/error.rs | 6 ---- contracts/cw4-group/src/state.rs | 7 +--- packages/cw0/Cargo.toml | 1 + packages/cw0/src/hooks.rs | 43 ++++++++++++++++++++++++ packages/cw0/src/lib.rs | 1 + scripts/publish.sh | 4 ++- 8 files changed, 73 insertions(+), 41 deletions(-) create mode 100644 packages/cw0/src/hooks.rs diff --git a/Cargo.lock b/Cargo.lock index 64893f06f..9984b228b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,7 @@ name = "cw0" version = "0.3.2" dependencies = [ "cosmwasm-std", + "cw-storage-plus", "schemars", "serde", ] diff --git a/contracts/cw4-group/src/contract.rs b/contracts/cw4-group/src/contract.rs index a6b28a654..7e8f09785 100644 --- a/contracts/cw4-group/src/contract.rs +++ b/contracts/cw4-group/src/contract.rs @@ -1,8 +1,11 @@ use cosmwasm_std::{ - to_binary, Api, Binary, CanonicalAddr, Context, Deps, DepsMut, Env, HandleResponse, HumanAddr, + to_binary, Api, Binary, CanonicalAddr, Deps, DepsMut, Env, HandleResponse, HumanAddr, InitResponse, MessageInfo, Order, StdResult, }; -use cw0::maybe_canonical; +use cw0::{ + hooks::{add_hook, prepare_hooks, remove_hook, HOOKS}, + maybe_canonical, +}; use cw2::set_contract_version; use cw4::{ AdminResponse, HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, @@ -12,7 +15,7 @@ use cw_storage_plus::Bound; use crate::error::ContractError; use crate::msg::{HandleMsg, InitMsg, QueryMsg}; -use crate::state::{ADMIN, HOOKS, MEMBERS, TOTAL}; +use crate::state::{ADMIN, MEMBERS, TOTAL}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw4-group"; @@ -98,12 +101,12 @@ pub fn handle_update_members( // make the local update let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; // call all registered hooks - let mut ctx = Context::new(); - for h in HOOKS.may_load(deps.storage)?.unwrap_or_default() { - let msg = diff.clone().into_cosmos_msg(h)?; - ctx.add_message(msg); - } - Ok(ctx.into()) + let messages = prepare_hooks(deps.storage, |h| diff.clone().into_cosmos_msg(h))?; + Ok(HandleResponse { + messages, + attributes: vec![], + data: None, + }) } // the logic from handle_update_admin extracted for easier import @@ -169,14 +172,7 @@ pub fn handle_add_hook( ) -> Result { let admin = ADMIN.load(deps.storage)?; assert_admin(deps.api, info.sender, admin)?; - - let mut hooks = HOOKS.may_load(deps.storage)?.unwrap_or_default(); - if !hooks.iter().any(|h| h == &addr) { - hooks.push(addr); - } else { - return Err(ContractError::HookAlreadyRegistered {}); - } - HOOKS.save(deps.storage, &hooks)?; + add_hook(deps.storage, addr)?; Ok(HandleResponse::default()) } @@ -187,14 +183,7 @@ pub fn handle_remove_hook( ) -> Result { let admin = ADMIN.load(deps.storage)?; assert_admin(deps.api, info.sender, admin)?; - - let mut hooks = HOOKS.load(deps.storage)?; - if let Some(p) = hooks.iter().position(|x| x == &addr) { - hooks.remove(p); - } else { - return Err(ContractError::HookNotRegistered {}); - } - HOOKS.save(deps.storage, &hooks)?; + remove_hook(deps.storage, addr)?; Ok(HandleResponse::default()) } @@ -271,7 +260,8 @@ fn list_members( mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{from_slice, OwnedDeps, Querier, Storage}; + use cosmwasm_std::{from_slice, OwnedDeps, Querier, StdError, Storage}; + use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; use cw4::{member_key, TOTAL_KEY}; const ADMIN: &str = "juan"; @@ -533,8 +523,11 @@ mod tests { remove_msg.clone(), ) .unwrap_err(); + match err { - ContractError::HookNotRegistered {} => {} + ContractError::Std(StdError::GenericErr { msg, .. }) => { + assert_eq!(msg, HOOK_NOT_REGISTERED) + } e => panic!("Unexpected error: {}", e), } @@ -555,7 +548,9 @@ mod tests { ) .unwrap_err(); match err { - ContractError::HookAlreadyRegistered {} => {} + ContractError::Std(StdError::GenericErr { msg, .. }) => { + assert_eq!(msg, HOOK_ALREADY_REGISTERED) + } e => panic!("Unexpected error: {}", e), } diff --git a/contracts/cw4-group/src/error.rs b/contracts/cw4-group/src/error.rs index d894dd319..dc19f1033 100644 --- a/contracts/cw4-group/src/error.rs +++ b/contracts/cw4-group/src/error.rs @@ -8,10 +8,4 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, - - #[error("Given address already registered as a hook")] - HookAlreadyRegistered {}, - - #[error("Given address not registered as a hook")] - HookNotRegistered {}, } diff --git a/contracts/cw4-group/src/state.rs b/contracts/cw4-group/src/state.rs index 6cdb0bcbd..011d1bb3c 100644 --- a/contracts/cw4-group/src/state.rs +++ b/contracts/cw4-group/src/state.rs @@ -1,18 +1,13 @@ -use cosmwasm_std::{CanonicalAddr, HumanAddr}; +use cosmwasm_std::CanonicalAddr; use cw4::TOTAL_KEY; use cw_storage_plus::{Item, SnapshotMap, Strategy}; pub const ADMIN: Item> = Item::new("admin"); pub const TOTAL: Item = Item::new(TOTAL_KEY); -// Note: this must be same as cw4::MEMBERS_KEY but macro needs literal, not const pub const MEMBERS: SnapshotMap<&[u8], u64> = SnapshotMap::new( cw4::MEMBERS_KEY, cw4::MEMBERS_CHECKPOINTS, cw4::MEMBERS_CHANGELOG, Strategy::EveryBlock, ); - -// store all hook addresses in one item. We cannot have many of them before the contract -// becomes unusable -pub const HOOKS: Item> = Item::new("hooks"); diff --git a/packages/cw0/Cargo.toml b/packages/cw0/Cargo.toml index 3fd31b751..119631e14 100644 --- a/packages/cw0/Cargo.toml +++ b/packages/cw0/Cargo.toml @@ -13,5 +13,6 @@ documentation = "https://docs.cosmwasm.com" [dependencies] cosmwasm-std = { version = "0.12.0" } +cw-storage-plus = { path = "../storage-plus", version = "0.3.2", features = ["iterator"] } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/packages/cw0/src/hooks.rs b/packages/cw0/src/hooks.rs new file mode 100644 index 000000000..440e67885 --- /dev/null +++ b/packages/cw0/src/hooks.rs @@ -0,0 +1,43 @@ +use cosmwasm_std::{CosmosMsg, HumanAddr, StdError, StdResult, Storage}; + +use cw_storage_plus::Item; + +// store all hook addresses in one item. We cannot have many of them before the contract becomes unusable anyway. +pub const HOOKS: Item> = Item::new("hooks"); + +// Returning a custom error here would be a headache - especially for the importing contract. +// rather I just define the error messages here and use StdError::GenericErr +pub const HOOK_ALREADY_REGISTERED: &str = "Given address already registered as a hook"; +pub const HOOK_NOT_REGISTERED: &str = "Given address not registered as a hook"; + +pub fn add_hook(storage: &mut dyn Storage, addr: HumanAddr) -> StdResult<()> { + let mut hooks = HOOKS.may_load(storage)?.unwrap_or_default(); + if !hooks.iter().any(|h| h == &addr) { + hooks.push(addr); + } else { + return Err(StdError::generic_err(HOOK_ALREADY_REGISTERED)); + } + HOOKS.save(storage, &hooks) +} + +pub fn remove_hook(storage: &mut dyn Storage, addr: HumanAddr) -> StdResult<()> { + let mut hooks = HOOKS.load(storage)?; + if let Some(p) = hooks.iter().position(|x| x == &addr) { + hooks.remove(p); + } else { + return Err(StdError::generic_err(HOOK_NOT_REGISTERED)); + } + HOOKS.save(storage, &hooks) +} + +pub fn prepare_hooks StdResult>( + storage: &dyn Storage, + prep: F, +) -> StdResult> { + HOOKS + .may_load(storage)? + .unwrap_or_default() + .into_iter() + .map(prep) + .collect() +} diff --git a/packages/cw0/src/lib.rs b/packages/cw0/src/lib.rs index 586ecea7e..3752e4b1b 100644 --- a/packages/cw0/src/lib.rs +++ b/packages/cw0/src/lib.rs @@ -1,5 +1,6 @@ mod balance; mod expiration; +pub mod hooks; mod pagination; pub use crate::balance::NativeBalance; diff --git a/scripts/publish.sh b/scripts/publish.sh index 1f723e4b2..21dc1e117 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -3,7 +3,7 @@ set -o errexit -o nounset -o pipefail command -v shellcheck > /dev/null && shellcheck "$0" # these are imported by other packages -BASE_PACKAGES="cw0 storage-plus" +BASE_PACKAGES="storage-plus cw0" ALL_PACKAGES="cw1 cw2 cw3 cw4 cw20 cw721" # these are imported by other contracts @@ -17,6 +17,8 @@ for pack in $BASE_PACKAGES; do cd "packages/$pack" echo "Publishing $pack" cargo publish + # there are some dependencies between these + sleep $SLEEP_TIME ) done From cd7026831ff9f9e0dc20c928e0cdceab28bd4b6c Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 10 Dec 2020 23:52:17 +0100 Subject: [PATCH 03/22] Separate out cw4 and cw4-group msg apis --- contracts/cw3-flex-multisig/src/contract.rs | 9 ++- contracts/cw4-group/examples/schema.rs | 4 +- contracts/cw4-group/src/contract.rs | 6 +- contracts/cw4-group/src/helpers.rs | 88 +++++++++++++++++++++ contracts/cw4-group/src/lib.rs | 4 +- contracts/cw4-group/src/msg.rs | 58 +++++++++++++- packages/cw4/examples/schema.rs | 6 +- packages/cw4/schema/admin_response.json | 22 ------ packages/cw4/schema/cw4_handle_msg.json | 72 ----------------- packages/cw4/schema/cw4_init_msg.json | 50 ------------ packages/cw4/schema/cw4_query_msg.json | 12 --- packages/cw4/src/helpers.rs | 32 ++------ packages/cw4/src/lib.rs | 4 +- packages/cw4/src/msg.rs | 26 ------ packages/cw4/src/query.rs | 18 ++--- 15 files changed, 173 insertions(+), 238 deletions(-) create mode 100644 contracts/cw4-group/src/helpers.rs delete mode 100644 packages/cw4/schema/admin_response.json delete mode 100644 packages/cw4/schema/cw4_init_msg.json diff --git a/contracts/cw3-flex-multisig/src/contract.rs b/contracts/cw3-flex-multisig/src/contract.rs index 26f01c855..40401724e 100644 --- a/contracts/cw3-flex-multisig/src/contract.rs +++ b/contracts/cw3-flex-multisig/src/contract.rs @@ -457,7 +457,8 @@ mod tests { use cw0::Duration; use cw2::{query_contract_info, ContractVersion}; - use cw4::{Cw4HandleMsg, Member}; + use cw4::Member; + use cw4_group::helpers::Cw4GroupContract; use cw_multi_test::{next_block, App, Contract, ContractWrapper, SimpleBank}; use super::*; @@ -559,7 +560,7 @@ mod tests { // 3. (Optional) Set the multisig as the group owner if multisig_as_group_admin { - let update_admin = Cw4HandleMsg::UpdateAdmin { + let update_admin = cw4_group::msg::HandleMsg::UpdateAdmin { admin: Some(flex_addr.clone()), }; app.execute_contract(OWNER, &group_addr, &update_admin, &[]) @@ -1056,7 +1057,7 @@ mod tests { // adds NEWBIE with 2 power -> with snapshot, invalid vote // removes VOTER3 -> with snapshot, can vote and pass proposal let newbie: &str = "newbie"; - let update_msg = Cw4HandleMsg::UpdateMembers { + let update_msg = cw4_group::msg::HandleMsg::UpdateMembers { remove: vec![VOTER3.into()], add: vec![member(VOTER2, 7), member(newbie, 2)], }; @@ -1135,7 +1136,7 @@ mod tests { ); // Start a proposal to remove VOTER3 from the set - let update_msg = Cw4Contract(group_addr.clone()) + let update_msg = Cw4GroupContract::new(group_addr.clone()) .update_members(vec![VOTER3.into()], vec![]) .unwrap(); let update_proposal = HandleMsg::Propose { diff --git a/contracts/cw4-group/examples/schema.rs b/contracts/cw4-group/examples/schema.rs index a5043e280..390f22dbf 100644 --- a/contracts/cw4-group/examples/schema.rs +++ b/contracts/cw4-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::{HandleMsg, InitMsg, QueryMsg}; +pub use cw4::{MemberListResponse, MemberResponse, TotalWeightResponse}; +pub use cw4_group::msg::{AdminResponse, HandleMsg, InitMsg, QueryMsg}; fn main() { let mut out_dir = current_dir().unwrap(); diff --git a/contracts/cw4-group/src/contract.rs b/contracts/cw4-group/src/contract.rs index 7e8f09785..f9c24e8c2 100644 --- a/contracts/cw4-group/src/contract.rs +++ b/contracts/cw4-group/src/contract.rs @@ -8,13 +8,13 @@ use cw0::{ }; use cw2::set_contract_version; use cw4::{ - AdminResponse, HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, - MemberResponse, TotalWeightResponse, + HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, + TotalWeightResponse, }; use cw_storage_plus::Bound; use crate::error::ContractError; -use crate::msg::{HandleMsg, InitMsg, QueryMsg}; +use crate::msg::{AdminResponse, HandleMsg, InitMsg, QueryMsg}; use crate::state::{ADMIN, MEMBERS, TOTAL}; // version info for migration info diff --git a/contracts/cw4-group/src/helpers.rs b/contracts/cw4-group/src/helpers.rs new file mode 100644 index 000000000..3f9c99cd0 --- /dev/null +++ b/contracts/cw4-group/src/helpers.rs @@ -0,0 +1,88 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; + +use cosmwasm_std::{ + to_binary, Api, CanonicalAddr, CosmosMsg, Empty, HumanAddr, QuerierWrapper, QueryRequest, + StdResult, WasmMsg, WasmQuery, +}; +use cw4::{Cw4Contract, Member}; + +use crate::msg::{AdminResponse, HandleMsg, QueryMsg}; + +/// Cw4GroupContract is a wrapper around HumanAddr 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: HumanAddr) -> Self { + Cw4GroupContract(Cw4Contract(addr)) + } + + /// Convert this address to a form fit for storage + pub fn canonical(&self, api: &dyn Api) -> StdResult { + let canon = api.canonical_address(&self.addr())?; + Ok(Cw4GroupCanonicalContract(canon)) + } + + fn encode_msg(&self, msg: HandleMsg) -> StdResult { + Ok(WasmMsg::Execute { + contract_addr: self.addr(), + msg: to_binary(&msg)?, + send: vec![], + } + .into()) + } + + pub fn update_admin>( + &self, + admin: Option, + ) -> StdResult { + let msg = HandleMsg::UpdateAdmin { admin }; + self.encode_msg(msg) + } + + pub fn update_members(&self, remove: Vec, add: Vec) -> StdResult { + let msg = HandleMsg::UpdateMembers { remove, add }; + self.encode_msg(msg) + } + + fn encode_smart_query(&self, msg: QueryMsg) -> StdResult> { + Ok(WasmQuery::Smart { + contract_addr: self.addr(), + msg: to_binary(&msg)?, + } + .into()) + } + + /// Read the admin + pub fn admin(&self, querier: &QuerierWrapper) -> StdResult> { + let query = self.encode_smart_query(QueryMsg::Admin {})?; + let res: AdminResponse = querier.query(&query)?; + Ok(res.admin) + } +} + +/// This is a representation of Cw4GroupContract for storage. +/// Don't use it directly, just translate to the Cw4GroupContract when needed. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Cw4GroupCanonicalContract(pub CanonicalAddr); + +impl Cw4GroupCanonicalContract { + /// Convert this address to a form fit for usage in messages and queries + pub fn human(&self, api: &dyn Api) -> StdResult { + let human = api.human_address(&self.0)?; + Ok(Cw4GroupContract::new(human)) + } +} diff --git a/contracts/cw4-group/src/lib.rs b/contracts/cw4-group/src/lib.rs index 0a2bfd93b..663735331 100644 --- a/contracts/cw4-group/src/lib.rs +++ b/contracts/cw4-group/src/lib.rs @@ -1,11 +1,9 @@ pub mod contract; pub mod error; +pub mod helpers; pub mod msg; pub mod state; // comment this out and use the lower form if the contract supports migrations #[cfg(all(target_arch = "wasm32", not(feature = "library")))] cosmwasm_std::create_entry_points!(contract); - -// #[cfg(all(target_arch = "wasm32", not(feature = "library")))] -// cosmwasm_std::create_entry_points_with_migration!(contract); diff --git a/contracts/cw4-group/src/msg.rs b/contracts/cw4-group/src/msg.rs index cfbb52f38..3b879d03b 100644 --- a/contracts/cw4-group/src/msg.rs +++ b/contracts/cw4-group/src/msg.rs @@ -1,7 +1,57 @@ -use cw4::{Cw4HandleMsg, Cw4InitMsg, Cw4QueryMsg}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; -pub type InitMsg = Cw4InitMsg; +use cosmwasm_std::HumanAddr; +use cw4::Member; -pub type HandleMsg = Cw4HandleMsg; +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub struct InitMsg { + /// 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, +} -pub type QueryMsg = Cw4QueryMsg; +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + /// 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: HumanAddr }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: HumanAddr }, +} + +#[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: HumanAddr, + at_height: Option, + }, + /// Shows all registered hooks. Returns HooksResponse. + Hooks {}, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct AdminResponse { + pub admin: Option, +} diff --git a/packages/cw4/examples/schema.rs b/packages/cw4/examples/schema.rs index b6078bbbe..31c7b0327 100644 --- a/packages/cw4/examples/schema.rs +++ b/packages/cw4/examples/schema.rs @@ -4,8 +4,8 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; pub use cw4::{ - AdminResponse, Cw4HandleMsg, Cw4InitMsg, Cw4QueryMsg, Member, MemberChangedHookMsg, - MemberListResponse, MemberResponse, TotalWeightResponse, + Cw4HandleMsg, Cw4QueryMsg, Member, MemberChangedHookMsg, MemberListResponse, MemberResponse, + TotalWeightResponse, }; fn main() { @@ -15,9 +15,7 @@ fn main() { remove_schemas(&out_dir).unwrap(); export_schema(&schema_for!(Cw4HandleMsg), &out_dir); - export_schema(&schema_for!(Cw4InitMsg), &out_dir); export_schema(&schema_for!(Cw4QueryMsg), &out_dir); - 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/packages/cw4/schema/admin_response.json b/packages/cw4/schema/admin_response.json deleted file mode 100644 index 71edb382e..000000000 --- a/packages/cw4/schema/admin_response.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AdminResponse", - "type": "object", - "properties": { - "admin": { - "anyOf": [ - { - "$ref": "#/definitions/HumanAddr" - }, - { - "type": "null" - } - ] - } - }, - "definitions": { - "HumanAddr": { - "type": "string" - } - } -} diff --git a/packages/cw4/schema/cw4_handle_msg.json b/packages/cw4/schema/cw4_handle_msg.json index ec3256791..dbfbab29d 100644 --- a/packages/cw4/schema/cw4_handle_msg.json +++ b/packages/cw4/schema/cw4_handle_msg.json @@ -2,60 +2,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Cw4HandleMsg", "anyOf": [ - { - "description": "Change the admin", - "type": "object", - "required": [ - "update_admin" - ], - "properties": { - "update_admin": { - "type": "object", - "properties": { - "admin": { - "anyOf": [ - { - "$ref": "#/definitions/HumanAddr" - }, - { - "type": "null" - } - ] - } - } - } - } - }, - { - "description": "apply a diff to the existing members. remove is applied after add, so if an address is in both, it is removed", - "type": "object", - "required": [ - "update_members" - ], - "properties": { - "update_members": { - "type": "object", - "required": [ - "add", - "remove" - ], - "properties": { - "add": { - "type": "array", - "items": { - "$ref": "#/definitions/Member" - } - }, - "remove": { - "type": "array", - "items": { - "$ref": "#/definitions/HumanAddr" - } - } - } - } - } - }, { "description": "Add a new hook to be informed of all membership changes. Must be called by Admin", "type": "object", @@ -100,24 +46,6 @@ "definitions": { "HumanAddr": { "type": "string" - }, - "Member": { - "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", - "type": "object", - "required": [ - "addr", - "weight" - ], - "properties": { - "addr": { - "$ref": "#/definitions/HumanAddr" - }, - "weight": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } } } } diff --git a/packages/cw4/schema/cw4_init_msg.json b/packages/cw4/schema/cw4_init_msg.json deleted file mode 100644 index 1c037b932..000000000 --- a/packages/cw4/schema/cw4_init_msg.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Cw4InitMsg", - "type": "object", - "required": [ - "members" - ], - "properties": { - "admin": { - "description": "The admin is the only account that can update the group state. Omit it to make the group immutable.", - "anyOf": [ - { - "$ref": "#/definitions/HumanAddr" - }, - { - "type": "null" - } - ] - }, - "members": { - "type": "array", - "items": { - "$ref": "#/definitions/Member" - } - } - }, - "definitions": { - "HumanAddr": { - "type": "string" - }, - "Member": { - "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", - "type": "object", - "required": [ - "addr", - "weight" - ], - "properties": { - "addr": { - "$ref": "#/definitions/HumanAddr" - }, - "weight": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } - } - } -} diff --git a/packages/cw4/schema/cw4_query_msg.json b/packages/cw4/schema/cw4_query_msg.json index 7b1b3abc6..3e5999563 100644 --- a/packages/cw4/schema/cw4_query_msg.json +++ b/packages/cw4/schema/cw4_query_msg.json @@ -2,18 +2,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Cw4QueryMsg", "anyOf": [ - { - "description": "Return AdminResponse", - "type": "object", - "required": [ - "admin" - ], - "properties": { - "admin": { - "type": "object" - } - } - }, { "description": "Return TotalWeightResponse", "type": "object", diff --git a/packages/cw4/src/helpers.rs b/packages/cw4/src/helpers.rs index 612708def..3932f5cfb 100644 --- a/packages/cw4/src/helpers.rs +++ b/packages/cw4/src/helpers.rs @@ -8,9 +8,7 @@ use cosmwasm_std::{ use crate::msg::Cw4HandleMsg; use crate::query::HooksResponse; -use crate::{ - member_key, AdminResponse, Cw4QueryMsg, Member, MemberListResponse, MemberResponse, TOTAL_KEY, -}; +use crate::{member_key, Cw4QueryMsg, Member, MemberListResponse, MemberResponse, TOTAL_KEY}; /// Cw4Contract is a wrapper around HumanAddr that provides a lot of helpers /// for working with cw4 contracts @@ -20,12 +18,16 @@ use crate::{ pub struct Cw4Contract(pub HumanAddr); impl Cw4Contract { + pub fn new(addr: HumanAddr) -> Self { + Cw4Contract(addr) + } + pub fn addr(&self) -> HumanAddr { self.0.clone() } /// Convert this address to a form fit for storage - pub fn canonical(&self, api: &A) -> StdResult { + pub fn canonical(&self, api: &dyn Api) -> StdResult { let canon = api.canonical_address(&self.0)?; Ok(Cw4CanonicalContract(canon)) } @@ -39,19 +41,6 @@ impl Cw4Contract { .into()) } - pub fn update_admin>( - &self, - admin: Option, - ) -> StdResult { - let msg = Cw4HandleMsg::UpdateAdmin { admin }; - self.encode_msg(msg) - } - - pub fn update_members(&self, remove: Vec, add: Vec) -> StdResult { - let msg = Cw4HandleMsg::UpdateMembers { remove, add }; - self.encode_msg(msg) - } - pub fn add_hook(&self, addr: HumanAddr) -> StdResult { let msg = Cw4HandleMsg::AddHook { addr }; self.encode_msg(msg) @@ -78,13 +67,6 @@ impl Cw4Contract { .into()) } - /// Read the admin - pub fn admin(&self, querier: &QuerierWrapper) -> StdResult> { - let query = self.encode_smart_query(Cw4QueryMsg::Admin {})?; - let res: AdminResponse = querier.query(&query)?; - Ok(res.admin) - } - /// Show the hooks pub fn hooks(&self, querier: &QuerierWrapper) -> StdResult> { let query = self.encode_smart_query(Cw4QueryMsg::Hooks {})?; @@ -164,7 +146,7 @@ pub struct Cw4CanonicalContract(pub CanonicalAddr); impl Cw4CanonicalContract { /// Convert this address to a form fit for usage in messages and queries - pub fn human(&self, api: &A) -> StdResult { + pub fn human(&self, api: &dyn Api) -> StdResult { let human = api.human_address(&self.0)?; Ok(Cw4Contract(human)) } diff --git a/packages/cw4/src/lib.rs b/packages/cw4/src/lib.rs index 747cc5041..5666ac384 100644 --- a/packages/cw4/src/lib.rs +++ b/packages/cw4/src/lib.rs @@ -5,9 +5,9 @@ mod query; pub use crate::helpers::{Cw4CanonicalContract, Cw4Contract}; pub use crate::hook::{MemberChangedHookMsg, MemberDiff}; -pub use crate::msg::{Cw4HandleMsg, Cw4InitMsg, Member}; +pub use crate::msg::Cw4HandleMsg; pub use crate::query::{ - member_key, AdminResponse, Cw4QueryMsg, HooksResponse, MemberListResponse, MemberResponse, + member_key, Cw4QueryMsg, HooksResponse, Member, MemberListResponse, MemberResponse, TotalWeightResponse, MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, TOTAL_KEY, }; diff --git a/packages/cw4/src/msg.rs b/packages/cw4/src/msg.rs index 5b86572de..27a002c32 100644 --- a/packages/cw4/src/msg.rs +++ b/packages/cw4/src/msg.rs @@ -3,35 +3,9 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::HumanAddr; -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -pub struct Cw4InitMsg { - /// 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, -} - -/// A group member has a weight associated with them. -/// This may all be equal, or may have meaning in the app that -/// makes use of the group (eg. voting power) -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub struct Member { - pub addr: HumanAddr, - pub weight: u64, -} - #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum Cw4HandleMsg { - /// 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: HumanAddr }, /// Remove a hook. Must be called by Admin diff --git a/packages/cw4/src/query.rs b/packages/cw4/src/query.rs index 6a788cd9d..04a9bbd94 100644 --- a/packages/cw4/src/query.rs +++ b/packages/cw4/src/query.rs @@ -3,13 +3,9 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::HumanAddr; -use crate::msg::Member; - #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum Cw4QueryMsg { - /// Return AdminResponse - Admin {}, /// Return TotalWeightResponse TotalWeight {}, /// Returns MembersListResponse @@ -26,6 +22,15 @@ pub enum Cw4QueryMsg { Hooks {}, } +/// A group member has a weight associated with them. +/// This may all be equal, or may have meaning in the app that +/// makes use of the group (eg. voting power) +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct Member { + pub addr: HumanAddr, + pub weight: u64, +} + #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct MemberListResponse { pub members: Vec, @@ -36,11 +41,6 @@ pub struct MemberResponse { pub weight: Option, } -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub struct AdminResponse { - pub admin: Option, -} - #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct TotalWeightResponse { pub weight: u64, From c931e1c92f94c5729a451800f884cef879de6531 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 10 Dec 2020 23:57:18 +0100 Subject: [PATCH 04/22] Update README files --- contracts/cw4-group/README.md | 18 ++++++++++++++++-- packages/cw4/README.md | 9 --------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/contracts/cw4-group/README.md b/contracts/cw4-group/README.md index d8fc361c6..e43f5e44b 100644 --- a/contracts/cw4-group/README.md +++ b/contracts/cw4-group/README.md @@ -39,5 +39,19 @@ decision-making. ## Messages -Update messages and queries are defined by the -[cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. \ No newline at end of file +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 two messages: + +`UpdateAdmin{admin}` - changes (or clears) the admin for the contract + +`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 occurance will be used. + +as well as one query: + +`Admin{}` - Returns the `admin` address, or `None` if unset. + diff --git a/packages/cw4/README.md b/packages/cw4/README.md index 722708b56..64464cb59 100644 --- a/packages/cw4/README.md +++ b/packages/cw4/README.md @@ -25,13 +25,6 @@ configure in the `init` phase. There are four messages supported by a group contract: -`UpdateAdmin{admin}` - changes (or clears) the admin for the contract - -`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 occurance will be used. - `AddHook{addr}` - adds a contract address to be called upon every `UpdateMembers` call. This can only be called by the admin, and care must be taken. A contract returning an error or running out of gas will @@ -51,8 +44,6 @@ problem, but we cover how to instantiate that in ### Smart -`Admin{}` - Returns the `admin` address, or `None` if unset. - `TotalWeight{}` - Returns the total weight of all current members, this is very useful if some conditions are defined on a "percentage of members". From f2a9f730026d006a1a76e93ebc4e22efc60585b8 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 00:17:52 +0100 Subject: [PATCH 05/22] Clean up cw4/cw4-group/cw4-stake messages --- contracts/cw4-group/examples/schema.rs | 4 +- contracts/cw4-group/src/contract.rs | 6 +- contracts/cw4-group/src/helpers.rs | 30 +-------- contracts/cw4-group/src/msg.rs | 28 +-------- contracts/cw4-stake/README.md | 19 +++--- contracts/cw4-stake/src/msg.rs | 84 +++++++++++++++++++++++-- packages/cw4/examples/schema.rs | 5 +- packages/cw4/schema/admin_response.json | 22 +++++++ packages/cw4/schema/cw4_handle_msg.json | 24 +++++++ packages/cw4/schema/cw4_query_msg.json | 12 ++++ packages/cw4/src/helpers.rs | 19 +++++- packages/cw4/src/lib.rs | 5 +- packages/cw4/src/msg.rs | 2 + packages/cw4/src/query.rs | 7 +++ 14 files changed, 191 insertions(+), 76 deletions(-) create mode 100644 packages/cw4/schema/admin_response.json diff --git a/contracts/cw4-group/examples/schema.rs b/contracts/cw4-group/examples/schema.rs index 390f22dbf..a5043e280 100644 --- a/contracts/cw4-group/examples/schema.rs +++ b/contracts/cw4-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::{MemberListResponse, MemberResponse, TotalWeightResponse}; -pub use cw4_group::msg::{AdminResponse, HandleMsg, InitMsg, QueryMsg}; +pub use cw4::{AdminResponse, MemberListResponse, MemberResponse, TotalWeightResponse}; +pub use cw4_group::msg::{HandleMsg, InitMsg, QueryMsg}; fn main() { let mut out_dir = current_dir().unwrap(); diff --git a/contracts/cw4-group/src/contract.rs b/contracts/cw4-group/src/contract.rs index f9c24e8c2..7e8f09785 100644 --- a/contracts/cw4-group/src/contract.rs +++ b/contracts/cw4-group/src/contract.rs @@ -8,13 +8,13 @@ use cw0::{ }; use cw2::set_contract_version; use cw4::{ - HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, MemberResponse, - TotalWeightResponse, + AdminResponse, HooksResponse, Member, MemberChangedHookMsg, MemberDiff, MemberListResponse, + MemberResponse, TotalWeightResponse, }; use cw_storage_plus::Bound; use crate::error::ContractError; -use crate::msg::{AdminResponse, HandleMsg, InitMsg, QueryMsg}; +use crate::msg::{HandleMsg, InitMsg, QueryMsg}; use crate::state::{ADMIN, MEMBERS, TOTAL}; // version info for migration info diff --git a/contracts/cw4-group/src/helpers.rs b/contracts/cw4-group/src/helpers.rs index 3f9c99cd0..d8f4812fb 100644 --- a/contracts/cw4-group/src/helpers.rs +++ b/contracts/cw4-group/src/helpers.rs @@ -2,13 +2,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ops::Deref; -use cosmwasm_std::{ - to_binary, Api, CanonicalAddr, CosmosMsg, Empty, HumanAddr, QuerierWrapper, QueryRequest, - StdResult, WasmMsg, WasmQuery, -}; +use cosmwasm_std::{to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, StdResult, WasmMsg}; use cw4::{Cw4Contract, Member}; -use crate::msg::{AdminResponse, HandleMsg, QueryMsg}; +use crate::msg::HandleMsg; /// Cw4GroupContract is a wrapper around HumanAddr that provides a lot of helpers /// for working with cw4-group contracts. @@ -45,33 +42,10 @@ impl Cw4GroupContract { .into()) } - pub fn update_admin>( - &self, - admin: Option, - ) -> StdResult { - let msg = HandleMsg::UpdateAdmin { admin }; - self.encode_msg(msg) - } - pub fn update_members(&self, remove: Vec, add: Vec) -> StdResult { let msg = HandleMsg::UpdateMembers { remove, add }; self.encode_msg(msg) } - - fn encode_smart_query(&self, msg: QueryMsg) -> StdResult> { - Ok(WasmQuery::Smart { - contract_addr: self.addr(), - msg: to_binary(&msg)?, - } - .into()) - } - - /// Read the admin - pub fn admin(&self, querier: &QuerierWrapper) -> StdResult> { - let query = self.encode_smart_query(QueryMsg::Admin {})?; - let res: AdminResponse = querier.query(&query)?; - Ok(res.admin) - } } /// This is a representation of Cw4GroupContract for storage. diff --git a/contracts/cw4-group/src/msg.rs b/contracts/cw4-group/src/msg.rs index 3b879d03b..b29bd867b 100644 --- a/contracts/cw4-group/src/msg.rs +++ b/contracts/cw4-group/src/msg.rs @@ -2,7 +2,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::HumanAddr; -use cw4::Member; +use cw4::{Cw4QueryMsg, Member}; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] @@ -30,28 +30,4 @@ pub enum HandleMsg { RemoveHook { addr: HumanAddr }, } -#[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: HumanAddr, - at_height: Option, - }, - /// Shows all registered hooks. Returns HooksResponse. - Hooks {}, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -pub struct AdminResponse { - pub admin: Option, -} +pub type QueryMsg = Cw4QueryMsg; diff --git a/contracts/cw4-stake/README.md b/contracts/cw4-stake/README.md index 524f489a1..a1e57198e 100644 --- a/contracts/cw4-stake/README.md +++ b/contracts/cw4-stake/README.md @@ -30,13 +30,11 @@ To create it, you must pass in a list of members, as well as an optional ```rust pub struct InitMsg { - pub admin: Option, - pub members: Vec, -} - -pub struct Member { - pub addr: HumanAddr, - pub weight: u64, + /// denom of the token to stake + pub stake: String, + pub tokens_per_weight: u64, + pub min_bond: Uint128, + pub unbonding_period: Duration, } ``` @@ -49,7 +47,12 @@ it does define this address is part of the group. This could be used in eg. a KYC whitelist to say they are allowed, but cannot participate in decision-making. +If `min_bond` is higher than `tokens_per_weight`, you cannot have anyone with 0 weight. + ## Messages Update messages and queries are defined by the -[cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. \ No newline at end of file +[cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. + +The following have been added: + diff --git a/contracts/cw4-stake/src/msg.rs b/contracts/cw4-stake/src/msg.rs index cfbb52f38..44de01b29 100644 --- a/contracts/cw4-stake/src/msg.rs +++ b/contracts/cw4-stake/src/msg.rs @@ -1,7 +1,83 @@ -use cw4::{Cw4HandleMsg, Cw4InitMsg, Cw4QueryMsg}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; -pub type InitMsg = Cw4InitMsg; +use cosmwasm_std::{Coin, HumanAddr, Uint128}; +use cw0::{Duration, Expiration}; -pub type HandleMsg = Cw4HandleMsg; +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub struct InitMsg { + /// denom of the token to stake + pub stake: String, + pub tokens_per_weight: u64, + pub min_bond: Uint128, + pub unbonding_period: Duration, -pub type QueryMsg = Cw4QueryMsg; + // admin can only add/remove hooks, not change other parameters + pub admin: Option, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum HandleMsg { + /// Bond will bond all staking tokens sent with the message and update membership weight + Bond {}, + /// Unbond will start the unbonding process for the given number of tokens. + /// The sender immediately looses weight from these tokens, and can claim them + /// back to his wallet after `unbonding_period` + Unbond { amount: Uint128 }, + /// Claim is used to claim your native tokens that you previously "unbonded" + /// after the contract-defined waiting period (eg. 1 week) + Claim {}, + + /// Change the admin + UpdateAdmin { admin: Option }, + /// Add a new hook to be informed of all membership changes. Must be called by Admin + AddHook { addr: HumanAddr }, + /// Remove a hook. Must be called by Admin + RemoveHook { addr: HumanAddr }, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + /// Claims shows the tokens in process of unbonding for this address + Claims { + address: HumanAddr, + }, + // Show the number of tokens currently staked by this address. + Staked { + address: HumanAddr, + }, + + /// Return TotalWeightResponse + TotalWeight {}, + /// Returns MembersListResponse + ListMembers { + start_after: Option, + limit: Option, + }, + /// Returns MemberResponse + Member { + addr: HumanAddr, + at_height: Option, + }, + /// Shows all registered hooks. Returns HooksResponse. + Hooks {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ClaimsResponse { + pub claims: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Claim { + pub amount: Uint128, + pub released: Expiration, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct StakedResponse { + pub stake: Coin, +} diff --git a/packages/cw4/examples/schema.rs b/packages/cw4/examples/schema.rs index 31c7b0327..294528a22 100644 --- a/packages/cw4/examples/schema.rs +++ b/packages/cw4/examples/schema.rs @@ -4,8 +4,8 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; pub use cw4::{ - Cw4HandleMsg, Cw4QueryMsg, Member, MemberChangedHookMsg, MemberListResponse, MemberResponse, - TotalWeightResponse, + AdminResponse, Cw4HandleMsg, Cw4QueryMsg, Member, MemberChangedHookMsg, MemberListResponse, + MemberResponse, TotalWeightResponse, }; fn main() { @@ -16,6 +16,7 @@ fn main() { export_schema(&schema_for!(Cw4HandleMsg), &out_dir); export_schema(&schema_for!(Cw4QueryMsg), &out_dir); + 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/packages/cw4/schema/admin_response.json b/packages/cw4/schema/admin_response.json new file mode 100644 index 000000000..71edb382e --- /dev/null +++ b/packages/cw4/schema/admin_response.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "type": "object", + "properties": { + "admin": { + "anyOf": [ + { + "$ref": "#/definitions/HumanAddr" + }, + { + "type": "null" + } + ] + } + }, + "definitions": { + "HumanAddr": { + "type": "string" + } + } +} diff --git a/packages/cw4/schema/cw4_handle_msg.json b/packages/cw4/schema/cw4_handle_msg.json index dbfbab29d..cb7b81c47 100644 --- a/packages/cw4/schema/cw4_handle_msg.json +++ b/packages/cw4/schema/cw4_handle_msg.json @@ -2,6 +2,30 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Cw4HandleMsg", "anyOf": [ + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { + "type": "object", + "properties": { + "admin": { + "anyOf": [ + { + "$ref": "#/definitions/HumanAddr" + }, + { + "type": "null" + } + ] + } + } + } + } + }, { "description": "Add a new hook to be informed of all membership changes. Must be called by Admin", "type": "object", diff --git a/packages/cw4/schema/cw4_query_msg.json b/packages/cw4/schema/cw4_query_msg.json index 3e5999563..7b1b3abc6 100644 --- a/packages/cw4/schema/cw4_query_msg.json +++ b/packages/cw4/schema/cw4_query_msg.json @@ -2,6 +2,18 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "Cw4QueryMsg", "anyOf": [ + { + "description": "Return AdminResponse", + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object" + } + } + }, { "description": "Return TotalWeightResponse", "type": "object", diff --git a/packages/cw4/src/helpers.rs b/packages/cw4/src/helpers.rs index 3932f5cfb..77e90e297 100644 --- a/packages/cw4/src/helpers.rs +++ b/packages/cw4/src/helpers.rs @@ -8,7 +8,9 @@ use cosmwasm_std::{ use crate::msg::Cw4HandleMsg; use crate::query::HooksResponse; -use crate::{member_key, Cw4QueryMsg, Member, MemberListResponse, MemberResponse, TOTAL_KEY}; +use crate::{ + member_key, AdminResponse, Cw4QueryMsg, Member, MemberListResponse, MemberResponse, TOTAL_KEY, +}; /// Cw4Contract is a wrapper around HumanAddr that provides a lot of helpers /// for working with cw4 contracts @@ -51,6 +53,14 @@ impl Cw4Contract { self.encode_msg(msg) } + pub fn update_admin>( + &self, + admin: Option, + ) -> StdResult { + let msg = Cw4HandleMsg::UpdateAdmin { admin }; + self.encode_msg(msg) + } + fn encode_smart_query(&self, msg: Cw4QueryMsg) -> StdResult> { Ok(WasmQuery::Smart { contract_addr: self.addr(), @@ -137,6 +147,13 @@ impl Cw4Contract { let res: MemberListResponse = querier.query(&query)?; Ok(res.members) } + + /// Read the admin + pub fn admin(&self, querier: &QuerierWrapper) -> StdResult> { + let query = self.encode_smart_query(Cw4QueryMsg::Admin {})?; + let res: AdminResponse = querier.query(&query)?; + Ok(res.admin) + } } /// This is a respresentation of Cw4Contract for storage. diff --git a/packages/cw4/src/lib.rs b/packages/cw4/src/lib.rs index 5666ac384..b71280c9a 100644 --- a/packages/cw4/src/lib.rs +++ b/packages/cw4/src/lib.rs @@ -7,8 +7,9 @@ pub use crate::helpers::{Cw4CanonicalContract, Cw4Contract}; pub use crate::hook::{MemberChangedHookMsg, MemberDiff}; pub use crate::msg::Cw4HandleMsg; pub use crate::query::{ - member_key, Cw4QueryMsg, HooksResponse, Member, MemberListResponse, MemberResponse, - TotalWeightResponse, MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, TOTAL_KEY, + member_key, AdminResponse, Cw4QueryMsg, HooksResponse, Member, MemberListResponse, + MemberResponse, TotalWeightResponse, MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, + TOTAL_KEY, }; #[cfg(test)] diff --git a/packages/cw4/src/msg.rs b/packages/cw4/src/msg.rs index 27a002c32..39692d029 100644 --- a/packages/cw4/src/msg.rs +++ b/packages/cw4/src/msg.rs @@ -6,6 +6,8 @@ use cosmwasm_std::HumanAddr; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum Cw4HandleMsg { + /// Change the admin + UpdateAdmin { admin: Option }, /// Add a new hook to be informed of all membership changes. Must be called by Admin AddHook { addr: HumanAddr }, /// Remove a hook. Must be called by Admin diff --git a/packages/cw4/src/query.rs b/packages/cw4/src/query.rs index 04a9bbd94..200338f21 100644 --- a/packages/cw4/src/query.rs +++ b/packages/cw4/src/query.rs @@ -6,6 +6,8 @@ use cosmwasm_std::HumanAddr; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum Cw4QueryMsg { + /// Return AdminResponse + Admin {}, /// Return TotalWeightResponse TotalWeight {}, /// Returns MembersListResponse @@ -22,6 +24,11 @@ pub enum Cw4QueryMsg { Hooks {}, } +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct AdminResponse { + pub admin: Option, +} + /// A group member has a weight associated with them. /// This may all be equal, or may have meaning in the app that /// makes use of the group (eg. voting power) From 77a74767ee0e42d3346ab14abd9bfcd693128eab Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 00:46:38 +0100 Subject: [PATCH 06/22] Extract claim logic from cw20-staking to reuse with cw4-stake --- contracts/cw20-staking/src/contract.rs | 47 ++++++------------- contracts/cw20-staking/src/msg.rs | 4 +- contracts/cw20-staking/src/state.rs | 24 +--------- packages/cw0/src/claim.rs | 63 ++++++++++++++++++++++++++ packages/cw0/src/lib.rs | 1 + 5 files changed, 81 insertions(+), 58 deletions(-) create mode 100644 packages/cw0/src/claim.rs diff --git a/contracts/cw20-staking/src/contract.rs b/contracts/cw20-staking/src/contract.rs index cd609ce8a..6ee517e99 100644 --- a/contracts/cw20-staking/src/contract.rs +++ b/contracts/cw20-staking/src/contract.rs @@ -2,6 +2,8 @@ use cosmwasm_std::{ attr, coin, to_binary, BankMsg, Binary, Decimal, Deps, DepsMut, Env, HandleResponse, HumanAddr, InitResponse, MessageInfo, QuerierWrapper, StakingMsg, StdError, StdResult, Uint128, WasmMsg, }; + +use cw0::claim::{claim_tokens, create_claim, CLAIMS}; use cw2::set_contract_version; use cw20_base::allowances::{ handle_burn_from, handle_decrease_allowance, handle_increase_allowance, handle_send_from, @@ -15,8 +17,7 @@ use cw20_base::state::{token_info, MinterData, TokenInfo}; use crate::error::ContractError; use crate::msg::{ClaimsResponse, HandleMsg, InitMsg, InvestmentResponse, QueryMsg}; use crate::state::{ - claims, claims_read, invest_info, invest_info_read, total_supply, total_supply_read, Claim, - InvestmentInfo, Supply, + invest_info, invest_info_read, total_supply, total_supply_read, InvestmentInfo, Supply, }; const FALLBACK_RATIO: Decimal = Decimal::one(); @@ -278,15 +279,12 @@ pub fn unbond( supply.claims += unbond; totals.save(&supply)?; - // add a claim to this user to get their tokens after the unbonding period - claims(deps.storage).update(sender_raw.as_slice(), |old| -> Result<_, ContractError> { - let mut claims = old.unwrap_or_default(); - claims.push(Claim { - amount: unbond, - released: invest.unbonding_period.after(&env.block), - }); - Ok(claims) - })?; + create_claim( + deps.storage, + &sender_raw, + unbond, + invest.unbonding_period.after(&env.block), + )?; // unbond them let res = HandleResponse { @@ -317,26 +315,9 @@ pub fn claim(deps: DepsMut, env: Env, info: MessageInfo) -> Result Result<_, ContractError> { - let (_send, waiting): (Vec<_>, _) = - claim.unwrap_or_default().iter().cloned().partition(|c| { - // if mature and we can pay, then include in _send - if c.released.is_expired(&env.block) && to_send + c.amount <= cap { - to_send += c.amount; - true - } else { - // not to send, leave in waiting and save again - false - } - }); - Ok(waiting) - })?; - + let to_send = claim_tokens(deps.storage, &sender_raw, &env.block, Some(balance.amount))?; if to_send == Uint128(0) { return Err(ContractError::NothingToClaim {}); } @@ -459,8 +440,8 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { pub fn query_claims(deps: Deps, address: HumanAddr) -> StdResult { let address_raw = deps.api.canonical_address(&address)?; - let claims = claims_read(deps.storage) - .may_load(address_raw.as_slice())? + let claims = CLAIMS + .may_load(deps.storage, &address_raw)? .unwrap_or_default(); Ok(ClaimsResponse { claims }) } @@ -492,7 +473,7 @@ mod tests { mock_dependencies, mock_env, mock_info, MockQuerier, MOCK_CONTRACT_ADDR, }; use cosmwasm_std::{coins, Coin, CosmosMsg, Decimal, FullDelegation, Validator}; - use cw0::{Duration, DAY, HOUR, WEEK}; + use cw0::{claim::Claim, Duration, DAY, HOUR, WEEK}; use std::str::FromStr; fn sample_validator>(addr: U) -> Validator { diff --git a/contracts/cw20-staking/src/msg.rs b/contracts/cw20-staking/src/msg.rs index a93e458a7..48a2c5d51 100644 --- a/contracts/cw20-staking/src/msg.rs +++ b/contracts/cw20-staking/src/msg.rs @@ -2,11 +2,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{Binary, Coin, Decimal, HumanAddr, Uint128}; -use cw0::Duration; +use cw0::{claim::Claim, Duration}; use cw20::Expiration; -use crate::state::Claim; - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InitMsg { /// name of the derivative token diff --git a/contracts/cw20-staking/src/state.rs b/contracts/cw20-staking/src/state.rs index 9c6d2378b..66b00cc06 100644 --- a/contracts/cw20-staking/src/state.rs +++ b/contracts/cw20-staking/src/state.rs @@ -2,32 +2,12 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{CanonicalAddr, Decimal, HumanAddr, Storage, Uint128}; -use cosmwasm_storage::{ - bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, - Singleton, -}; -use cw0::{Duration, Expiration}; +use cosmwasm_storage::{singleton, singleton_read, ReadonlySingleton, Singleton}; +use cw0::Duration; pub const KEY_INVESTMENT: &[u8] = b"invest"; pub const KEY_TOTAL_SUPPLY: &[u8] = b"total_supply"; -pub const PREFIX_CLAIMS: &[u8] = b"claim"; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Claim { - pub amount: Uint128, - pub released: Expiration, -} - -/// claims are the claims to money being unbonded, index by claimer address -pub fn claims(storage: &mut dyn Storage) -> Bucket> { - bucket(storage, PREFIX_CLAIMS) -} - -pub fn claims_read(storage: &dyn Storage) -> ReadonlyBucket> { - bucket_read(storage, PREFIX_CLAIMS) -} - /// Investment info is fixed at initialization, and is used to control the function of the contract #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct InvestmentInfo { diff --git a/packages/cw0/src/claim.rs b/packages/cw0/src/claim.rs new file mode 100644 index 000000000..73678a385 --- /dev/null +++ b/packages/cw0/src/claim.rs @@ -0,0 +1,63 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::Expiration; +use cosmwasm_std::{BlockInfo, CanonicalAddr, StdResult, Storage, Uint128}; +use cw_storage_plus::Map; + +// TODO: revisit design (multiple keys?) +pub const CLAIMS: Map<&[u8], Vec> = Map::new(b"claim"); + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct Claim { + pub amount: Uint128, + pub released: Expiration, +} + +/// this creates a claim, such that the given address an claim an amount of tokens after the release date +pub fn create_claim( + storage: &mut dyn Storage, + addr: &CanonicalAddr, + amount: Uint128, + released: Expiration, +) -> StdResult<()> { + // add a claim to this user to get their tokens after the unbonding period + CLAIMS.update(storage, &addr, |old| -> StdResult<_> { + let mut claims = old.unwrap_or_default(); + claims.push(Claim { amount, released }); + Ok(claims) + })?; + Ok(()) +} + +/// This iterates over all mature claims for the address, and removes them, up to an optional cap. +/// it removes the finished claims and returns the total amount of tokens to be released. +pub fn claim_tokens( + storage: &mut dyn Storage, + addr: &CanonicalAddr, + block: &BlockInfo, + cap: Option, +) -> StdResult { + let mut to_send = Uint128(0); + CLAIMS.update(storage, &addr, |claim| -> StdResult<_> { + let (_send, waiting): (Vec<_>, _) = + claim.unwrap_or_default().iter().cloned().partition(|c| { + // if mature and we can pay fully, then include in _send + if c.released.is_expired(block) { + if let Some(limit) = cap { + if to_send + c.amount > limit { + return false; + } + } + // TODO: handle partial paying claims? + to_send += c.amount; + true + } else { + // not to send, leave in waiting and save again + false + } + }); + Ok(waiting) + })?; + Ok(to_send) +} diff --git a/packages/cw0/src/lib.rs b/packages/cw0/src/lib.rs index 3752e4b1b..30f50b614 100644 --- a/packages/cw0/src/lib.rs +++ b/packages/cw0/src/lib.rs @@ -1,4 +1,5 @@ mod balance; +pub mod claim; mod expiration; pub mod hooks; mod pagination; From c8440af5a6a20cda9d8cf7ed03905e3f38cff932 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 01:20:41 +0100 Subject: [PATCH 07/22] Define cw4-stake message, code builds with some unimplemented --- contracts/cw4-stake/examples/schema.rs | 4 +- .../cw4-stake/schema/claims_response.json | 81 ++++++++++ contracts/cw4-stake/schema/handle_msg.json | 93 ++++++----- contracts/cw4-stake/schema/init_msg.json | 75 ++++++--- contracts/cw4-stake/schema/query_msg.json | 39 +++++ .../cw4-stake/schema/staked_response.json | 33 ++++ contracts/cw4-stake/src/contract.rs | 146 +++++++++++++----- contracts/cw4-stake/src/error.rs | 3 + contracts/cw4-stake/src/msg.rs | 12 +- contracts/cw4-stake/src/state.rs | 22 ++- 10 files changed, 386 insertions(+), 122 deletions(-) create mode 100644 contracts/cw4-stake/schema/claims_response.json create mode 100644 contracts/cw4-stake/schema/staked_response.json diff --git a/contracts/cw4-stake/examples/schema.rs b/contracts/cw4-stake/examples/schema.rs index 42277092e..03bedb9c7 100644 --- a/contracts/cw4-stake/examples/schema.rs +++ b/contracts/cw4-stake/examples/schema.rs @@ -4,7 +4,7 @@ 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_stake::msg::{HandleMsg, InitMsg, QueryMsg}; +pub use cw4_stake::msg::{ClaimsResponse, HandleMsg, InitMsg, QueryMsg, StakedResponse}; fn main() { let mut out_dir = current_dir().unwrap(); @@ -19,4 +19,6 @@ fn main() { 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!(ClaimsResponse), &out_dir); + export_schema(&schema_for!(StakedResponse), &out_dir); } diff --git a/contracts/cw4-stake/schema/claims_response.json b/contracts/cw4-stake/schema/claims_response.json new file mode 100644 index 000000000..494fb0c3e --- /dev/null +++ b/contracts/cw4-stake/schema/claims_response.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ClaimsResponse", + "type": "object", + "required": [ + "claims" + ], + "properties": { + "claims": { + "type": "array", + "items": { + "$ref": "#/definitions/Claim" + } + } + }, + "definitions": { + "Claim": { + "type": "object", + "required": [ + "amount", + "released" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "released": { + "$ref": "#/definitions/Expiration" + } + } + }, + "Expiration": { + "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)", + "anyOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + } + } + ] + }, + "Uint128": { + "type": "string" + } + } +} diff --git a/contracts/cw4-stake/schema/handle_msg.json b/contracts/cw4-stake/schema/handle_msg.json index bb66c7771..10ccabe35 100644 --- a/contracts/cw4-stake/schema/handle_msg.json +++ b/contracts/cw4-stake/schema/handle_msg.json @@ -3,54 +3,68 @@ "title": "HandleMsg", "anyOf": [ { - "description": "Change the admin", + "description": "Bond will bond all staking tokens sent with the message and update membership weight", "type": "object", "required": [ - "update_admin" + "bond" ], "properties": { - "update_admin": { + "bond": { + "type": "object" + } + } + }, + { + "description": "Unbond will start the unbonding process for the given number of tokens. The sender immediately looses weight from these tokens, and can claim them back to his wallet after `unbonding_period`", + "type": "object", + "required": [ + "unbond" + ], + "properties": { + "unbond": { "type": "object", + "required": [ + "amount" + ], "properties": { - "admin": { - "anyOf": [ - { - "$ref": "#/definitions/HumanAddr" - }, - { - "type": "null" - } - ] + "amount": { + "$ref": "#/definitions/Uint128" } } } } }, { - "description": "apply a diff to the existing members. remove is applied after add, so if an address is in both, it is removed", + "description": "Claim is used to claim your native tokens that you previously \"unbonded\" after the contract-defined waiting period (eg. 1 week)", "type": "object", "required": [ - "update_members" + "claim" ], "properties": { - "update_members": { + "claim": { + "type": "object" + } + } + }, + { + "description": "Change the admin", + "type": "object", + "required": [ + "update_admin" + ], + "properties": { + "update_admin": { "type": "object", - "required": [ - "add", - "remove" - ], "properties": { - "add": { - "type": "array", - "items": { - "$ref": "#/definitions/Member" - } - }, - "remove": { - "type": "array", - "items": { - "$ref": "#/definitions/HumanAddr" - } + "admin": { + "anyOf": [ + { + "$ref": "#/definitions/HumanAddr" + }, + { + "type": "null" + } + ] } } } @@ -101,23 +115,8 @@ "HumanAddr": { "type": "string" }, - "Member": { - "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", - "type": "object", - "required": [ - "addr", - "weight" - ], - "properties": { - "addr": { - "$ref": "#/definitions/HumanAddr" - }, - "weight": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } + "Uint128": { + "type": "string" } } } diff --git a/contracts/cw4-stake/schema/init_msg.json b/contracts/cw4-stake/schema/init_msg.json index f52208750..4de53155e 100644 --- a/contracts/cw4-stake/schema/init_msg.json +++ b/contracts/cw4-stake/schema/init_msg.json @@ -3,11 +3,13 @@ "title": "InitMsg", "type": "object", "required": [ - "members" + "min_bond", + "stake", + "tokens_per_weight", + "unbonding_period" ], "properties": { "admin": { - "description": "The admin is the only account that can update the group state. Omit it to make the group immutable.", "anyOf": [ { "$ref": "#/definitions/HumanAddr" @@ -17,34 +19,59 @@ } ] }, - "members": { - "type": "array", - "items": { - "$ref": "#/definitions/Member" - } + "min_bond": { + "$ref": "#/definitions/Uint128" + }, + "stake": { + "description": "denom of the token to stake", + "type": "string" + }, + "tokens_per_weight": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "unbonding_period": { + "$ref": "#/definitions/Duration" } }, "definitions": { + "Duration": { + "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined", + "anyOf": [ + { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "type": "object", + "required": [ + "time" + ], + "properties": { + "time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + } + ] + }, "HumanAddr": { "type": "string" }, - "Member": { - "description": "A group member has a weight associated with them. This may all be equal, or may have meaning in the app that makes use of the group (eg. voting power)", - "type": "object", - "required": [ - "addr", - "weight" - ], - "properties": { - "addr": { - "$ref": "#/definitions/HumanAddr" - }, - "weight": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 - } - } + "Uint128": { + "type": "string" } } } diff --git a/contracts/cw4-stake/schema/query_msg.json b/contracts/cw4-stake/schema/query_msg.json index f8461d9a3..1a607ec4b 100644 --- a/contracts/cw4-stake/schema/query_msg.json +++ b/contracts/cw4-stake/schema/query_msg.json @@ -2,6 +2,45 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", "anyOf": [ + { + "description": "Claims shows the tokens in process of unbonding for this address", + "type": "object", + "required": [ + "claims" + ], + "properties": { + "claims": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + }, + { + "type": "object", + "required": [ + "staked" + ], + "properties": { + "staked": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "$ref": "#/definitions/HumanAddr" + } + } + } + } + }, { "description": "Return AdminResponse", "type": "object", diff --git a/contracts/cw4-stake/schema/staked_response.json b/contracts/cw4-stake/schema/staked_response.json new file mode 100644 index 000000000..0496f8f69 --- /dev/null +++ b/contracts/cw4-stake/schema/staked_response.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StakedResponse", + "type": "object", + "required": [ + "stake" + ], + "properties": { + "stake": { + "$ref": "#/definitions/Coin" + } + }, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "type": "string" + } + } +} diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index d19686863..5b60d4f0d 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -1,9 +1,9 @@ use cosmwasm_std::{ - to_binary, Api, Binary, CanonicalAddr, Deps, DepsMut, Env, HandleResponse, HumanAddr, - InitResponse, MessageInfo, Order, StdResult, + coin, coins, to_binary, Api, BankMsg, Binary, CanonicalAddr, Deps, DepsMut, Env, + HandleResponse, HumanAddr, InitResponse, MessageInfo, Order, StdResult, Uint128, }; use cw0::{ - hooks::{add_hook, prepare_hooks, remove_hook, HOOKS}, + hooks::{add_hook, remove_hook, HOOKS}, maybe_canonical, }; use cw2::set_contract_version; @@ -14,8 +14,9 @@ use cw4::{ use cw_storage_plus::Bound; use crate::error::ContractError; -use crate::msg::{HandleMsg, InitMsg, QueryMsg}; -use crate::state::{ADMIN, MEMBERS, TOTAL}; +use crate::msg::{ClaimsResponse, HandleMsg, InitMsg, QueryMsg, StakedResponse}; +use crate::state::{Config, ADMIN, CONFIG, MEMBERS, STAKE, TOTAL}; +use cw0::claim::{claim_tokens, create_claim, CLAIMS}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw4-group"; @@ -23,32 +24,22 @@ 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 -pub fn init(deps: DepsMut, env: Env, _info: MessageInfo, msg: InitMsg) -> StdResult { +pub fn init(deps: DepsMut, _env: Env, _info: MessageInfo, msg: InitMsg) -> StdResult { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - create(deps, msg.admin, msg.members, env.block.height)?; - Ok(InitResponse::default()) -} -// create is the init logic with set_contract_version removed so it can more -// easily be imported in other contracts -pub fn create( - deps: DepsMut, - admin: Option, - members: Vec, - height: u64, -) -> StdResult<()> { - let admin_raw = maybe_canonical(deps.api, admin)?; + let admin_raw = maybe_canonical(deps.api, msg.admin)?; ADMIN.save(deps.storage, &admin_raw)?; - let mut total = 0u64; - for member in members.into_iter() { - total += member.weight; - let raw = deps.api.canonical_address(&member.addr)?; - MEMBERS.save(deps.storage, &raw, &member.weight, height)?; - } - TOTAL.save(deps.storage, &total)?; + let config = Config { + denom: msg.stake, + tokens_per_weight: msg.tokens_per_weight, + min_bond: msg.min_bond, + unbonding_period: msg.unbonding_period, + }; + CONFIG.save(deps.storage, &config)?; + TOTAL.save(deps.storage, &0)?; - Ok(()) + Ok(InitResponse::default()) } // And declare a custom Error variant for the ones where you will want to make use of it @@ -60,11 +51,11 @@ pub fn handle( ) -> Result { match msg { HandleMsg::UpdateAdmin { admin } => handle_update_admin(deps, info, admin), - HandleMsg::UpdateMembers { add, remove } => { - handle_update_members(deps, env, info, add, remove) - } HandleMsg::AddHook { addr } => handle_add_hook(deps, info, addr), HandleMsg::RemoveHook { addr } => handle_remove_hook(deps, info, addr), + HandleMsg::Bond {} => handle_bond(deps, info), + HandleMsg::Unbond { amount } => handle_unbond(deps, env, info, amount), + HandleMsg::Claim {} => handle_claim(deps, env, info), } } @@ -91,17 +82,77 @@ pub fn update_admin( }) } -pub fn handle_update_members( - mut deps: DepsMut, +pub fn handle_bond(_deps: DepsMut, _info: MessageInfo) -> Result { + unimplemented!(); + // TODO: ensure the denom was proper + + // // make the local update + // let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; + // // call all registered hooks + // let messages = prepare_hooks(deps.storage, |h| diff.clone().into_cosmos_msg(h))?; + // Ok(HandleResponse { + // messages, + // attributes: vec![], + // data: None, + // }) +} + +pub fn handle_unbond( + deps: DepsMut, + env: Env, + info: MessageInfo, + amount: Uint128, +) -> Result { + // reduce the sender's stake - aborting if insufficient + let sender_raw = deps.api.canonical_address(&info.sender)?; + let _new_stake = STAKE.update(deps.storage, &sender_raw, |stake| -> StdResult<_> { + stake.unwrap_or_default() - amount + })?; + + // provide them a claim + let config = CONFIG.load(deps.storage)?; + create_claim( + deps.storage, + &sender_raw, + amount, + config.unbonding_period.after(&env.block), + )?; + + // update their membership + unimplemented!(); + + // // make the local update + // let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; + // // call all registered hooks + // let messages = prepare_hooks(deps.storage, |h| diff.clone().into_cosmos_msg(h))?; + // Ok(HandleResponse { + // messages, + // attributes: vec![], + // data: None, + // }) +} + +pub fn handle_claim( + deps: DepsMut, env: Env, info: MessageInfo, - add: Vec, - remove: Vec, ) -> Result { - // make the local update - let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; - // call all registered hooks - let messages = prepare_hooks(deps.storage, |h| diff.clone().into_cosmos_msg(h))?; + let sender_raw = deps.api.canonical_address(&info.sender)?; + let release = claim_tokens(deps.storage, &sender_raw, &env.block, None)?; + if release == Uint128(0) { + return Err(ContractError::NothingToClaim {}); + } + + let config = CONFIG.load(deps.storage)?; + let amount = coins(release.u128(), config.denom); + + let messages = vec![BankMsg::Send { + from_address: env.contract.address, + to_address: info.sender, + amount, + } + .into()]; + Ok(HandleResponse { messages, attributes: vec![], @@ -199,6 +250,8 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::Admin {} => to_binary(&query_admin(deps)?), QueryMsg::TotalWeight {} => to_binary(&query_total_weight(deps)?), QueryMsg::Hooks {} => to_binary(&query_hooks(deps)?), + QueryMsg::Claims { address } => to_binary(&query_claims(deps, address)?), + QueryMsg::Staked { address } => to_binary(&query_staked(deps, address)?), } } @@ -218,6 +271,25 @@ fn query_total_weight(deps: Deps) -> StdResult { Ok(TotalWeightResponse { weight }) } +pub fn query_claims(deps: Deps, address: HumanAddr) -> StdResult { + let address_raw = deps.api.canonical_address(&address)?; + let claims = CLAIMS + .may_load(deps.storage, &address_raw)? + .unwrap_or_default(); + Ok(ClaimsResponse { claims }) +} + +pub fn query_staked(deps: Deps, address: HumanAddr) -> StdResult { + let address_raw = deps.api.canonical_address(&address)?; + let stake = STAKE + .may_load(deps.storage, &address_raw)? + .unwrap_or_default(); + let denom = CONFIG.load(deps.storage)?.denom; + Ok(StakedResponse { + stake: coin(stake.u128(), denom), + }) +} + fn query_member(deps: Deps, addr: HumanAddr, height: Option) -> StdResult { let raw = deps.api.canonical_address(&addr)?; let weight = match height { diff --git a/contracts/cw4-stake/src/error.rs b/contracts/cw4-stake/src/error.rs index dc19f1033..adc1f3822 100644 --- a/contracts/cw4-stake/src/error.rs +++ b/contracts/cw4-stake/src/error.rs @@ -8,4 +8,7 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, + + #[error("No claims that can be released currently")] + NothingToClaim {}, } diff --git a/contracts/cw4-stake/src/msg.rs b/contracts/cw4-stake/src/msg.rs index 44de01b29..47adaa066 100644 --- a/contracts/cw4-stake/src/msg.rs +++ b/contracts/cw4-stake/src/msg.rs @@ -2,10 +2,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{Coin, HumanAddr, Uint128}; -use cw0::{Duration, Expiration}; +use cw0::claim::Claim; +use cw0::Duration; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] pub struct InitMsg { /// denom of the token to stake pub stake: String, @@ -50,6 +50,8 @@ pub enum QueryMsg { address: HumanAddr, }, + /// Return AdminResponse + Admin {}, /// Return TotalWeightResponse TotalWeight {}, /// Returns MembersListResponse @@ -71,12 +73,6 @@ pub struct ClaimsResponse { pub claims: Vec, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Claim { - pub amount: Uint128, - pub released: Expiration, -} - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct StakedResponse { pub stake: Coin, diff --git a/contracts/cw4-stake/src/state.rs b/contracts/cw4-stake/src/state.rs index c2bcf66f1..590f7f78c 100644 --- a/contracts/cw4-stake/src/state.rs +++ b/contracts/cw4-stake/src/state.rs @@ -1,14 +1,26 @@ -use cosmwasm_std::{CanonicalAddr, HumanAddr}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use cosmwasm_std::{CanonicalAddr, Uint128}; +use cw0::Duration; use cw4::TOTAL_KEY; -use cw_storage_plus::{snapshot_names, Item, SnapshotMap, SnapshotNamespaces, Strategy}; +use cw_storage_plus::{snapshot_names, Item, Map, SnapshotMap, SnapshotNamespaces, Strategy}; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct Config { + /// denom of the token to stake + pub denom: String, + pub tokens_per_weight: u64, + pub min_bond: Uint128, + pub unbonding_period: Duration, +} pub const ADMIN: Item> = Item::new(b"admin"); +pub const CONFIG: Item = Item::new(b"config"); pub const TOTAL: Item = Item::new(TOTAL_KEY); // Note: this must be same as cw4::MEMBERS_KEY but macro needs literal, not const pub const MEMBERS: SnapshotMap<&[u8], u64> = SnapshotMap::new(snapshot_names!("members"), Strategy::EveryBlock); -// store all hook addresses in one item. We cannot have many of them before the contract -// becomes unusable -pub const HOOKS: Item> = Item::new(b"hooks"); +pub const STAKE: Map<&[u8], Uint128> = Map::new(b"stake"); From 9492372003e129e52073aa177f178408c1aeeb5f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 12:39:10 +0100 Subject: [PATCH 08/22] Implement bond and unbond properly --- contracts/cw4-stake/src/contract.rs | 202 ++++++++++++++++------------ contracts/cw4-stake/src/error.rs | 6 + contracts/cw4-stake/src/msg.rs | 2 +- contracts/cw4-stake/src/state.rs | 2 +- packages/cw4/src/hook.rs | 23 +++- 5 files changed, 138 insertions(+), 97 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 5b60d4f0d..ed85898d6 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ - coin, coins, to_binary, Api, BankMsg, Binary, CanonicalAddr, Deps, DepsMut, Env, - HandleResponse, HumanAddr, InitResponse, MessageInfo, Order, StdResult, Uint128, + coin, coins, to_binary, Api, BankMsg, Binary, CanonicalAddr, CosmosMsg, Deps, DepsMut, Env, + HandleResponse, HumanAddr, InitResponse, MessageInfo, Order, StdResult, Storage, Uint128, }; use cw0::{ hooks::{add_hook, remove_hook, HOOKS}, @@ -17,6 +17,7 @@ use crate::error::ContractError; use crate::msg::{ClaimsResponse, HandleMsg, InitMsg, QueryMsg, StakedResponse}; use crate::state::{Config, ADMIN, CONFIG, MEMBERS, STAKE, TOTAL}; use cw0::claim::{claim_tokens, create_claim, CLAIMS}; +use cw0::hooks::prepare_hooks; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw4-group"; @@ -30,10 +31,16 @@ pub fn init(deps: DepsMut, _env: Env, _info: MessageInfo, msg: InitMsg) -> StdRe let admin_raw = maybe_canonical(deps.api, msg.admin)?; ADMIN.save(deps.storage, &admin_raw)?; + // min_bond is at least 1, so 0 stake -> non-membership + let min_bond = match msg.min_bond { + Uint128(0) => Uint128(1), + v => v, + }; + let config = Config { denom: msg.stake, tokens_per_weight: msg.tokens_per_weight, - min_bond: msg.min_bond, + min_bond, unbonding_period: msg.unbonding_period, }; CONFIG.save(deps.storage, &config)?; @@ -53,7 +60,7 @@ pub fn handle( HandleMsg::UpdateAdmin { admin } => handle_update_admin(deps, info, admin), HandleMsg::AddHook { addr } => handle_add_hook(deps, info, addr), HandleMsg::RemoveHook { addr } => handle_remove_hook(deps, info, addr), - HandleMsg::Bond {} => handle_bond(deps, info), + HandleMsg::Bond {} => handle_bond(deps, env, info), HandleMsg::Unbond { amount } => handle_unbond(deps, env, info, amount), HandleMsg::Claim {} => handle_claim(deps, env, info), } @@ -64,37 +71,57 @@ pub fn handle_update_admin( info: MessageInfo, new_admin: Option, ) -> Result { - update_admin(deps, info.sender, new_admin)?; - Ok(HandleResponse::default()) -} - -// the logic from handle_update_admin extracted for easier import -pub fn update_admin( - deps: DepsMut, - sender: HumanAddr, - new_admin: Option, -) -> Result, ContractError> { let api = deps.api; ADMIN.update(deps.storage, |state| -> Result<_, ContractError> { - assert_admin(api, sender, state)?; + assert_admin(api, &info.sender, state)?; let new_admin = maybe_canonical(api, new_admin)?; Ok(new_admin) - }) + })?; + Ok(HandleResponse::default()) } -pub fn handle_bond(_deps: DepsMut, _info: MessageInfo) -> Result { - unimplemented!(); - // TODO: ensure the denom was proper - - // // make the local update - // let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; - // // call all registered hooks - // let messages = prepare_hooks(deps.storage, |h| diff.clone().into_cosmos_msg(h))?; - // Ok(HandleResponse { - // messages, - // attributes: vec![], - // data: None, - // }) +pub fn handle_bond( + deps: DepsMut, + env: Env, + info: MessageInfo, +) -> Result { + let cfg = CONFIG.load(deps.storage)?; + + // ensure the sent denom was proper + // NOTE: those clones are not needed (if we move denom, we return early), + // but the compiler cannot see that + let sent = match info.sent_funds.len() { + 0 => Err(ContractError::MissingDenom(cfg.denom.clone())), + 1 => { + if info.sent_funds[0].denom == cfg.denom { + Ok(info.sent_funds[0].amount) + } else { + Err(ContractError::ExtraDenoms(cfg.denom.clone())) + } + } + _ => Err(ContractError::ExtraDenoms(cfg.denom.clone())), + }?; + + // update the sender's stake + let sender_raw = deps.api.canonical_address(&info.sender)?; + let new_stake = STAKE.update(deps.storage, &sender_raw, |stake| -> StdResult<_> { + Ok(stake.unwrap_or_default() + sent) + })?; + + let messages = update_membership( + deps.storage, + info.sender, + &sender_raw, + new_stake, + &cfg, + env.block.height, + )?; + + Ok(HandleResponse { + messages, + attributes: vec![], + data: None, + }) } pub fn handle_unbond( @@ -105,31 +132,70 @@ pub fn handle_unbond( ) -> Result { // reduce the sender's stake - aborting if insufficient let sender_raw = deps.api.canonical_address(&info.sender)?; - let _new_stake = STAKE.update(deps.storage, &sender_raw, |stake| -> StdResult<_> { + let new_stake = STAKE.update(deps.storage, &sender_raw, |stake| -> StdResult<_> { stake.unwrap_or_default() - amount })?; // provide them a claim - let config = CONFIG.load(deps.storage)?; + let cfg = CONFIG.load(deps.storage)?; create_claim( deps.storage, &sender_raw, amount, - config.unbonding_period.after(&env.block), + cfg.unbonding_period.after(&env.block), + )?; + + let messages = update_membership( + deps.storage, + info.sender, + &sender_raw, + new_stake, + &cfg, + env.block.height, )?; - // update their membership - unimplemented!(); - - // // make the local update - // let diff = update_members(deps.branch(), env.block.height, info.sender, add, remove)?; - // // call all registered hooks - // let messages = prepare_hooks(deps.storage, |h| diff.clone().into_cosmos_msg(h))?; - // Ok(HandleResponse { - // messages, - // attributes: vec![], - // data: None, - // }) + Ok(HandleResponse { + messages, + attributes: vec![], + data: None, + }) +} + +fn update_membership( + storage: &mut dyn Storage, + sender: HumanAddr, + sender_raw: &CanonicalAddr, + new_stake: Uint128, + cfg: &Config, + height: u64, +) -> StdResult> { + // update their membership weight + let new = calc_weight(new_stake, cfg); + let old = MEMBERS.may_load(storage, sender_raw)?; + match new.as_ref() { + Some(w) => MEMBERS.save(storage, sender_raw, w, height), + None => MEMBERS.remove(storage, sender_raw, height), + }?; + + // update total + TOTAL.update(storage, |total| -> StdResult<_> { + Ok(total + new.unwrap_or_default() - old.unwrap_or_default()) + })?; + + // alert the hooks + let diff = MemberDiff::new(sender, old, new); + prepare_hooks(storage, |h| { + MemberChangedHookMsg::one(diff.clone()).into_cosmos_msg(h) + }) +} + +fn calc_weight(stake: Uint128, cfg: &Config) -> Option { + if stake < cfg.min_bond { + None + } else { + let w = stake.u128() / (cfg.tokens_per_weight.u128()); + Some(w as u64) + } } pub fn handle_claim( @@ -160,56 +226,16 @@ pub fn handle_claim( }) } -// the logic from handle_update_admin extracted for easier import -pub fn update_members( - deps: DepsMut, - height: u64, - sender: HumanAddr, - to_add: Vec, - to_remove: Vec, -) -> Result { - let admin = ADMIN.load(deps.storage)?; - assert_admin(deps.api, sender, admin)?; - - 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 raw = deps.api.canonical_address(&add.addr)?; - MEMBERS.update(deps.storage, &raw, 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 raw = deps.api.canonical_address(&remove)?; - let old = MEMBERS.may_load(deps.storage, &raw)?; - // 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, &raw, height)?; - } - } - - TOTAL.save(deps.storage, &total)?; - Ok(MemberChangedHookMsg { diffs }) -} - fn assert_admin( api: &dyn Api, - sender: HumanAddr, + sender: &HumanAddr, admin: Option, ) -> Result<(), ContractError> { let owner = match admin { Some(x) => x, None => return Err(ContractError::Unauthorized {}), }; - if api.canonical_address(&sender)? != owner { + if api.canonical_address(sender)? != owner { Err(ContractError::Unauthorized {}) } else { Ok(()) @@ -222,7 +248,7 @@ pub fn handle_add_hook( addr: HumanAddr, ) -> Result { let admin = ADMIN.load(deps.storage)?; - assert_admin(deps.api, info.sender, admin)?; + assert_admin(deps.api, &info.sender, admin)?; add_hook(deps.storage, addr)?; Ok(HandleResponse::default()) } @@ -233,7 +259,7 @@ pub fn handle_remove_hook( addr: HumanAddr, ) -> Result { let admin = ADMIN.load(deps.storage)?; - assert_admin(deps.api, info.sender, admin)?; + assert_admin(deps.api, &info.sender, admin)?; remove_hook(deps.storage, addr)?; Ok(HandleResponse::default()) } diff --git a/contracts/cw4-stake/src/error.rs b/contracts/cw4-stake/src/error.rs index adc1f3822..4d04239d9 100644 --- a/contracts/cw4-stake/src/error.rs +++ b/contracts/cw4-stake/src/error.rs @@ -11,4 +11,10 @@ pub enum ContractError { #[error("No claims that can be released currently")] NothingToClaim {}, + + #[error("Must send '{0}' to stake")] + MissingDenom(String), + + #[error("Sent unsupported denoms, must send '{0}' to stake")] + ExtraDenoms(String), } diff --git a/contracts/cw4-stake/src/msg.rs b/contracts/cw4-stake/src/msg.rs index 47adaa066..91ae5024a 100644 --- a/contracts/cw4-stake/src/msg.rs +++ b/contracts/cw4-stake/src/msg.rs @@ -9,7 +9,7 @@ use cw0::Duration; pub struct InitMsg { /// denom of the token to stake pub stake: String, - pub tokens_per_weight: u64, + pub tokens_per_weight: Uint128, pub min_bond: Uint128, pub unbonding_period: Duration, diff --git a/contracts/cw4-stake/src/state.rs b/contracts/cw4-stake/src/state.rs index 590f7f78c..4ede89818 100644 --- a/contracts/cw4-stake/src/state.rs +++ b/contracts/cw4-stake/src/state.rs @@ -10,7 +10,7 @@ use cw_storage_plus::{snapshot_names, Item, Map, SnapshotMap, SnapshotNamespaces pub struct Config { /// denom of the token to stake pub denom: String, - pub tokens_per_weight: u64, + pub tokens_per_weight: Uint128, pub min_bond: Uint128, pub unbonding_period: Duration, } diff --git a/packages/cw4/src/hook.rs b/packages/cw4/src/hook.rs index 85ed53425..563b1635b 100644 --- a/packages/cw4/src/hook.rs +++ b/packages/cw4/src/hook.rs @@ -3,13 +3,6 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::{to_binary, Binary, CosmosMsg, HumanAddr, StdResult, WasmMsg}; -/// MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] -#[serde(rename_all = "snake_case")] -pub struct MemberChangedHookMsg { - pub diffs: Vec, -} - /// MemberDiff shows the old and new states for a given cw4 member /// They cannot both be None. /// old = None, new = Some -> Insert @@ -36,7 +29,23 @@ impl MemberDiff { } } +/// MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg +/// This contains a list of all diffs on the given transaction. +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub struct MemberChangedHookMsg { + pub diffs: Vec, +} + impl MemberChangedHookMsg { + pub fn one(diff: MemberDiff) -> Self { + MemberChangedHookMsg { diffs: vec![diff] } + } + + pub fn new(diffs: Vec) -> Self { + MemberChangedHookMsg { diffs } + } + /// serializes the message pub fn into_binary(self) -> StdResult { let msg = MemberChangedHandleMsg::MemberChangedHook(self); From fc9eb8d40f9aad8e49a15d9d70a7c51fd06ded74 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 13:03:30 +0100 Subject: [PATCH 09/22] Basic tests passing --- contracts/cw4-stake/src/contract.rs | 533 +++++++++++++--------------- 1 file changed, 248 insertions(+), 285 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index ed85898d6..55d702887 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -71,13 +71,22 @@ pub fn handle_update_admin( info: MessageInfo, new_admin: Option, ) -> Result { + update_admin(deps, info.sender, new_admin)?; + Ok(HandleResponse::default()) +} + +// the logic from handle_update_admin extracted for easier import +pub fn update_admin( + deps: DepsMut, + sender: HumanAddr, + new_admin: Option, +) -> Result, ContractError> { let api = deps.api; ADMIN.update(deps.storage, |state| -> Result<_, ContractError> { - assert_admin(api, &info.sender, state)?; + assert_admin(api, &sender, state)?; let new_admin = maybe_canonical(api, new_admin)?; Ok(new_admin) - })?; - Ok(HandleResponse::default()) + }) } pub fn handle_bond( @@ -358,8 +367,9 @@ fn list_members( mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{from_slice, OwnedDeps, Querier, StdError, Storage}; - use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; + use cosmwasm_std::{from_slice, OwnedDeps, Querier, Storage}; + // use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; + use cw0::Duration; use cw4::{member_key, TOTAL_KEY}; const ADMIN: &str = "juan"; @@ -367,41 +377,67 @@ mod tests { const USER2: &str = "else"; const USER3: &str = "funny"; - fn do_init(deps: DepsMut) { + const DENOM: &str = "stake"; + const TOKENS_PER_WEIGHT: Uint128 = Uint128(1_000); + const MIN_BOND: Uint128 = Uint128(5_000); + const UNBONDING_BLOCKS: u64 = 100; + + fn default_init(deps: DepsMut) { + do_init( + deps, + TOKENS_PER_WEIGHT, + MIN_BOND, + Duration::Height(UNBONDING_BLOCKS), + ) + } + + fn do_init( + deps: DepsMut, + tokens_per_weight: Uint128, + min_bond: Uint128, + unbonding_period: Duration, + ) { let msg = InitMsg { + stake: DENOM.to_string(), + tokens_per_weight, + min_bond, + unbonding_period, admin: Some(ADMIN.into()), - members: vec![ - Member { - addr: USER1.into(), - weight: 11, - }, - Member { - addr: USER2.into(), - weight: 6, - }, - ], }; let info = mock_info("creator", &[]); init(deps, mock_env(), info, msg).unwrap(); } + fn bond_stake(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) { + let mut env = mock_env(); + env.block.height += height_delta; + + for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] { + if *stake != 0 { + let msg = HandleMsg::Bond {}; + let info = mock_info(HumanAddr::from(*addr), &coins(*stake, DENOM)); + handle(deps.branch(), env.clone(), info, msg).unwrap(); + } + } + } + #[test] fn proper_initialization() { let mut deps = mock_dependencies(&[]); - do_init(deps.as_mut()); + default_init(deps.as_mut()); // it worked, let's query the state let res = query_admin(deps.as_ref()).unwrap(); assert_eq!(Some(HumanAddr::from(ADMIN)), res.admin); let res = query_total_weight(deps.as_ref()).unwrap(); - assert_eq!(17, res.weight); + assert_eq!(0, res.weight); } #[test] fn try_update_admin() { let mut deps = mock_dependencies(&[]); - do_init(deps.as_mut()); + default_init(deps.as_mut()); // a member cannot update admin let err = update_admin(deps.as_mut(), USER1.into(), Some(USER3.into())).unwrap_err(); @@ -429,25 +465,7 @@ mod tests { } } - #[test] - fn try_member_queries() { - let mut deps = mock_dependencies(&[]); - do_init(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 - } - + // this tests the member queries fn assert_users( deps: &OwnedDeps, user1_weight: Option, @@ -481,267 +499,39 @@ mod tests { } #[test] - fn add_new_remove_old_member() { - let mut deps = mock_dependencies(&[]); - do_init(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, - USER1.into(), - add.clone(), - remove.clone(), - ) - .unwrap_err(); - match err { - ContractError::Unauthorized {} => {} - e => panic!("Unexpected error: {}", e), - } - - // Test the values from init - 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 init (expected initial values) - assert_users(&deps, Some(11), Some(6), None, Some(height + 1)); - - // admin updates properly - update_members(deps.as_mut(), height + 10, ADMIN.into(), 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_init(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, ADMIN.into(), 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 + fn bond_stake_adds_membership() { let mut deps = mock_dependencies(&[]); - do_init(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 + default_init(deps.as_mut()); let height = mock_env().block.height; - update_members(deps.as_mut(), height, ADMIN.into(), 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_init(deps.as_mut()); + // Assert original weights + assert_users(&deps, None, None, None, None); - let hooks = query_hooks(deps.as_ref()).unwrap(); - assert!(hooks.hooks.is_empty()); + // ensure it rounds down, and respects cut-off + bond_stake(deps.as_mut(), 12_000, 7_500, 4_000, 1); - let contract1 = HumanAddr::from("hook1"); - let contract2 = HumanAddr::from("hook2"); + // Assert updated weights + assert_users(&deps, Some(12), Some(7), None, None); - let add_msg = HandleMsg::AddHook { - addr: contract1.clone(), - }; + // add some more, ensure the sum is properly respected (7.5 + 7.6 = 15 not 14) + bond_stake(deps.as_mut(), 0, 7_600, 1_200, 2); - // non-admin cannot add hook - let user_info = mock_info(USER1, &[]); - let err = handle( - deps.as_mut(), - mock_env(), - user_info.clone(), - add_msg.clone(), - ) - .unwrap_err(); - match err { - ContractError::Unauthorized {} => {} - e => panic!("Unexpected error: {}", e), - } + // Assert updated weights + assert_users(&deps, Some(12), Some(15), Some(5), None); - // admin can add it, and it appears in the query - let admin_info = mock_info(ADMIN, &[]); - let _ = handle( - deps.as_mut(), - mock_env(), - admin_info.clone(), - add_msg.clone(), - ) - .unwrap(); - let hooks = query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract1.clone()]); - - // cannot remove a non-registered contract - let remove_msg = HandleMsg::RemoveHook { - addr: contract2.clone(), - }; - let err = handle( - deps.as_mut(), - mock_env(), - admin_info.clone(), - remove_msg.clone(), - ) - .unwrap_err(); - - match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, HOOK_NOT_REGISTERED) - } - e => panic!("Unexpected error: {}", e), - } - - // add second contract - let add_msg2 = HandleMsg::AddHook { - addr: contract2.clone(), - }; - let _ = handle(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); - let hooks = query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); - - // cannot re-add an existing contract - let err = handle( - deps.as_mut(), - mock_env(), - admin_info.clone(), - add_msg.clone(), - ) - .unwrap_err(); - match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, HOOK_ALREADY_REGISTERED) - } - e => panic!("Unexpected error: {}", e), - } - - // non-admin cannot remove - let remove_msg = HandleMsg::RemoveHook { - addr: contract1.clone(), - }; - let err = handle( - deps.as_mut(), - mock_env(), - user_info.clone(), - remove_msg.clone(), - ) - .unwrap_err(); - match err { - ContractError::Unauthorized {} => {} - e => panic!("Unexpected error: {}", e), - } - - // remove the original - let _ = handle( - deps.as_mut(), - mock_env(), - admin_info.clone(), - remove_msg.clone(), - ) - .unwrap(); - let hooks = query_hooks(deps.as_ref()).unwrap(); - assert_eq!(hooks.hooks, vec![contract2.clone()]); - } - - #[test] - fn hooks_fire() { - let mut deps = mock_dependencies(&[]); - do_init(deps.as_mut()); - - let hooks = query_hooks(deps.as_ref()).unwrap(); - assert!(hooks.hooks.is_empty()); - - let contract1 = HumanAddr::from("hook1"); - let contract2 = HumanAddr::from("hook2"); - - // register 2 hooks - let admin_info = mock_info(ADMIN, &[]); - let add_msg = HandleMsg::AddHook { - addr: contract1.clone(), - }; - let add_msg2 = HandleMsg::AddHook { - addr: contract2.clone(), - }; - for msg in vec![add_msg, add_msg2] { - let _ = handle(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 = HandleMsg::UpdateMembers { remove, add }; - - // admin updates properly - assert_users(&deps, Some(11), Some(6), None, None); - let res = handle(deps.as_mut(), mock_env(), admin_info.clone(), 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 = hook_msg.clone().into_cosmos_msg(contract1).unwrap(); - let msg2 = hook_msg.into_cosmos_msg(contract2).unwrap(); - assert_eq!(res.messages, vec![msg1, msg2]); + // check historical queries all work + assert_users(&deps, None, None, None, Some(height + 1)); // before first stake + assert_users(&deps, Some(12), Some(7), None, Some(height + 2)); // after first stake + assert_users(&deps, Some(12), Some(15), Some(5), Some(height + 3)); // after second stake } #[test] fn raw_queries_work() { // add will over-write and remove have no effect let mut deps = mock_dependencies(&[]); - do_init(deps.as_mut()); + default_init(deps.as_mut()); + // Set values as (11, 6, None) + bond_stake(deps.as_mut(), 11_000, 6_000, 0, 1); // get total from raw key let total_raw = deps.storage.get(TOTAL_KEY).unwrap(); @@ -759,4 +549,177 @@ mod tests { let member3_raw = deps.storage.get(&member_key(&member3_canon)); assert_eq!(None, member3_raw); } + + // TODO: unbonding -> claims + // TODO: accepting claims + // TODO: edge-case -> weight = 0, also min_bond = 0 + + // #[test] + // fn add_remove_hooks() { + // // add will over-write and remove have no effect + // let mut deps = mock_dependencies(&[]); + // default_init(deps.as_mut()); + // + // let hooks = query_hooks(deps.as_ref()).unwrap(); + // assert!(hooks.hooks.is_empty()); + // + // let contract1 = HumanAddr::from("hook1"); + // let contract2 = HumanAddr::from("hook2"); + // + // let add_msg = HandleMsg::AddHook { + // addr: contract1.clone(), + // }; + // + // // non-admin cannot add hook + // let user_info = mock_info(USER1, &[]); + // let err = handle( + // deps.as_mut(), + // mock_env(), + // user_info.clone(), + // add_msg.clone(), + // ) + // .unwrap_err(); + // match err { + // ContractError::Unauthorized {} => {} + // e => panic!("Unexpected error: {}", e), + // } + // + // // admin can add it, and it appears in the query + // let admin_info = mock_info(ADMIN, &[]); + // let _ = handle( + // deps.as_mut(), + // mock_env(), + // admin_info.clone(), + // add_msg.clone(), + // ) + // .unwrap(); + // let hooks = query_hooks(deps.as_ref()).unwrap(); + // assert_eq!(hooks.hooks, vec![contract1.clone()]); + // + // // cannot remove a non-registered contract + // let remove_msg = HandleMsg::RemoveHook { + // addr: contract2.clone(), + // }; + // let err = handle( + // deps.as_mut(), + // mock_env(), + // admin_info.clone(), + // remove_msg.clone(), + // ) + // .unwrap_err(); + // + // match err { + // ContractError::Std(StdError::GenericErr { msg, .. }) => { + // assert_eq!(msg, HOOK_NOT_REGISTERED) + // } + // e => panic!("Unexpected error: {}", e), + // } + // + // // add second contract + // let add_msg2 = HandleMsg::AddHook { + // addr: contract2.clone(), + // }; + // let _ = handle(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); + // let hooks = query_hooks(deps.as_ref()).unwrap(); + // assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); + // + // // cannot re-add an existing contract + // let err = handle( + // deps.as_mut(), + // mock_env(), + // admin_info.clone(), + // add_msg.clone(), + // ) + // .unwrap_err(); + // match err { + // ContractError::Std(StdError::GenericErr { msg, .. }) => { + // assert_eq!(msg, HOOK_ALREADY_REGISTERED) + // } + // e => panic!("Unexpected error: {}", e), + // } + // + // // non-admin cannot remove + // let remove_msg = HandleMsg::RemoveHook { + // addr: contract1.clone(), + // }; + // let err = handle( + // deps.as_mut(), + // mock_env(), + // user_info.clone(), + // remove_msg.clone(), + // ) + // .unwrap_err(); + // match err { + // ContractError::Unauthorized {} => {} + // e => panic!("Unexpected error: {}", e), + // } + // + // // remove the original + // let _ = handle( + // deps.as_mut(), + // mock_env(), + // admin_info.clone(), + // remove_msg.clone(), + // ) + // .unwrap(); + // let hooks = query_hooks(deps.as_ref()).unwrap(); + // assert_eq!(hooks.hooks, vec![contract2.clone()]); + // } + // + // #[test] + // fn hooks_fire() { + // let mut deps = mock_dependencies(&[]); + // default_init(deps.as_mut()); + // + // let hooks = query_hooks(deps.as_ref()).unwrap(); + // assert!(hooks.hooks.is_empty()); + // + // let contract1 = HumanAddr::from("hook1"); + // let contract2 = HumanAddr::from("hook2"); + // + // // register 2 hooks + // let admin_info = mock_info(ADMIN, &[]); + // let add_msg = HandleMsg::AddHook { + // addr: contract1.clone(), + // }; + // let add_msg2 = HandleMsg::AddHook { + // addr: contract2.clone(), + // }; + // for msg in vec![add_msg, add_msg2] { + // let _ = handle(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 = HandleMsg::UpdateMembers { remove, add }; + // + // // admin updates properly + // assert_users(&deps, Some(11), Some(6), None, None); + // let res = handle(deps.as_mut(), mock_env(), admin_info.clone(), 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 = hook_msg.clone().into_cosmos_msg(contract1).unwrap(); + // let msg2 = hook_msg.into_cosmos_msg(contract2).unwrap(); + // assert_eq!(res.messages, vec![msg1, msg2]); + // } } From 34a2f411bfce71fd0a0a8ca9697d2434e6b9189f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 13:05:50 +0100 Subject: [PATCH 10/22] Update schema --- contracts/cw4-stake/schema/init_msg.json | 4 +--- packages/cw4/schema/member_changed_hook_msg.json | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/contracts/cw4-stake/schema/init_msg.json b/contracts/cw4-stake/schema/init_msg.json index 4de53155e..0d7f1d7f8 100644 --- a/contracts/cw4-stake/schema/init_msg.json +++ b/contracts/cw4-stake/schema/init_msg.json @@ -27,9 +27,7 @@ "type": "string" }, "tokens_per_weight": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "$ref": "#/definitions/Uint128" }, "unbonding_period": { "$ref": "#/definitions/Duration" diff --git a/packages/cw4/schema/member_changed_hook_msg.json b/packages/cw4/schema/member_changed_hook_msg.json index 823135050..ae3c7bf3a 100644 --- a/packages/cw4/schema/member_changed_hook_msg.json +++ b/packages/cw4/schema/member_changed_hook_msg.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MemberChangedHookMsg", - "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg", + "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg This contains a list of all diffs on the given transaction.", "type": "object", "required": [ "diffs" From 4cf8e428d357eaa1f30841387ea19004377e2409 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 15:49:24 +0100 Subject: [PATCH 11/22] Test claiming process --- .../cw3-flex-multisig/schema/handle_msg.json | 2 +- contracts/cw4-stake/src/contract.rs | 204 +++++++++++++++++- packages/cw0/src/claim.rs | 9 + 3 files changed, 208 insertions(+), 7 deletions(-) diff --git a/contracts/cw3-flex-multisig/schema/handle_msg.json b/contracts/cw3-flex-multisig/schema/handle_msg.json index a6c801c68..58d1878a2 100644 --- a/contracts/cw3-flex-multisig/schema/handle_msg.json +++ b/contracts/cw3-flex-multisig/schema/handle_msg.json @@ -277,7 +277,7 @@ "type": "string" }, "MemberChangedHookMsg": { - "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg", + "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg This contains a list of all diffs on the given transaction.", "type": "object", "required": [ "diffs" diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 55d702887..61af15814 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -369,6 +369,7 @@ mod tests { use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{from_slice, OwnedDeps, Querier, Storage}; // use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; + use cw0::claim::Claim; use cw0::Duration; use cw4::{member_key, TOTAL_KEY}; @@ -408,7 +409,7 @@ mod tests { init(deps, mock_env(), info, msg).unwrap(); } - fn bond_stake(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) { + fn bond(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) { let mut env = mock_env(); env.block.height += height_delta; @@ -421,6 +422,21 @@ mod tests { } } + fn unbond(mut deps: DepsMut, user1: u128, user2: u128, user3: u128, height_delta: u64) { + let mut env = mock_env(); + env.block.height += height_delta; + + for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] { + if *stake != 0 { + let msg = HandleMsg::Unbond { + amount: Uint128(*stake), + }; + let info = mock_info(HumanAddr::from(*addr), &[]); + handle(deps.branch(), env.clone(), info, msg).unwrap(); + } + } + } + #[test] fn proper_initialization() { let mut deps = mock_dependencies(&[]); @@ -508,13 +524,13 @@ mod tests { assert_users(&deps, None, None, None, None); // ensure it rounds down, and respects cut-off - bond_stake(deps.as_mut(), 12_000, 7_500, 4_000, 1); + bond(deps.as_mut(), 12_000, 7_500, 4_000, 1); // Assert updated weights assert_users(&deps, Some(12), Some(7), None, None); // add some more, ensure the sum is properly respected (7.5 + 7.6 = 15 not 14) - bond_stake(deps.as_mut(), 0, 7_600, 1_200, 2); + bond(deps.as_mut(), 0, 7_600, 1_200, 2); // Assert updated weights assert_users(&deps, Some(12), Some(15), Some(5), None); @@ -525,13 +541,41 @@ mod tests { assert_users(&deps, Some(12), Some(15), Some(5), Some(height + 3)); // after second stake } + #[test] + fn unbond_stake_update_membership() { + let mut deps = mock_dependencies(&[]); + default_init(deps.as_mut()); + let height = mock_env().block.height; + + // ensure it rounds down, and respects cut-off + bond(deps.as_mut(), 12_000, 7_500, 4_000, 1); + unbond(deps.as_mut(), 4_500, 2_600, 1_111, 2); + + // Assert updated weights + assert_users(&deps, Some(7), None, None, None); + + // Adding a little more returns weight + bond(deps.as_mut(), 600, 100, 2_222, 3); + + // Assert updated weights + assert_users(&deps, Some(8), Some(5), Some(5), None); + + // check historical queries all work + assert_users(&deps, None, None, None, Some(height + 1)); // before first stake + assert_users(&deps, Some(12), Some(7), None, Some(height + 2)); // after first bond + assert_users(&deps, Some(7), None, None, Some(height + 3)); // after first unbond + assert_users(&deps, Some(8), Some(5), Some(5), Some(height + 4)); // after second bond + + // TODO: error if try to ubond more than stake + } + #[test] fn raw_queries_work() { // add will over-write and remove have no effect let mut deps = mock_dependencies(&[]); default_init(deps.as_mut()); // Set values as (11, 6, None) - bond_stake(deps.as_mut(), 11_000, 6_000, 0, 1); + bond(deps.as_mut(), 11_000, 6_000, 0, 1); // get total from raw key let total_raw = deps.storage.get(TOTAL_KEY).unwrap(); @@ -550,8 +594,156 @@ mod tests { assert_eq!(None, member3_raw); } - // TODO: unbonding -> claims - // TODO: accepting claims + fn get_claims>(deps: Deps, addr: U) -> Vec { + query_claims(deps, addr.into()).unwrap().claims + } + + #[test] + fn unbond_claim_workflow() { + let mut deps = mock_dependencies(&[]); + default_init(deps.as_mut()); + + // create some data + bond(deps.as_mut(), 12_000, 7_500, 4_000, 1); + unbond(deps.as_mut(), 4_500, 2_600, 0, 2); + let mut env = mock_env(); + env.block.height += 2; + + // check the claims for each user + let expires = Duration::Height(UNBONDING_BLOCKS).after(&env.block); + assert_eq!( + get_claims(deps.as_ref(), USER1), + vec![Claim::new(4_500, expires)] + ); + assert_eq!( + get_claims(deps.as_ref(), USER2), + vec![Claim::new(2_600, expires)] + ); + assert_eq!(get_claims(deps.as_ref(), USER3), vec![]); + + // do another unbond later on + let mut env2 = mock_env(); + env2.block.height += 22; + unbond(deps.as_mut(), 0, 1_345, 1_500, 22); + + // with updated claims + let expires2 = Duration::Height(UNBONDING_BLOCKS).after(&env2.block); + assert_eq!( + get_claims(deps.as_ref(), USER1), + vec![Claim::new(4_500, expires)] + ); + assert_eq!( + get_claims(deps.as_ref(), USER2), + vec![Claim::new(2_600, expires), Claim::new(1_345, expires2)] + ); + assert_eq!( + get_claims(deps.as_ref(), USER3), + vec![Claim::new(1_500, expires2)] + ); + + // nothing can be withdrawn yet + let err = handle( + deps.as_mut(), + env2.clone(), + mock_info(USER1, &[]), + HandleMsg::Claim {}, + ) + .unwrap_err(); + match err { + ContractError::NothingToClaim {} => {} + e => panic!("unexpected error: {}", e), + } + + // now mature first section, withdraw that + let mut env3 = mock_env(); + env3.block.height += 2 + UNBONDING_BLOCKS; + // first one can now release + let res = handle( + deps.as_mut(), + env3.clone(), + mock_info(USER1, &[]), + HandleMsg::Claim {}, + ) + .unwrap(); + assert_eq!( + res.messages, + vec![BankMsg::Send { + from_address: env3.contract.address.clone(), + to_address: USER1.into(), + amount: coins(4_500, DENOM), + } + .into()] + ); + + // second releases partially + let res = handle( + deps.as_mut(), + env3.clone(), + mock_info(USER2, &[]), + HandleMsg::Claim {}, + ) + .unwrap(); + assert_eq!( + res.messages, + vec![BankMsg::Send { + from_address: env3.contract.address.clone(), + to_address: USER2.into(), + amount: coins(2_600, DENOM), + } + .into()] + ); + + // but the third one cannot release + let err = handle( + deps.as_mut(), + env3.clone(), + mock_info(USER3, &[]), + HandleMsg::Claim {}, + ) + .unwrap_err(); + match err { + ContractError::NothingToClaim {} => {} + e => panic!("unexpected error: {}", e), + } + + // claims updated properly + assert_eq!(get_claims(deps.as_ref(), USER1), vec![]); + assert_eq!( + get_claims(deps.as_ref(), USER2), + vec![Claim::new(1_345, expires2)] + ); + assert_eq!( + get_claims(deps.as_ref(), USER3), + vec![Claim::new(1_500, expires2)] + ); + + // add another few claims for 2 + unbond(deps.as_mut(), 0, 600, 0, 30 + UNBONDING_BLOCKS); + unbond(deps.as_mut(), 0, 1_005, 0, 50 + UNBONDING_BLOCKS); + + // ensure second can claim all tokens at once + let mut env4 = mock_env(); + env4.block.height += 55 + UNBONDING_BLOCKS + UNBONDING_BLOCKS; + let res = handle( + deps.as_mut(), + env4.clone(), + mock_info(USER2, &[]), + HandleMsg::Claim {}, + ) + .unwrap(); + assert_eq!( + res.messages, + vec![BankMsg::Send { + from_address: env4.contract.address.clone(), + to_address: USER2.into(), + // 1_345 + 600 + 1_005 + amount: coins(2_950, DENOM), + } + .into()] + ); + assert_eq!(get_claims(deps.as_ref(), USER2), vec![]); + } + // TODO: edge-case -> weight = 0, also min_bond = 0 // #[test] diff --git a/packages/cw0/src/claim.rs b/packages/cw0/src/claim.rs index 73678a385..2a9283df5 100644 --- a/packages/cw0/src/claim.rs +++ b/packages/cw0/src/claim.rs @@ -14,6 +14,15 @@ pub struct Claim { pub released: Expiration, } +impl Claim { + pub fn new(amount: u128, released: Expiration) -> Self { + Claim { + amount: amount.into(), + released, + } + } +} + /// this creates a claim, such that the given address an claim an amount of tokens after the release date pub fn create_claim( storage: &mut dyn Storage, From 2638195e6fa87d679380c767b5cd8a565218985d Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 17:51:27 +0100 Subject: [PATCH 12/22] Update SnapshotMap usage in cw4-stake --- contracts/cw4-stake/src/state.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contracts/cw4-stake/src/state.rs b/contracts/cw4-stake/src/state.rs index 4ede89818..7f4c2d214 100644 --- a/contracts/cw4-stake/src/state.rs +++ b/contracts/cw4-stake/src/state.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use cosmwasm_std::{CanonicalAddr, Uint128}; use cw0::Duration; use cw4::TOTAL_KEY; -use cw_storage_plus::{snapshot_names, Item, Map, SnapshotMap, SnapshotNamespaces, Strategy}; +use cw_storage_plus::{Item, Map, SnapshotMap, Strategy}; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct Config { @@ -19,8 +19,11 @@ pub const ADMIN: Item> = Item::new(b"admin"); pub const CONFIG: Item = Item::new(b"config"); pub const TOTAL: Item = Item::new(TOTAL_KEY); -// Note: this must be same as cw4::MEMBERS_KEY but macro needs literal, not const -pub const MEMBERS: SnapshotMap<&[u8], u64> = - SnapshotMap::new(snapshot_names!("members"), Strategy::EveryBlock); +pub const MEMBERS: SnapshotMap<&[u8], u64> = SnapshotMap::new( + cw4::MEMBERS_KEY, + cw4::MEMBERS_CHECKPOINTS, + cw4::MEMBERS_CHANGELOG, + Strategy::EveryBlock, +); pub const STAKE: Map<&[u8], Uint128> = Map::new(b"stake"); From 031488f44eba19bce5fbbbd48faa3eb5f7df4a03 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 21:14:29 +0100 Subject: [PATCH 13/22] Cover staked queries --- contracts/cw4-stake/src/contract.rs | 61 ++++++++++++++++++----------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 61af15814..81e3047c6 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -367,7 +367,7 @@ fn list_members( mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{from_slice, OwnedDeps, Querier, Storage}; + use cosmwasm_std::{from_slice, Storage}; // use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; use cw0::claim::Claim; use cw0::Duration; @@ -482,20 +482,20 @@ mod tests { } // this tests the member queries - fn assert_users( - deps: &OwnedDeps, + fn assert_users( + deps: Deps, user1_weight: Option, user2_weight: Option, user3_weight: Option, height: Option, ) { - let member1 = query_member(deps.as_ref(), USER1.into(), height).unwrap(); + let member1 = query_member(deps, USER1.into(), height).unwrap(); assert_eq!(member1.weight, user1_weight); - let member2 = query_member(deps.as_ref(), USER2.into(), height).unwrap(); + let member2 = query_member(deps, USER2.into(), height).unwrap(); assert_eq!(member2.weight, user2_weight); - let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap(); + let member3 = query_member(deps, USER3.into(), height).unwrap(); assert_eq!(member3.weight, user3_weight); // this is only valid if we are not doing a historical query @@ -506,14 +506,26 @@ mod tests { let count = weights.iter().filter(|x| x.is_some()).count(); // TODO: more detailed compare? - let members = list_members(deps.as_ref(), None, None).unwrap(); + let members = list_members(deps, None, None).unwrap(); assert_eq!(count, members.members.len()); - let total = query_total_weight(deps.as_ref()).unwrap(); + let total = query_total_weight(deps).unwrap(); assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 } } + // this tests the member queries + fn assert_stake(deps: Deps, user1_stake: u128, user2_stake: u128, user3_stake: u128) { + let stake1 = query_staked(deps, USER1.into()).unwrap(); + assert_eq!(stake1.stake, coin(user1_stake, DENOM)); + + let stake2 = query_staked(deps, USER2.into()).unwrap(); + assert_eq!(stake2.stake, coin(user2_stake, DENOM)); + + let stake3 = query_staked(deps, USER3.into()).unwrap(); + assert_eq!(stake3.stake, coin(user3_stake, DENOM)); + } + #[test] fn bond_stake_adds_membership() { let mut deps = mock_dependencies(&[]); @@ -521,24 +533,27 @@ mod tests { let height = mock_env().block.height; // Assert original weights - assert_users(&deps, None, None, None, None); + assert_users(deps.as_ref(), None, None, None, None); // ensure it rounds down, and respects cut-off bond(deps.as_mut(), 12_000, 7_500, 4_000, 1); // Assert updated weights - assert_users(&deps, Some(12), Some(7), None, None); + assert_stake(deps.as_ref(), 12_000, 7_500, 4_000); + assert_users(deps.as_ref(), Some(12), Some(7), None, None); // add some more, ensure the sum is properly respected (7.5 + 7.6 = 15 not 14) bond(deps.as_mut(), 0, 7_600, 1_200, 2); // Assert updated weights - assert_users(&deps, Some(12), Some(15), Some(5), None); + assert_stake(deps.as_ref(), 12_000, 15_100, 5_200); + assert_users(deps.as_ref(), Some(12), Some(15), Some(5), None); // check historical queries all work - assert_users(&deps, None, None, None, Some(height + 1)); // before first stake - assert_users(&deps, Some(12), Some(7), None, Some(height + 2)); // after first stake - assert_users(&deps, Some(12), Some(15), Some(5), Some(height + 3)); // after second stake + assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake + assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first stake + assert_users(deps.as_ref(), Some(12), Some(15), Some(5), Some(height + 3)); + // after second stake } #[test] @@ -552,19 +567,21 @@ mod tests { unbond(deps.as_mut(), 4_500, 2_600, 1_111, 2); // Assert updated weights - assert_users(&deps, Some(7), None, None, None); + assert_stake(deps.as_ref(), 7_500, 4_900, 2_889); + assert_users(deps.as_ref(), Some(7), None, None, None); // Adding a little more returns weight bond(deps.as_mut(), 600, 100, 2_222, 3); // Assert updated weights - assert_users(&deps, Some(8), Some(5), Some(5), None); + assert_users(deps.as_ref(), Some(8), Some(5), Some(5), None); // check historical queries all work - assert_users(&deps, None, None, None, Some(height + 1)); // before first stake - assert_users(&deps, Some(12), Some(7), None, Some(height + 2)); // after first bond - assert_users(&deps, Some(7), None, None, Some(height + 3)); // after first unbond - assert_users(&deps, Some(8), Some(5), Some(5), Some(height + 4)); // after second bond + assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake + assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first bond + assert_users(deps.as_ref(), Some(7), None, None, Some(height + 3)); // after first unbond + assert_users(deps.as_ref(), Some(8), Some(5), Some(5), Some(height + 4)); + // after second bond // TODO: error if try to ubond more than stake } @@ -897,9 +914,9 @@ mod tests { // let msg = HandleMsg::UpdateMembers { remove, add }; // // // admin updates properly - // assert_users(&deps, Some(11), Some(6), None, None); + // assert_users(deps.as_ref(), Some(11), Some(6), None, None); // let res = handle(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap(); - // assert_users(&deps, Some(20), None, Some(5), None); + // assert_users(deps.as_ref(), Some(20), None, Some(5), None); // // // ensure 2 messages for the 2 hooks // assert_eq!(res.messages.len(), 2); From 0e755a05579c6d7b6a4fa48bec7d1c10d3323d3e Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 21:29:34 +0100 Subject: [PATCH 14/22] Test hook functionality --- contracts/cw4-stake/src/contract.rs | 339 ++++++++++++++-------------- 1 file changed, 169 insertions(+), 170 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 81e3047c6..aef3172eb 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -367,9 +367,9 @@ fn list_members( mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{from_slice, Storage}; - // use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; + use cosmwasm_std::{from_slice, StdError, Storage}; use cw0::claim::Claim; + use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; use cw0::Duration; use cw4::{member_key, TOTAL_KEY}; @@ -761,174 +761,173 @@ mod tests { assert_eq!(get_claims(deps.as_ref(), USER2), vec![]); } + #[test] + fn add_remove_hooks() { + // add will over-write and remove have no effect + let mut deps = mock_dependencies(&[]); + default_init(deps.as_mut()); + + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert!(hooks.hooks.is_empty()); + + let contract1 = HumanAddr::from("hook1"); + let contract2 = HumanAddr::from("hook2"); + + let add_msg = HandleMsg::AddHook { + addr: contract1.clone(), + }; + + // non-admin cannot add hook + let user_info = mock_info(USER1, &[]); + let err = handle( + deps.as_mut(), + mock_env(), + user_info.clone(), + add_msg.clone(), + ) + .unwrap_err(); + match err { + ContractError::Unauthorized {} => {} + e => panic!("Unexpected error: {}", e), + } + + // admin can add it, and it appears in the query + let admin_info = mock_info(ADMIN, &[]); + let _ = handle( + deps.as_mut(), + mock_env(), + admin_info.clone(), + add_msg.clone(), + ) + .unwrap(); + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract1.clone()]); + + // cannot remove a non-registered contract + let remove_msg = HandleMsg::RemoveHook { + addr: contract2.clone(), + }; + let err = handle( + deps.as_mut(), + mock_env(), + admin_info.clone(), + remove_msg.clone(), + ) + .unwrap_err(); + + match err { + ContractError::Std(StdError::GenericErr { msg, .. }) => { + assert_eq!(msg, HOOK_NOT_REGISTERED) + } + e => panic!("Unexpected error: {}", e), + } + + // add second contract + let add_msg2 = HandleMsg::AddHook { + addr: contract2.clone(), + }; + let _ = handle(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); + + // cannot re-add an existing contract + let err = handle( + deps.as_mut(), + mock_env(), + admin_info.clone(), + add_msg.clone(), + ) + .unwrap_err(); + match err { + ContractError::Std(StdError::GenericErr { msg, .. }) => { + assert_eq!(msg, HOOK_ALREADY_REGISTERED) + } + e => panic!("Unexpected error: {}", e), + } + + // non-admin cannot remove + let remove_msg = HandleMsg::RemoveHook { + addr: contract1.clone(), + }; + let err = handle( + deps.as_mut(), + mock_env(), + user_info.clone(), + remove_msg.clone(), + ) + .unwrap_err(); + match err { + ContractError::Unauthorized {} => {} + e => panic!("Unexpected error: {}", e), + } + + // remove the original + let _ = handle( + deps.as_mut(), + mock_env(), + admin_info.clone(), + remove_msg.clone(), + ) + .unwrap(); + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert_eq!(hooks.hooks, vec![contract2.clone()]); + } + + #[test] + fn hooks_fire() { + let mut deps = mock_dependencies(&[]); + default_init(deps.as_mut()); + + let hooks = query_hooks(deps.as_ref()).unwrap(); + assert!(hooks.hooks.is_empty()); + + let contract1 = HumanAddr::from("hook1"); + let contract2 = HumanAddr::from("hook2"); + + // register 2 hooks + let admin_info = mock_info(ADMIN, &[]); + let add_msg = HandleMsg::AddHook { + addr: contract1.clone(), + }; + let add_msg2 = HandleMsg::AddHook { + addr: contract2.clone(), + }; + for msg in vec![add_msg, add_msg2] { + let _ = handle(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap(); + } + + // check firing on bond + assert_users(deps.as_ref(), None, None, None, None); + let info = mock_info(USER1, &coins(13_800, DENOM)); + let res = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap(); + assert_users(deps.as_ref(), Some(13), None, None, None); + + // ensure messages for each of the 2 hooks + assert_eq!(res.messages.len(), 2); + let diff = MemberDiff::new(USER1, None, Some(13)); + let hook_msg = MemberChangedHookMsg::one(diff); + let msg1 = hook_msg.clone().into_cosmos_msg(contract1.clone()).unwrap(); + let msg2 = hook_msg.into_cosmos_msg(contract2.clone()).unwrap(); + assert_eq!(res.messages, vec![msg1, msg2]); + + // check firing on unbond + let msg = HandleMsg::Unbond { + amount: Uint128(7_300), + }; + let info = mock_info(USER1, &[]); + let res = handle(deps.as_mut(), mock_env(), info, msg).unwrap(); + assert_users(deps.as_ref(), Some(6), None, None, None); + + // ensure messages for each of the 2 hooks + assert_eq!(res.messages.len(), 2); + let diff = MemberDiff::new(USER1, Some(13), Some(6)); + let hook_msg = MemberChangedHookMsg::one(diff); + let msg1 = hook_msg.clone().into_cosmos_msg(contract1).unwrap(); + let msg2 = hook_msg.into_cosmos_msg(contract2).unwrap(); + assert_eq!(res.messages, vec![msg1, msg2]); + } + // TODO: edge-case -> weight = 0, also min_bond = 0 - // #[test] - // fn add_remove_hooks() { - // // add will over-write and remove have no effect - // let mut deps = mock_dependencies(&[]); - // default_init(deps.as_mut()); - // - // let hooks = query_hooks(deps.as_ref()).unwrap(); - // assert!(hooks.hooks.is_empty()); - // - // let contract1 = HumanAddr::from("hook1"); - // let contract2 = HumanAddr::from("hook2"); - // - // let add_msg = HandleMsg::AddHook { - // addr: contract1.clone(), - // }; - // - // // non-admin cannot add hook - // let user_info = mock_info(USER1, &[]); - // let err = handle( - // deps.as_mut(), - // mock_env(), - // user_info.clone(), - // add_msg.clone(), - // ) - // .unwrap_err(); - // match err { - // ContractError::Unauthorized {} => {} - // e => panic!("Unexpected error: {}", e), - // } - // - // // admin can add it, and it appears in the query - // let admin_info = mock_info(ADMIN, &[]); - // let _ = handle( - // deps.as_mut(), - // mock_env(), - // admin_info.clone(), - // add_msg.clone(), - // ) - // .unwrap(); - // let hooks = query_hooks(deps.as_ref()).unwrap(); - // assert_eq!(hooks.hooks, vec![contract1.clone()]); - // - // // cannot remove a non-registered contract - // let remove_msg = HandleMsg::RemoveHook { - // addr: contract2.clone(), - // }; - // let err = handle( - // deps.as_mut(), - // mock_env(), - // admin_info.clone(), - // remove_msg.clone(), - // ) - // .unwrap_err(); - // - // match err { - // ContractError::Std(StdError::GenericErr { msg, .. }) => { - // assert_eq!(msg, HOOK_NOT_REGISTERED) - // } - // e => panic!("Unexpected error: {}", e), - // } - // - // // add second contract - // let add_msg2 = HandleMsg::AddHook { - // addr: contract2.clone(), - // }; - // let _ = handle(deps.as_mut(), mock_env(), admin_info.clone(), add_msg2).unwrap(); - // let hooks = query_hooks(deps.as_ref()).unwrap(); - // assert_eq!(hooks.hooks, vec![contract1.clone(), contract2.clone()]); - // - // // cannot re-add an existing contract - // let err = handle( - // deps.as_mut(), - // mock_env(), - // admin_info.clone(), - // add_msg.clone(), - // ) - // .unwrap_err(); - // match err { - // ContractError::Std(StdError::GenericErr { msg, .. }) => { - // assert_eq!(msg, HOOK_ALREADY_REGISTERED) - // } - // e => panic!("Unexpected error: {}", e), - // } - // - // // non-admin cannot remove - // let remove_msg = HandleMsg::RemoveHook { - // addr: contract1.clone(), - // }; - // let err = handle( - // deps.as_mut(), - // mock_env(), - // user_info.clone(), - // remove_msg.clone(), - // ) - // .unwrap_err(); - // match err { - // ContractError::Unauthorized {} => {} - // e => panic!("Unexpected error: {}", e), - // } - // - // // remove the original - // let _ = handle( - // deps.as_mut(), - // mock_env(), - // admin_info.clone(), - // remove_msg.clone(), - // ) - // .unwrap(); - // let hooks = query_hooks(deps.as_ref()).unwrap(); - // assert_eq!(hooks.hooks, vec![contract2.clone()]); - // } - // - // #[test] - // fn hooks_fire() { - // let mut deps = mock_dependencies(&[]); - // default_init(deps.as_mut()); - // - // let hooks = query_hooks(deps.as_ref()).unwrap(); - // assert!(hooks.hooks.is_empty()); - // - // let contract1 = HumanAddr::from("hook1"); - // let contract2 = HumanAddr::from("hook2"); - // - // // register 2 hooks - // let admin_info = mock_info(ADMIN, &[]); - // let add_msg = HandleMsg::AddHook { - // addr: contract1.clone(), - // }; - // let add_msg2 = HandleMsg::AddHook { - // addr: contract2.clone(), - // }; - // for msg in vec![add_msg, add_msg2] { - // let _ = handle(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 = HandleMsg::UpdateMembers { remove, add }; - // - // // admin updates properly - // assert_users(deps.as_ref(), Some(11), Some(6), None, None); - // let res = handle(deps.as_mut(), mock_env(), admin_info.clone(), msg).unwrap(); - // assert_users(deps.as_ref(), 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 = hook_msg.clone().into_cosmos_msg(contract1).unwrap(); - // let msg2 = hook_msg.into_cosmos_msg(contract2).unwrap(); - // assert_eq!(res.messages, vec![msg1, msg2]); - // } + // TODO: bond with invalid coins } From ad0234a437f147fad7b27cad36a1e73fdce0014b Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 21:45:43 +0100 Subject: [PATCH 15/22] Better cover admin setting, coin validation by bonding --- .gitignore | 3 ++ contracts/cw4-stake/src/contract.rs | 53 +++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 486bef4cd..58379de3e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ target/ hash.txt contracts.txt artifacts/ + +# code coverage +tarpaulin-report.* \ No newline at end of file diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index aef3172eb..abecb6e77 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -450,6 +450,12 @@ mod tests { assert_eq!(0, res.weight); } + fn get_admin(deps: Deps) -> Option { + let raw = query(deps, mock_env(), QueryMsg::Admin {}).unwrap(); + let res: AdminResponse = from_slice(&raw).unwrap(); + res.admin + } + #[test] fn try_update_admin() { let mut deps = mock_dependencies(&[]); @@ -464,14 +470,11 @@ mod tests { // admin can change it update_admin(deps.as_mut(), ADMIN.into(), Some(USER3.into())).unwrap(); - assert_eq!( - query_admin(deps.as_ref()).unwrap().admin, - Some(USER3.into()) - ); + assert_eq!(get_admin(deps.as_ref()), Some(USER3.into())); // and unset it update_admin(deps.as_mut(), USER3.into(), None).unwrap(); - assert_eq!(query_admin(deps.as_ref()).unwrap().admin, None); + assert_eq!(get_admin(deps.as_ref()), None); // no one can change it now let err = update_admin(deps.as_mut(), USER3.into(), Some(USER1.into())).unwrap_err(); @@ -927,7 +930,43 @@ mod tests { assert_eq!(res.messages, vec![msg1, msg2]); } - // TODO: edge-case -> weight = 0, also min_bond = 0 + #[test] + fn only_bond_valid_coins() { + let mut deps = mock_dependencies(&[]); + default_init(deps.as_mut()); + + // cannot bond with 0 coins + let info = mock_info(HumanAddr::from(USER1), &[]); + let err = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap_err(); + match err { + ContractError::MissingDenom(denom) => assert_eq!(denom.as_str(), DENOM), + _ => panic!("Unexpected error: {}", err), + } - // TODO: bond with invalid coins + // cannot bond with incorrect denom + let info = mock_info(HumanAddr::from(USER1), &[coin(500, "FOO")]); + let err = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap_err(); + match err { + ContractError::ExtraDenoms(denom) => assert_eq!(denom.as_str(), DENOM), + _ => panic!("Unexpected error: {}", err), + } + + // cannot bond with 2 coins (even if one is correct) + let info = mock_info( + HumanAddr::from(USER1), + &[coin(1234, DENOM), coin(5000, "BAR")], + ); + let err = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap_err(); + match err { + ContractError::ExtraDenoms(denom) => assert_eq!(denom.as_str(), DENOM), + _ => panic!("Unexpected error: {}", err), + } + + // can bond with just the proper denom + // cannot bond with incorrect denom + let info = mock_info(HumanAddr::from(USER1), &[coin(500, DENOM)]); + handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap(); + } + + // TODO: edge-case -> weight = 0, also min_bond = 0 } From f8cd5eb7e646100b26bbe7bcd1c791ac850248c8 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 22:17:59 +0100 Subject: [PATCH 16/22] Cover edge cases --- contracts/cw4-stake/src/contract.rs | 84 +++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index abecb6e77..273110454 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -462,28 +462,44 @@ mod tests { default_init(deps.as_mut()); // a member cannot update admin - let err = update_admin(deps.as_mut(), USER1.into(), Some(USER3.into())).unwrap_err(); + let msg = HandleMsg::UpdateAdmin { + admin: Some(USER3.into()), + }; + let err = handle(deps.as_mut(), mock_env(), mock_info(USER1, &[]), msg).unwrap_err(); match err { ContractError::Unauthorized {} => {} e => panic!("Unexpected error: {}", e), } // admin can change it - update_admin(deps.as_mut(), ADMIN.into(), Some(USER3.into())).unwrap(); + let msg = HandleMsg::UpdateAdmin { + admin: Some(USER3.into()), + }; + handle(deps.as_mut(), mock_env(), mock_info(ADMIN, &[]), msg).unwrap(); assert_eq!(get_admin(deps.as_ref()), Some(USER3.into())); // and unset it - update_admin(deps.as_mut(), USER3.into(), None).unwrap(); + let msg = HandleMsg::UpdateAdmin { admin: None }; + handle(deps.as_mut(), mock_env(), mock_info(USER3, &[]), msg).unwrap(); assert_eq!(get_admin(deps.as_ref()), None); // no one can change it now - let err = update_admin(deps.as_mut(), USER3.into(), Some(USER1.into())).unwrap_err(); + let msg = HandleMsg::UpdateAdmin { + admin: Some(USER1.into()), + }; + let err = handle(deps.as_mut(), mock_env(), mock_info(USER3, &[]), msg).unwrap_err(); match err { ContractError::Unauthorized {} => {} e => panic!("Unexpected error: {}", e), } } + fn get_member(deps: Deps, addr: HumanAddr, at_height: Option) -> Option { + let raw = query(deps, mock_env(), QueryMsg::Member { addr, at_height }).unwrap(); + let res: MemberResponse = from_slice(&raw).unwrap(); + res.weight + } + // this tests the member queries fn assert_users( deps: Deps, @@ -492,14 +508,14 @@ mod tests { user3_weight: Option, height: Option, ) { - let member1 = query_member(deps, USER1.into(), height).unwrap(); - assert_eq!(member1.weight, user1_weight); + let member1 = get_member(deps, USER1.into(), height); + assert_eq!(member1, user1_weight); - let member2 = query_member(deps, USER2.into(), height).unwrap(); - assert_eq!(member2.weight, user2_weight); + let member2 = get_member(deps, USER2.into(), height); + assert_eq!(member2, user2_weight); - let member3 = query_member(deps, USER3.into(), height).unwrap(); - assert_eq!(member3.weight, user3_weight); + let member3 = get_member(deps, USER3.into(), height); + assert_eq!(member3, user3_weight); // this is only valid if we are not doing a historical query if height.is_none() { @@ -509,10 +525,16 @@ mod tests { let count = weights.iter().filter(|x| x.is_some()).count(); // TODO: more detailed compare? - let members = list_members(deps, None, None).unwrap(); + let msg = QueryMsg::ListMembers { + start_after: None, + limit: None, + }; + let raw = query(deps, mock_env(), msg).unwrap(); + let members: MemberListResponse = from_slice(&raw).unwrap(); assert_eq!(count, members.members.len()); - let total = query_total_weight(deps).unwrap(); + let raw = query(deps, mock_env(), QueryMsg::TotalWeight {}).unwrap(); + let total: TotalWeightResponse = from_slice(&raw).unwrap(); assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 } } @@ -583,10 +605,26 @@ mod tests { assert_users(deps.as_ref(), None, None, None, Some(height + 1)); // before first stake assert_users(deps.as_ref(), Some(12), Some(7), None, Some(height + 2)); // after first bond assert_users(deps.as_ref(), Some(7), None, None, Some(height + 3)); // after first unbond - assert_users(deps.as_ref(), Some(8), Some(5), Some(5), Some(height + 4)); - // after second bond + assert_users(deps.as_ref(), Some(8), Some(5), Some(5), Some(height + 4)); // after second bond - // TODO: error if try to ubond more than stake + // error if try to unbond more than stake (USER2 has 5000 staked) + let msg = HandleMsg::Unbond { + amount: Uint128(5100), + }; + let mut env = mock_env(); + env.block.height += 5; + let info = mock_info(USER2, &[]); + let err = handle(deps.as_mut(), env, info, msg).unwrap_err(); + match err { + ContractError::Std(StdError::Underflow { + minuend, + subtrahend, + }) => { + assert_eq!(minuend.as_str(), "5000"); + assert_eq!(subtrahend.as_str(), "5100"); + } + e => panic!("Unexpected error: {}", e), + } } #[test] @@ -968,5 +1006,19 @@ mod tests { handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap(); } - // TODO: edge-case -> weight = 0, also min_bond = 0 + #[test] + fn ensure_bonding_edge_cases() { + // use min_bond 0, tokens_per_weight 500 + let mut deps = mock_dependencies(&[]); + do_init(deps.as_mut(), Uint128(100), Uint128(0), Duration::Height(5)); + + // setting 50 tokens, gives us Some(0) weight + // even setting to 1 token + bond(deps.as_mut(), 50, 1, 102, 1); + assert_users(deps.as_ref(), Some(0), Some(0), Some(1), None); + + // reducing to 0 token makes us None even with min_bond 0 + unbond(deps.as_mut(), 49, 1, 102, 2); + assert_users(deps.as_ref(), Some(0), None, None, None); + } } From 7ed59084423aae5680fbde32fb04aa183470450c Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Fri, 11 Dec 2020 22:44:19 +0100 Subject: [PATCH 17/22] Fixup after rebase on PR #170 --- contracts/cw4-stake/src/contract.rs | 2 +- contracts/cw4-stake/src/state.rs | 6 +++--- packages/cw0/src/claim.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 273110454..09c3465dd 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -636,7 +636,7 @@ mod tests { bond(deps.as_mut(), 11_000, 6_000, 0, 1); // get total from raw key - let total_raw = deps.storage.get(TOTAL_KEY).unwrap(); + let total_raw = deps.storage.get(TOTAL_KEY.as_bytes()).unwrap(); let total: u64 = from_slice(&total_raw).unwrap(); assert_eq!(17, total); diff --git a/contracts/cw4-stake/src/state.rs b/contracts/cw4-stake/src/state.rs index 7f4c2d214..fd75c6b68 100644 --- a/contracts/cw4-stake/src/state.rs +++ b/contracts/cw4-stake/src/state.rs @@ -15,8 +15,8 @@ pub struct Config { pub unbonding_period: Duration, } -pub const ADMIN: Item> = Item::new(b"admin"); -pub const CONFIG: Item = Item::new(b"config"); +pub const ADMIN: Item> = Item::new("admin"); +pub const CONFIG: Item = Item::new("config"); pub const TOTAL: Item = Item::new(TOTAL_KEY); pub const MEMBERS: SnapshotMap<&[u8], u64> = SnapshotMap::new( @@ -26,4 +26,4 @@ pub const MEMBERS: SnapshotMap<&[u8], u64> = SnapshotMap::new( Strategy::EveryBlock, ); -pub const STAKE: Map<&[u8], Uint128> = Map::new(b"stake"); +pub const STAKE: Map<&[u8], Uint128> = Map::new("stake"); diff --git a/packages/cw0/src/claim.rs b/packages/cw0/src/claim.rs index 2a9283df5..0f1c7ce7e 100644 --- a/packages/cw0/src/claim.rs +++ b/packages/cw0/src/claim.rs @@ -5,8 +5,8 @@ use crate::Expiration; use cosmwasm_std::{BlockInfo, CanonicalAddr, StdResult, Storage, Uint128}; use cw_storage_plus::Map; -// TODO: revisit design (multiple keys?) -pub const CLAIMS: Map<&[u8], Vec> = Map::new(b"claim"); +// TODO: revisit design (split each claim on own key?) +pub const CLAIMS: Map<&[u8], Vec> = Map::new("claim"); #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct Claim { From 06525691137e0f27d8da0eeadffa2f0989f06d8a Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Sun, 13 Dec 2020 22:33:41 +0100 Subject: [PATCH 18/22] Use HookError not Generic strings --- Cargo.lock | 1 + contracts/cw4-group/src/contract.rs | 12 ++++-------- contracts/cw4-group/src/error.rs | 5 +++++ contracts/cw4-stake/src/contract.rs | 10 +++------- contracts/cw4-stake/src/error.rs | 5 +++++ packages/cw0/Cargo.toml | 1 + packages/cw0/src/hooks.rs | 28 ++++++++++++++++++---------- 7 files changed, 37 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9984b228b..92d6a60e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,6 +99,7 @@ dependencies = [ "cw-storage-plus", "schemars", "serde", + "thiserror", ] [[package]] diff --git a/contracts/cw4-group/src/contract.rs b/contracts/cw4-group/src/contract.rs index 7e8f09785..838d81637 100644 --- a/contracts/cw4-group/src/contract.rs +++ b/contracts/cw4-group/src/contract.rs @@ -260,8 +260,8 @@ fn list_members( mod tests { use super::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{from_slice, OwnedDeps, Querier, StdError, Storage}; - use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; + use cosmwasm_std::{from_slice, OwnedDeps, Querier, Storage}; + use cw0::hooks::HookError; use cw4::{member_key, TOTAL_KEY}; const ADMIN: &str = "juan"; @@ -525,9 +525,7 @@ mod tests { .unwrap_err(); match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, HOOK_NOT_REGISTERED) - } + ContractError::Hook(HookError::HookNotRegistered {}) => {} e => panic!("Unexpected error: {}", e), } @@ -548,9 +546,7 @@ mod tests { ) .unwrap_err(); match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, HOOK_ALREADY_REGISTERED) - } + ContractError::Hook(HookError::HookAlreadyRegistered {}) => {} e => panic!("Unexpected error: {}", e), } diff --git a/contracts/cw4-group/src/error.rs b/contracts/cw4-group/src/error.rs index dc19f1033..2b7af8908 100644 --- a/contracts/cw4-group/src/error.rs +++ b/contracts/cw4-group/src/error.rs @@ -1,11 +1,16 @@ use cosmwasm_std::StdError; use thiserror::Error; +use cw0::hooks::HookError; + #[derive(Error, Debug)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + Hook(#[from] HookError), + #[error("Unauthorized")] Unauthorized {}, } diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 09c3465dd..7712e965f 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -369,7 +369,7 @@ mod tests { use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{from_slice, StdError, Storage}; use cw0::claim::Claim; - use cw0::hooks::{HOOK_ALREADY_REGISTERED, HOOK_NOT_REGISTERED}; + use cw0::hooks::HookError; use cw0::Duration; use cw4::{member_key, TOTAL_KEY}; @@ -857,9 +857,7 @@ mod tests { .unwrap_err(); match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, HOOK_NOT_REGISTERED) - } + ContractError::Hook(HookError::HookNotRegistered {}) => {} e => panic!("Unexpected error: {}", e), } @@ -880,9 +878,7 @@ mod tests { ) .unwrap_err(); match err { - ContractError::Std(StdError::GenericErr { msg, .. }) => { - assert_eq!(msg, HOOK_ALREADY_REGISTERED) - } + ContractError::Hook(HookError::HookAlreadyRegistered {}) => {} e => panic!("Unexpected error: {}", e), } diff --git a/contracts/cw4-stake/src/error.rs b/contracts/cw4-stake/src/error.rs index 4d04239d9..017fc0df1 100644 --- a/contracts/cw4-stake/src/error.rs +++ b/contracts/cw4-stake/src/error.rs @@ -1,11 +1,16 @@ use cosmwasm_std::StdError; use thiserror::Error; +use cw0::hooks::HookError; + #[derive(Error, Debug)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + Hook(#[from] HookError), + #[error("Unauthorized")] Unauthorized {}, diff --git a/packages/cw0/Cargo.toml b/packages/cw0/Cargo.toml index 119631e14..5f7b140a0 100644 --- a/packages/cw0/Cargo.toml +++ b/packages/cw0/Cargo.toml @@ -16,3 +16,4 @@ cosmwasm-std = { version = "0.12.0" } cw-storage-plus = { path = "../storage-plus", version = "0.3.2", features = ["iterator"] } schemars = "0.7" serde = { version = "1.0.103", default-features = false, features = ["derive"] } +thiserror = { version = "1.0.21" } diff --git a/packages/cw0/src/hooks.rs b/packages/cw0/src/hooks.rs index 440e67885..a9d1b4449 100644 --- a/packages/cw0/src/hooks.rs +++ b/packages/cw0/src/hooks.rs @@ -1,33 +1,41 @@ use cosmwasm_std::{CosmosMsg, HumanAddr, StdError, StdResult, Storage}; +use thiserror::Error; use cw_storage_plus::Item; // store all hook addresses in one item. We cannot have many of them before the contract becomes unusable anyway. pub const HOOKS: Item> = Item::new("hooks"); -// Returning a custom error here would be a headache - especially for the importing contract. -// rather I just define the error messages here and use StdError::GenericErr -pub const HOOK_ALREADY_REGISTERED: &str = "Given address already registered as a hook"; -pub const HOOK_NOT_REGISTERED: &str = "Given address not registered as a hook"; +#[derive(Error, Debug)] +pub enum HookError { + #[error("{0}")] + Std(#[from] StdError), -pub fn add_hook(storage: &mut dyn Storage, addr: HumanAddr) -> StdResult<()> { + #[error("Given address already registered as a hook")] + HookAlreadyRegistered {}, + + #[error("Given address not registered as a hook")] + HookNotRegistered {}, +} + +pub fn add_hook(storage: &mut dyn Storage, addr: HumanAddr) -> Result<(), HookError> { let mut hooks = HOOKS.may_load(storage)?.unwrap_or_default(); if !hooks.iter().any(|h| h == &addr) { hooks.push(addr); } else { - return Err(StdError::generic_err(HOOK_ALREADY_REGISTERED)); + return Err(HookError::HookAlreadyRegistered {}); } - HOOKS.save(storage, &hooks) + Ok(HOOKS.save(storage, &hooks)?) } -pub fn remove_hook(storage: &mut dyn Storage, addr: HumanAddr) -> StdResult<()> { +pub fn remove_hook(storage: &mut dyn Storage, addr: HumanAddr) -> Result<(), HookError> { let mut hooks = HOOKS.load(storage)?; if let Some(p) = hooks.iter().position(|x| x == &addr) { hooks.remove(p); } else { - return Err(StdError::generic_err(HOOK_NOT_REGISTERED)); + return Err(HookError::HookNotRegistered {}); } - HOOKS.save(storage, &hooks) + Ok(HOOKS.save(storage, &hooks)?) } pub fn prepare_hooks StdResult>( From bd578cabb834030f23b347603854c7639ba73ea0 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 14 Dec 2020 21:09:29 +0100 Subject: [PATCH 19/22] Update docs from PR review --- contracts/cw4-group/README.md | 8 +---- contracts/cw4-stake/README.md | 35 ++++++++++++++----- contracts/cw4-stake/src/lib.rs | 3 -- packages/cw4/README.md | 6 +++- .../cw4/schema/member_changed_hook_msg.json | 2 +- packages/cw4/src/hook.rs | 2 +- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/contracts/cw4-group/README.md b/contracts/cw4-group/README.md index e43f5e44b..ae8180947 100644 --- a/contracts/cw4-group/README.md +++ b/contracts/cw4-group/README.md @@ -42,16 +42,10 @@ decision-making. 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 two messages: - -`UpdateAdmin{admin}` - changes (or clears) the admin for the contract +`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 occurance will be used. -as well as one query: - -`Admin{}` - Returns the `admin` address, or `None` if unset. - diff --git a/contracts/cw4-stake/README.md b/contracts/cw4-stake/README.md index a1e57198e..29c58e39c 100644 --- a/contracts/cw4-stake/README.md +++ b/contracts/cw4-stake/README.md @@ -2,7 +2,7 @@ This is a second implementation of the [cw4 spec](../../packages/cw4/README.md). It fufills all elements of the spec, including the raw query lookups, -and it designed to be used as a backing storage for +and it is designed to be used as a backing storage for [cw3 compliant contracts](../../packages/cw3/README.md). It provides a similar API to [`cw4-group`] (which handles elected membership), @@ -42,17 +42,36 @@ 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 -eg. a KYC whitelist to say they are allowed, but cannot participate in -decision-making. +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, which may be +meaningful in some circumstances. -If `min_bond` is higher than `tokens_per_weight`, you cannot have anyone with 0 weight. +The weights of the members will be computed as the funds they send +(in tokens) divided by `tokens_per_weight`, rounded down to the nearest +whole number (i.e. using integer division). If the total sent is less than +`min_bond`, the stake will remain, but they will not be counted as a +member. If `min_bond` is higher than `tokens_per_weight`, you cannot +have any member with 0 weight. ## Messages -Update messages and queries are defined by the +Most messages and queries are defined by the [cw4 spec](../../packages/cw4/README.md). Please refer to it for more info. -The following have been added: +The following messages have been added to handle un/staking tokens: +`Bond{}` - bond all staking tokens sent with the message and update membership weight + +`Unbond{tokens}` - starts the unbonding process for the given number + of tokens. The sender immediately looses weight from these tokens, + and can claim them back to his wallet after `unbonding_period` + +`Claim{}` - used to claim your native tokens that you previously "unbonded" + after the contract-defined waiting period (eg. 1 week) + +And the corresponding queries: + +`Claims{address}` - Claims shows the tokens in process of unbonding + for this address + +`Staked{address}` - Show the number of tokens currently staked by this address. diff --git a/contracts/cw4-stake/src/lib.rs b/contracts/cw4-stake/src/lib.rs index 0a2bfd93b..c6449a7dd 100644 --- a/contracts/cw4-stake/src/lib.rs +++ b/contracts/cw4-stake/src/lib.rs @@ -6,6 +6,3 @@ pub mod state; // comment this out and use the lower form if the contract supports migrations #[cfg(all(target_arch = "wasm32", not(feature = "library")))] cosmwasm_std::create_entry_points!(contract); - -// #[cfg(all(target_arch = "wasm32", not(feature = "library")))] -// cosmwasm_std::create_entry_points_with_migration!(contract); diff --git a/packages/cw4/README.md b/packages/cw4/README.md index 64464cb59..02e545d35 100644 --- a/packages/cw4/README.md +++ b/packages/cw4/README.md @@ -23,7 +23,9 @@ as part of another flow. Implementations should work with this setup, but may add extra `Option` fields for non-essential extensions to configure in the `init` phase. -There are four messages supported by a group contract: +There are three messages supported by a group contract: + +`UpdateAdmin{admin}` - changes (or clears) the admin for the contract `AddHook{addr}` - adds a contract address to be called upon every `UpdateMembers` call. This can only be called by the admin, and care must @@ -56,6 +58,8 @@ problem, but we cover how to instantiate that in `MemberList{start_after, limit}` - Allows us to paginate over the list of all members. 0-weight members will be included. Removed members will not. +`Admin{}` - Returns the `admin` address, or `None` if unset. + ### Raw In addition to the above "SmartQueries", which make up the public API, diff --git a/packages/cw4/schema/member_changed_hook_msg.json b/packages/cw4/schema/member_changed_hook_msg.json index ae3c7bf3a..32a9300dd 100644 --- a/packages/cw4/schema/member_changed_hook_msg.json +++ b/packages/cw4/schema/member_changed_hook_msg.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "MemberChangedHookMsg", - "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg This contains a list of all diffs on the given transaction.", + "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg. This contains a list of all diffs on the given transaction.", "type": "object", "required": [ "diffs" diff --git a/packages/cw4/src/hook.rs b/packages/cw4/src/hook.rs index 563b1635b..dcafc0223 100644 --- a/packages/cw4/src/hook.rs +++ b/packages/cw4/src/hook.rs @@ -29,7 +29,7 @@ impl MemberDiff { } } -/// MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg +/// MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg. /// This contains a list of all diffs on the given transaction. #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] From d582ed68379440294da7c966b7efe340ff7e9042 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 14 Dec 2020 21:13:38 +0100 Subject: [PATCH 20/22] Update message variants --- .../cw20-staking/schema/claims_response.json | 4 ++-- contracts/cw20-staking/src/contract.rs | 4 ++-- contracts/cw4-stake/README.md | 2 +- contracts/cw4-stake/schema/claims_response.json | 4 ++-- contracts/cw4-stake/schema/handle_msg.json | 6 +++--- contracts/cw4-stake/schema/init_msg.json | 10 +++++----- contracts/cw4-stake/src/contract.rs | 16 ++++++++-------- contracts/cw4-stake/src/msg.rs | 6 +++--- packages/cw0/src/claim.rs | 13 +++++++------ 9 files changed, 33 insertions(+), 32 deletions(-) diff --git a/contracts/cw20-staking/schema/claims_response.json b/contracts/cw20-staking/schema/claims_response.json index 494fb0c3e..efadc9a12 100644 --- a/contracts/cw20-staking/schema/claims_response.json +++ b/contracts/cw20-staking/schema/claims_response.json @@ -18,13 +18,13 @@ "type": "object", "required": [ "amount", - "released" + "release_at" ], "properties": { "amount": { "$ref": "#/definitions/Uint128" }, - "released": { + "release_at": { "$ref": "#/definitions/Expiration" } } diff --git a/contracts/cw20-staking/src/contract.rs b/contracts/cw20-staking/src/contract.rs index 6ee517e99..148ce3189 100644 --- a/contracts/cw20-staking/src/contract.rs +++ b/contracts/cw20-staking/src/contract.rs @@ -827,7 +827,7 @@ mod tests { // proper claims let expected_claims = vec![Claim { amount: bobs_claim, - released: (DAY * 3).after(&env.block), + release_at: (DAY * 3).after(&env.block), }]; assert_eq!(expected_claims, get_claims(deps.as_ref(), &bob)); @@ -870,7 +870,7 @@ mod tests { let bobs_claim = Uint128(540); let original_claims = vec![Claim { amount: bobs_claim, - released: (DAY * 3).after(&env.block), + release_at: (DAY * 3).after(&env.block), }]; assert_eq!(original_claims, get_claims(deps.as_ref(), &bob)); diff --git a/contracts/cw4-stake/README.md b/contracts/cw4-stake/README.md index 29c58e39c..e505758bb 100644 --- a/contracts/cw4-stake/README.md +++ b/contracts/cw4-stake/README.md @@ -63,7 +63,7 @@ The following messages have been added to handle un/staking tokens: `Bond{}` - bond all staking tokens sent with the message and update membership weight `Unbond{tokens}` - starts the unbonding process for the given number - of tokens. The sender immediately looses weight from these tokens, + of tokens. The sender immediately loses weight from these tokens, and can claim them back to his wallet after `unbonding_period` `Claim{}` - used to claim your native tokens that you previously "unbonded" diff --git a/contracts/cw4-stake/schema/claims_response.json b/contracts/cw4-stake/schema/claims_response.json index 494fb0c3e..efadc9a12 100644 --- a/contracts/cw4-stake/schema/claims_response.json +++ b/contracts/cw4-stake/schema/claims_response.json @@ -18,13 +18,13 @@ "type": "object", "required": [ "amount", - "released" + "release_at" ], "properties": { "amount": { "$ref": "#/definitions/Uint128" }, - "released": { + "release_at": { "$ref": "#/definitions/Expiration" } } diff --git a/contracts/cw4-stake/schema/handle_msg.json b/contracts/cw4-stake/schema/handle_msg.json index 10ccabe35..13bbce746 100644 --- a/contracts/cw4-stake/schema/handle_msg.json +++ b/contracts/cw4-stake/schema/handle_msg.json @@ -15,7 +15,7 @@ } }, { - "description": "Unbond will start the unbonding process for the given number of tokens. The sender immediately looses weight from these tokens, and can claim them back to his wallet after `unbonding_period`", + "description": "Unbond will start the unbonding process for the given number of tokens. The sender immediately loses weight from these tokens, and can claim them back to his wallet after `unbonding_period`", "type": "object", "required": [ "unbond" @@ -24,10 +24,10 @@ "unbond": { "type": "object", "required": [ - "amount" + "tokens" ], "properties": { - "amount": { + "tokens": { "$ref": "#/definitions/Uint128" } } diff --git a/contracts/cw4-stake/schema/init_msg.json b/contracts/cw4-stake/schema/init_msg.json index 0d7f1d7f8..d9bbf38ab 100644 --- a/contracts/cw4-stake/schema/init_msg.json +++ b/contracts/cw4-stake/schema/init_msg.json @@ -3,8 +3,8 @@ "title": "InitMsg", "type": "object", "required": [ + "denom", "min_bond", - "stake", "tokens_per_weight", "unbonding_period" ], @@ -19,13 +19,13 @@ } ] }, - "min_bond": { - "$ref": "#/definitions/Uint128" - }, - "stake": { + "denom": { "description": "denom of the token to stake", "type": "string" }, + "min_bond": { + "$ref": "#/definitions/Uint128" + }, "tokens_per_weight": { "$ref": "#/definitions/Uint128" }, diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 7712e965f..064092e42 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -38,7 +38,7 @@ pub fn init(deps: DepsMut, _env: Env, _info: MessageInfo, msg: InitMsg) -> StdRe }; let config = Config { - denom: msg.stake, + denom: msg.denom, tokens_per_weight: msg.tokens_per_weight, min_bond, unbonding_period: msg.unbonding_period, @@ -61,7 +61,7 @@ pub fn handle( HandleMsg::AddHook { addr } => handle_add_hook(deps, info, addr), HandleMsg::RemoveHook { addr } => handle_remove_hook(deps, info, addr), HandleMsg::Bond {} => handle_bond(deps, env, info), - HandleMsg::Unbond { amount } => handle_unbond(deps, env, info, amount), + HandleMsg::Unbond { tokens: amount } => handle_unbond(deps, env, info, amount), HandleMsg::Claim {} => handle_claim(deps, env, info), } } @@ -98,7 +98,7 @@ pub fn handle_bond( // ensure the sent denom was proper // NOTE: those clones are not needed (if we move denom, we return early), - // but the compiler cannot see that + // but the compiler cannot see that (yet...) let sent = match info.sent_funds.len() { 0 => Err(ContractError::MissingDenom(cfg.denom.clone())), 1 => { @@ -214,7 +214,7 @@ pub fn handle_claim( ) -> Result { let sender_raw = deps.api.canonical_address(&info.sender)?; let release = claim_tokens(deps.storage, &sender_raw, &env.block, None)?; - if release == Uint128(0) { + if release.is_zero() { return Err(ContractError::NothingToClaim {}); } @@ -399,7 +399,7 @@ mod tests { unbonding_period: Duration, ) { let msg = InitMsg { - stake: DENOM.to_string(), + denom: DENOM.to_string(), tokens_per_weight, min_bond, unbonding_period, @@ -429,7 +429,7 @@ mod tests { for (addr, stake) in &[(USER1, user1), (USER2, user2), (USER3, user3)] { if *stake != 0 { let msg = HandleMsg::Unbond { - amount: Uint128(*stake), + tokens: Uint128(*stake), }; let info = mock_info(HumanAddr::from(*addr), &[]); handle(deps.branch(), env.clone(), info, msg).unwrap(); @@ -609,7 +609,7 @@ mod tests { // error if try to unbond more than stake (USER2 has 5000 staked) let msg = HandleMsg::Unbond { - amount: Uint128(5100), + tokens: Uint128(5100), }; let mut env = mock_env(); env.block.height += 5; @@ -949,7 +949,7 @@ mod tests { // check firing on unbond let msg = HandleMsg::Unbond { - amount: Uint128(7_300), + tokens: Uint128(7_300), }; let info = mock_info(USER1, &[]); let res = handle(deps.as_mut(), mock_env(), info, msg).unwrap(); diff --git a/contracts/cw4-stake/src/msg.rs b/contracts/cw4-stake/src/msg.rs index 91ae5024a..82c9c6573 100644 --- a/contracts/cw4-stake/src/msg.rs +++ b/contracts/cw4-stake/src/msg.rs @@ -8,7 +8,7 @@ use cw0::Duration; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct InitMsg { /// denom of the token to stake - pub stake: String, + pub denom: String, pub tokens_per_weight: Uint128, pub min_bond: Uint128, pub unbonding_period: Duration, @@ -23,9 +23,9 @@ pub enum HandleMsg { /// Bond will bond all staking tokens sent with the message and update membership weight Bond {}, /// Unbond will start the unbonding process for the given number of tokens. - /// The sender immediately looses weight from these tokens, and can claim them + /// The sender immediately loses weight from these tokens, and can claim them /// back to his wallet after `unbonding_period` - Unbond { amount: Uint128 }, + Unbond { tokens: Uint128 }, /// Claim is used to claim your native tokens that you previously "unbonded" /// after the contract-defined waiting period (eg. 1 week) Claim {}, diff --git a/packages/cw0/src/claim.rs b/packages/cw0/src/claim.rs index 0f1c7ce7e..7d9f893ba 100644 --- a/packages/cw0/src/claim.rs +++ b/packages/cw0/src/claim.rs @@ -11,29 +11,30 @@ pub const CLAIMS: Map<&[u8], Vec> = Map::new("claim"); #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct Claim { pub amount: Uint128, - pub released: Expiration, + pub release_at: Expiration, } impl Claim { pub fn new(amount: u128, released: Expiration) -> Self { Claim { amount: amount.into(), - released, + release_at: released, } } } -/// this creates a claim, such that the given address an claim an amount of tokens after the release date +/// This creates a claim, such that the given address can claim an amount of tokens after +/// the release date. pub fn create_claim( storage: &mut dyn Storage, addr: &CanonicalAddr, amount: Uint128, - released: Expiration, + release_at: Expiration, ) -> StdResult<()> { // add a claim to this user to get their tokens after the unbonding period CLAIMS.update(storage, &addr, |old| -> StdResult<_> { let mut claims = old.unwrap_or_default(); - claims.push(Claim { amount, released }); + claims.push(Claim { amount, release_at }); Ok(claims) })?; Ok(()) @@ -52,7 +53,7 @@ pub fn claim_tokens( let (_send, waiting): (Vec<_>, _) = claim.unwrap_or_default().iter().cloned().partition(|c| { // if mature and we can pay fully, then include in _send - if c.released.is_expired(block) { + if c.release_at.is_expired(block) { if let Some(limit) = cap { if to_send + c.amount > limit { return false; From 2b398c02335d43a27b9320c54a2e7567d6244d2a Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 14 Dec 2020 21:19:49 +0100 Subject: [PATCH 21/22] Remove unused XyzCanonicalContract wrappers --- contracts/cw4-group/src/helpers.rs | 21 +-------------------- packages/cw20/src/helpers.rs | 23 ++--------------------- packages/cw20/src/lib.rs | 2 +- packages/cw3/src/helpers.rs | 21 +-------------------- packages/cw3/src/lib.rs | 2 +- packages/cw4/src/helpers.rs | 21 +-------------------- packages/cw4/src/lib.rs | 2 +- 7 files changed, 8 insertions(+), 84 deletions(-) diff --git a/contracts/cw4-group/src/helpers.rs b/contracts/cw4-group/src/helpers.rs index d8f4812fb..c73c76840 100644 --- a/contracts/cw4-group/src/helpers.rs +++ b/contracts/cw4-group/src/helpers.rs @@ -2,7 +2,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::ops::Deref; -use cosmwasm_std::{to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, StdResult, WasmMsg}; +use cosmwasm_std::{to_binary, CosmosMsg, HumanAddr, StdResult, WasmMsg}; use cw4::{Cw4Contract, Member}; use crate::msg::HandleMsg; @@ -27,12 +27,6 @@ impl Cw4GroupContract { Cw4GroupContract(Cw4Contract(addr)) } - /// Convert this address to a form fit for storage - pub fn canonical(&self, api: &dyn Api) -> StdResult { - let canon = api.canonical_address(&self.addr())?; - Ok(Cw4GroupCanonicalContract(canon)) - } - fn encode_msg(&self, msg: HandleMsg) -> StdResult { Ok(WasmMsg::Execute { contract_addr: self.addr(), @@ -47,16 +41,3 @@ impl Cw4GroupContract { self.encode_msg(msg) } } - -/// This is a representation of Cw4GroupContract for storage. -/// Don't use it directly, just translate to the Cw4GroupContract when needed. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Cw4GroupCanonicalContract(pub CanonicalAddr); - -impl Cw4GroupCanonicalContract { - /// Convert this address to a form fit for usage in messages and queries - pub fn human(&self, api: &dyn Api) -> StdResult { - let human = api.human_address(&self.0)?; - Ok(Cw4GroupContract::new(human)) - } -} diff --git a/packages/cw20/src/helpers.rs b/packages/cw20/src/helpers.rs index 0b249f0ad..79ffbad41 100644 --- a/packages/cw20/src/helpers.rs +++ b/packages/cw20/src/helpers.rs @@ -2,8 +2,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{ - to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, Querier, QuerierWrapper, StdResult, - Uint128, WasmMsg, WasmQuery, + to_binary, CosmosMsg, HumanAddr, Querier, QuerierWrapper, StdResult, Uint128, WasmMsg, + WasmQuery, }; use crate::{ @@ -23,12 +23,6 @@ impl Cw20Contract { self.0.clone() } - /// Convert this address to a form fit for storage - pub fn canonical(&self, api: &A) -> StdResult { - let canon = api.canonical_address(&self.0)?; - Ok(Cw20CanonicalContract(canon)) - } - pub fn call>(&self, msg: T) -> StdResult { let msg = to_binary(&msg.into())?; Ok(WasmMsg::Execute { @@ -100,16 +94,3 @@ impl Cw20Contract { self.minter(querier).is_ok() } } - -/// This is a respresentation of Cw20Contract for storage. -/// Don't use it directly, just translate to the Cw20Contract when needed. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Cw20CanonicalContract(pub CanonicalAddr); - -impl Cw20CanonicalContract { - /// Convert this address to a form fit for usage in messages and queries - pub fn human(&self, api: &A) -> StdResult { - let human = api.human_address(&self.0)?; - Ok(Cw20Contract(human)) - } -} diff --git a/packages/cw20/src/lib.rs b/packages/cw20/src/lib.rs index e904f240e..20b4e1492 100644 --- a/packages/cw20/src/lib.rs +++ b/packages/cw20/src/lib.rs @@ -9,7 +9,7 @@ pub use cw0::Expiration; pub use crate::balance::Balance; pub use crate::coin::{Cw20Coin, Cw20CoinHuman}; -pub use crate::helpers::{Cw20CanonicalContract, Cw20Contract}; +pub use crate::helpers::Cw20Contract; pub use crate::msg::Cw20HandleMsg; pub use crate::query::{ AllAccountsResponse, AllAllowancesResponse, AllowanceInfo, AllowanceResponse, BalanceResponse, diff --git a/packages/cw3/src/helpers.rs b/packages/cw3/src/helpers.rs index 6cd83e08a..9e52490eb 100644 --- a/packages/cw3/src/helpers.rs +++ b/packages/cw3/src/helpers.rs @@ -1,7 +1,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use cosmwasm_std::{to_binary, Api, CanonicalAddr, CosmosMsg, HumanAddr, StdResult, WasmMsg}; +use cosmwasm_std::{to_binary, CosmosMsg, HumanAddr, StdResult, WasmMsg}; use crate::msg::{Cw3HandleMsg, Vote}; use cw0::Expiration; @@ -21,12 +21,6 @@ impl Cw3Contract { self.0.clone() } - /// Convert this address to a form fit for storage - pub fn canonical(&self, api: &A) -> StdResult { - let canon = api.canonical_address(&self.0)?; - Ok(Cw3CanonicalContract(canon)) - } - pub fn encode_msg(&self, msg: Cw3HandleMsg) -> StdResult { Ok(WasmMsg::Execute { contract_addr: self.addr(), @@ -70,16 +64,3 @@ impl Cw3Contract { self.encode_msg(msg) } } - -/// This is a respresentation of Cw3Contract for storage. -/// Don't use it directly, just translate to the Cw3Contract when needed. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Cw3CanonicalContract(pub CanonicalAddr); - -impl Cw3CanonicalContract { - /// Convert this address to a form fit for usage in messages and queries - pub fn human(&self, api: &A) -> StdResult { - let human = api.human_address(&self.0)?; - Ok(Cw3Contract(human)) - } -} diff --git a/packages/cw3/src/lib.rs b/packages/cw3/src/lib.rs index 0f020e105..388dd9dee 100644 --- a/packages/cw3/src/lib.rs +++ b/packages/cw3/src/lib.rs @@ -3,7 +3,7 @@ mod helpers; mod msg; mod query; -pub use crate::helpers::{Cw3CanonicalContract, Cw3Contract}; +pub use crate::helpers::Cw3Contract; pub use crate::msg::{Cw3HandleMsg, Vote}; pub use crate::query::{ Cw3QueryMsg, ProposalListResponse, ProposalResponse, Status, ThresholdResponse, VoteInfo, diff --git a/packages/cw4/src/helpers.rs b/packages/cw4/src/helpers.rs index 77e90e297..fe54b2338 100644 --- a/packages/cw4/src/helpers.rs +++ b/packages/cw4/src/helpers.rs @@ -2,7 +2,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{ - from_slice, to_binary, to_vec, Api, Binary, CanonicalAddr, ContractResult, CosmosMsg, Empty, + from_slice, to_binary, to_vec, Binary, CanonicalAddr, ContractResult, CosmosMsg, Empty, HumanAddr, QuerierWrapper, QueryRequest, StdError, StdResult, SystemResult, WasmMsg, WasmQuery, }; @@ -28,12 +28,6 @@ impl Cw4Contract { self.0.clone() } - /// Convert this address to a form fit for storage - pub fn canonical(&self, api: &dyn Api) -> StdResult { - let canon = api.canonical_address(&self.0)?; - Ok(Cw4CanonicalContract(canon)) - } - fn encode_msg(&self, msg: Cw4HandleMsg) -> StdResult { Ok(WasmMsg::Execute { contract_addr: self.addr(), @@ -155,16 +149,3 @@ impl Cw4Contract { Ok(res.admin) } } - -/// This is a respresentation of Cw4Contract for storage. -/// Don't use it directly, just translate to the Cw4Contract when needed. -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct Cw4CanonicalContract(pub CanonicalAddr); - -impl Cw4CanonicalContract { - /// Convert this address to a form fit for usage in messages and queries - pub fn human(&self, api: &dyn Api) -> StdResult { - let human = api.human_address(&self.0)?; - Ok(Cw4Contract(human)) - } -} diff --git a/packages/cw4/src/lib.rs b/packages/cw4/src/lib.rs index b71280c9a..6628fbb80 100644 --- a/packages/cw4/src/lib.rs +++ b/packages/cw4/src/lib.rs @@ -3,7 +3,7 @@ mod hook; mod msg; mod query; -pub use crate::helpers::{Cw4CanonicalContract, Cw4Contract}; +pub use crate::helpers::Cw4Contract; pub use crate::hook::{MemberChangedHookMsg, MemberDiff}; pub use crate::msg::Cw4HandleMsg; pub use crate::query::{ From 092032eff2f08859ee0109280a8195b7066a9130 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Mon, 14 Dec 2020 21:42:55 +0100 Subject: [PATCH 22/22] Cleanup logic from PR review --- .../cw3-flex-multisig/schema/handle_msg.json | 2 +- contracts/cw3-flex-multisig/src/contract.rs | 4 ++-- contracts/cw4-stake/src/contract.rs | 17 +++++++++++++---- contracts/cw4-stake/src/error.rs | 3 +++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/contracts/cw3-flex-multisig/schema/handle_msg.json b/contracts/cw3-flex-multisig/schema/handle_msg.json index 58d1878a2..1a2c2be90 100644 --- a/contracts/cw3-flex-multisig/schema/handle_msg.json +++ b/contracts/cw3-flex-multisig/schema/handle_msg.json @@ -277,7 +277,7 @@ "type": "string" }, "MemberChangedHookMsg": { - "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg This contains a list of all diffs on the given transaction.", + "description": "MemberChangedHookMsg should be de/serialized under `MemberChangedHook()` variant in a HandleMsg. This contains a list of all diffs on the given transaction.", "type": "object", "required": [ "diffs" diff --git a/contracts/cw3-flex-multisig/src/contract.rs b/contracts/cw3-flex-multisig/src/contract.rs index 40401724e..aea23fb82 100644 --- a/contracts/cw3-flex-multisig/src/contract.rs +++ b/contracts/cw3-flex-multisig/src/contract.rs @@ -457,7 +457,7 @@ mod tests { use cw0::Duration; use cw2::{query_contract_info, ContractVersion}; - use cw4::Member; + use cw4::{Cw4HandleMsg, Member}; use cw4_group::helpers::Cw4GroupContract; use cw_multi_test::{next_block, App, Contract, ContractWrapper, SimpleBank}; @@ -560,7 +560,7 @@ mod tests { // 3. (Optional) Set the multisig as the group owner if multisig_as_group_admin { - let update_admin = cw4_group::msg::HandleMsg::UpdateAdmin { + let update_admin = Cw4HandleMsg::UpdateAdmin { admin: Some(flex_addr.clone()), }; app.execute_contract(OWNER, &group_addr, &update_admin, &[]) diff --git a/contracts/cw4-stake/src/contract.rs b/contracts/cw4-stake/src/contract.rs index 064092e42..0040b260c 100644 --- a/contracts/cw4-stake/src/contract.rs +++ b/contracts/cw4-stake/src/contract.rs @@ -100,16 +100,19 @@ pub fn handle_bond( // NOTE: those clones are not needed (if we move denom, we return early), // but the compiler cannot see that (yet...) let sent = match info.sent_funds.len() { - 0 => Err(ContractError::MissingDenom(cfg.denom.clone())), + 0 => Err(ContractError::NoFunds {}), 1 => { if info.sent_funds[0].denom == cfg.denom { Ok(info.sent_funds[0].amount) } else { - Err(ContractError::ExtraDenoms(cfg.denom.clone())) + Err(ContractError::MissingDenom(cfg.denom.clone())) } } _ => Err(ContractError::ExtraDenoms(cfg.denom.clone())), }?; + if sent.is_zero() { + return Err(ContractError::NoFunds {}); + } // update the sender's stake let sender_raw = deps.api.canonical_address(&info.sender)?; @@ -181,6 +184,12 @@ fn update_membership( // update their membership weight let new = calc_weight(new_stake, cfg); let old = MEMBERS.may_load(storage, sender_raw)?; + + // short-circuit if no change + if new == old { + return Ok(vec![]); + } + // otherwise, record change of weight match new.as_ref() { Some(w) => MEMBERS.save(storage, sender_raw, w, height), None => MEMBERS.remove(storage, sender_raw, height), @@ -973,7 +982,7 @@ mod tests { let info = mock_info(HumanAddr::from(USER1), &[]); let err = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap_err(); match err { - ContractError::MissingDenom(denom) => assert_eq!(denom.as_str(), DENOM), + ContractError::NoFunds {} => {} _ => panic!("Unexpected error: {}", err), } @@ -981,7 +990,7 @@ mod tests { let info = mock_info(HumanAddr::from(USER1), &[coin(500, "FOO")]); let err = handle(deps.as_mut(), mock_env(), info, HandleMsg::Bond {}).unwrap_err(); match err { - ContractError::ExtraDenoms(denom) => assert_eq!(denom.as_str(), DENOM), + ContractError::MissingDenom(denom) => assert_eq!(denom.as_str(), DENOM), _ => panic!("Unexpected error: {}", err), } diff --git a/contracts/cw4-stake/src/error.rs b/contracts/cw4-stake/src/error.rs index 017fc0df1..12b414b9b 100644 --- a/contracts/cw4-stake/src/error.rs +++ b/contracts/cw4-stake/src/error.rs @@ -22,4 +22,7 @@ pub enum ContractError { #[error("Sent unsupported denoms, must send '{0}' to stake")] ExtraDenoms(String), + + #[error("No funds sent")] + NoFunds {}, }