diff --git a/contracts/cw3-flex-multisig/src/contract.rs b/contracts/cw3-flex-multisig/src/contract.rs index b46351ba7..26f01c855 100644 --- a/contracts/cw3-flex-multisig/src/contract.rs +++ b/contracts/cw3-flex-multisig/src/contract.rs @@ -16,10 +16,7 @@ use cw_storage_plus::Bound; use crate::error::ContractError; use crate::msg::{HandleMsg, InitMsg, QueryMsg}; -use crate::snapshot::{snapshot_diff, snapshoted_weight}; -use crate::state::{ - max_proposal_height, next_id, parse_id, proposals, Ballot, Config, Proposal, BALLOTS, CONFIG, -}; +use crate::state::{next_id, parse_id, Ballot, Config, Proposal, BALLOTS, CONFIG, PROPOSALS}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw3-flex-multisig"; @@ -129,7 +126,7 @@ pub fn handle_propose( required_weight: cfg.required_weight, }; let id = next_id(deps.storage)?; - proposals().save(deps.storage, id.into(), &prop)?; + PROPOSALS.save(deps.storage, id.into(), &prop)?; // add the first yes vote from voter let ballot = Ballot { @@ -162,7 +159,7 @@ pub fn handle_vote( let cfg = CONFIG.load(deps.storage)?; // ensure proposal exists and can be voted on - let mut prop = proposals().load(deps.storage, proposal_id.into())?; + let mut prop = PROPOSALS.load(deps.storage, proposal_id.into())?; if prop.status != Status::Open { return Err(ContractError::NotOpen {}); } @@ -171,13 +168,10 @@ pub fn handle_vote( } // use a snapshot of "start of proposal" if available, otherwise, current group weight - let vote_power = snapshoted_weight( - deps.as_ref(), - &raw_sender, - prop.start_height, - &cfg.group_addr, - )? - .ok_or_else(|| ContractError::Unauthorized {})?; + let vote_power = cfg + .group_addr + .member_at_height(&deps.querier, info.sender.clone(), prop.start_height)? + .ok_or_else(|| ContractError::Unauthorized {})?; // cast vote if no vote previously cast BALLOTS.update( @@ -199,7 +193,7 @@ pub fn handle_vote( if prop.yes_weight >= prop.required_weight { prop.status = Status::Passed; } - proposals().save(deps.storage, proposal_id.into(), &prop)?; + PROPOSALS.save(deps.storage, proposal_id.into(), &prop)?; } Ok(HandleResponse { @@ -222,7 +216,7 @@ pub fn handle_execute( ) -> Result { // anyone can trigger this if the vote passed - let mut prop = proposals().load(deps.storage, proposal_id.into())?; + let mut prop = PROPOSALS.load(deps.storage, proposal_id.into())?; // we allow execution even after the proposal "expiration" as long as all vote come in before // that point. If it was approved on time, it can be executed any time. if prop.status != Status::Passed { @@ -231,7 +225,7 @@ pub fn handle_execute( // set it to executed prop.status = Status::Executed; - proposals().save(deps.storage, proposal_id.into(), &prop)?; + PROPOSALS.save(deps.storage, proposal_id.into(), &prop)?; // dispatch all proposed messages Ok(HandleResponse { @@ -253,7 +247,7 @@ pub fn handle_close( ) -> Result, ContractError> { // anyone can trigger this if the vote passed - let mut prop = proposals().load(deps.storage, proposal_id.into())?; + let mut prop = PROPOSALS.load(deps.storage, proposal_id.into())?; if [Status::Executed, Status::Rejected, Status::Passed] .iter() .any(|x| *x == prop.status) @@ -266,7 +260,7 @@ pub fn handle_close( // set it to failed prop.status = Status::Rejected; - proposals().save(deps.storage, proposal_id.into(), &prop)?; + PROPOSALS.save(deps.storage, proposal_id.into(), &prop)?; Ok(HandleResponse { messages: vec![], @@ -280,28 +274,18 @@ pub fn handle_close( } pub fn handle_membership_hook( - mut deps: DepsMut, - env: Env, + deps: DepsMut, + _env: Env, info: MessageInfo, - diffs: Vec, + _diffs: Vec, ) -> Result, ContractError> { - // this must be called with the same group contract + // This is now a no-op + // But we leave the authorization check as a demo let cfg = CONFIG.load(deps.storage)?; if info.sender != cfg.group_addr.0 { return Err(ContractError::Unauthorized {}); } - // find the latest snapshot height - let max_height = max_proposal_height(deps.storage)?; - - // only try snapshot if there is an open proposal - if let Some(last_height) = max_height { - // save the diff if we have no diff on that account since last snapshot - for diff in diffs { - snapshot_diff(deps.branch(), diff, env.block.height, last_height)?; - } - } - Ok(HandleResponse::default()) } @@ -339,7 +323,7 @@ fn query_threshold(deps: Deps) -> StdResult { } fn query_proposal(deps: Deps, env: Env, id: u64) -> StdResult { - let prop = proposals().load(deps.storage, id.into())?; + let prop = PROPOSALS.load(deps.storage, id.into())?; let status = prop.current_status(&env.block); Ok(ProposalResponse { id, @@ -363,7 +347,7 @@ fn list_proposals( ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let start = start_after.map(Bound::exclusive_int); - let props: StdResult> = proposals() + let props: StdResult> = PROPOSALS .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|p| map_proposal(&env.block, p)) @@ -380,7 +364,7 @@ fn reverse_proposals( ) -> StdResult { let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; let end = start_before.map(Bound::exclusive_int); - let props: StdResult> = proposals() + let props: StdResult> = PROPOSALS .range(deps.storage, None, end, Order::Descending) .take(limit) .map(|p| map_proposal(&env.block, p)) @@ -474,7 +458,7 @@ mod tests { use cw0::Duration; use cw2::{query_contract_info, ContractVersion}; use cw4::{Cw4HandleMsg, Member}; - use cw_multi_test::{App, Contract, ContractWrapper, SimpleBank}; + use cw_multi_test::{next_block, App, Contract, ContractWrapper, SimpleBank}; use super::*; @@ -567,24 +551,20 @@ mod tests { member(VOTER5, 5), ]; let group_addr = init_group(app, members); + app.update_block(next_block); // 2. Set up Multisig backed by this group let flex_addr = init_flex(app, group_addr.clone(), required_weight, max_voting_period); + app.update_block(next_block); - // 3. Register a hook to the multisig on the group contract - let add_hook = Cw4HandleMsg::AddHook { - addr: flex_addr.clone(), - }; - app.execute_contract(OWNER, &group_addr, &add_hook, &[]) - .unwrap(); - - // 4. (Optional) Set the multisig as the group owner + // 3. (Optional) Set the multisig as the group owner if multisig_as_group_admin { let update_admin = Cw4HandleMsg::UpdateAdmin { admin: Some(flex_addr.clone()), }; app.execute_contract(OWNER, &group_addr, &update_admin, &[]) .unwrap(); + app.update_block(next_block); } // Bonus: set some funds on the multisig contract for future proposals diff --git a/contracts/cw3-flex-multisig/src/lib.rs b/contracts/cw3-flex-multisig/src/lib.rs index 198be7dab..33ecec4df 100644 --- a/contracts/cw3-flex-multisig/src/lib.rs +++ b/contracts/cw3-flex-multisig/src/lib.rs @@ -1,7 +1,6 @@ pub mod contract; mod error; pub mod msg; -mod snapshot; pub mod state; #[cfg(all(target_arch = "wasm32", not(feature = "library")))] diff --git a/contracts/cw3-flex-multisig/src/snapshot.rs b/contracts/cw3-flex-multisig/src/snapshot.rs deleted file mode 100644 index 9fd3f1470..000000000 --- a/contracts/cw3-flex-multisig/src/snapshot.rs +++ /dev/null @@ -1,61 +0,0 @@ -use cosmwasm_std::{CanonicalAddr, Deps, DepsMut, Order, StdResult, Storage}; -use cw3::VoterInfo; -use cw4::{Cw4Contract, MemberDiff}; -use cw_storage_plus::{Bound, Map, U64Key}; - -/// SNAPSHOTS pk: (HumanAddr, height) -> get the weight -/// Use VoterInfo, so None (no data) is different than VoterInfo{weight: None} (record it was removed) -pub const SNAPSHOTS: Map<(&[u8], U64Key), VoterInfo> = Map::new(b"snapshots"); - -/// load the weight from the snapshot - that is, first change >= height, -/// or query the current contract state otherwise -pub fn snapshoted_weight( - deps: Deps, - addr: &CanonicalAddr, - height: u64, - group: &Cw4Contract, -) -> StdResult> { - let snapshot = load_snapshot(deps.storage, &addr, height)?; - match snapshot { - // use snapshot if available - Some(info) => Ok(info.weight), - // otherwise load from the group - None => group.is_member(&deps.querier, &addr), - } -} - -/// saves this diff only if no updates have been saved since the latest snapshot -pub fn snapshot_diff( - deps: DepsMut, - diff: MemberDiff, - current_height: u64, - latest_snapshot_height: u64, -) -> StdResult<()> { - let raw_addr = deps.api.canonical_address(&diff.key)?; - match load_snapshot(deps.storage, &raw_addr, latest_snapshot_height)? { - Some(_) => Ok(()), - None => SNAPSHOTS.save( - deps.storage, - (&raw_addr, current_height.into()), - &VoterInfo { weight: diff.old }, - ), - } -} - -/// this will look for the first snapshot of the given address >= given height -/// If None, there is no snapshot since that time. -fn load_snapshot( - storage: &dyn Storage, - addr: &CanonicalAddr, - height: u64, -) -> StdResult> { - let start = Bound::inclusive(U64Key::new(height)); - let first = SNAPSHOTS - .prefix(&addr) - .range(storage, Some(start), None, Order::Ascending) - .next(); - match first { - None => Ok(None), - Some(r) => r.map(|(_, v)| Some(v)), - } -} diff --git a/contracts/cw3-flex-multisig/src/state.rs b/contracts/cw3-flex-multisig/src/state.rs index eda4cca4a..5a2fb7975 100644 --- a/contracts/cw3-flex-multisig/src/state.rs +++ b/contracts/cw3-flex-multisig/src/state.rs @@ -2,14 +2,12 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::convert::TryInto; -use cosmwasm_std::{BlockInfo, CosmosMsg, Empty, Order, StdError, StdResult, Storage}; +use cosmwasm_std::{BlockInfo, CosmosMsg, Empty, StdError, StdResult, Storage}; use cw0::{Duration, Expiration}; use cw3::{Status, Vote}; use cw4::Cw4Contract; -use cw_storage_plus::{ - range_with_prefix, Index, IndexList, IndexedMap, Item, Map, MultiIndex, Prefix, U64Key, -}; +use cw_storage_plus::{Item, Map, U64Key}; #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct Config { @@ -34,7 +32,6 @@ pub struct Proposal { } impl Proposal { - /// TODO: we should get the current BlockInfo and then we can determine this a bit better pub fn current_status(&self, block: &BlockInfo) -> Status { let mut status = self.status; @@ -50,38 +47,6 @@ impl Proposal { } } -pub fn max_proposal_height(storage: &dyn Storage) -> StdResult> { - // we grab the last height under Status::Open s O(1) - // unfortunately there is no good API for it, we have to reverse the format of MultiIndex - // it uses `Map<'a, (&'a [u8], &'a [u8]), u32>` with namespace b"proposals__status" - // keys there are formed from (b"proposals__status", index, pk) with the first 2 length-prefixed - // - // we know that index is always 9 bytes, and if we try to query with just the status byte it has - // the wrong length-prefix. - // We can find the prefix for status_height_index(h=0) (with proper length prefix) - // then trim off the last 8 bytes (height), and do a range_prefix query to scan the first value in that space - // ooff... do not try this at home. One day I will add an API for it in storage-plus - let prefix = Prefix::::new( - b"proposals__status", - &[&status_height_index(Status::Open, 0)], - ); - let cutoff = prefix.len() - 8; - let raw_prefix = &prefix[..cutoff]; - - let last = range_with_prefix(storage, raw_prefix, None, None, Order::Descending).next(); - let res = match last { - Some((k, _)) => { - // k is big-endian encoding of u64 (first 8 bytes) - let fixed: [u8; 8] = k[..8].try_into().map_err(|e| { - StdError::generic_err(format!("wrong length for k: {} - {}", k.len(), e)) - })?; - Some(u64::from_be_bytes(fixed)) - } - None => None, - }; - Ok(res) -} - // we cast a ballot with our chosen vote and a given weight // stored under the key that voted #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] @@ -96,6 +61,7 @@ pub const PROPOSAL_COUNT: Item = Item::new(b"proposal_count"); // multiple-item map pub const BALLOTS: Map<(U64Key, &[u8]), Ballot> = Map::new(b"votes"); +pub const PROPOSALS: Map = Map::new(b"proposals"); pub fn next_id(store: &mut dyn Storage) -> StdResult { let id: u64 = PROPOSAL_COUNT.may_load(store)?.unwrap_or_default() + 1; @@ -111,35 +77,3 @@ pub fn parse_id(data: &[u8]) -> StdResult { )), } } - -// pub const PROPOSALS: Map = Map::new(b"proposals"); - -pub struct ProposalIndexes<'a> { - pub status: MultiIndex<'a, Proposal>, -} - -impl<'a> IndexList for ProposalIndexes<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.status]; - Box::new(v.into_iter()) - } -} - -/// Returns a value that can be used as a secondary index key in the proposals map -pub fn status_height_index(status: Status, height: u64) -> Vec { - let mut idx = vec![status as u8]; - idx.extend_from_slice(&height.to_be_bytes()); - idx -} - -// secondary indexes on state for PROPOSALS to find all open proposals efficiently -pub fn proposals<'a>() -> IndexedMap<'a, U64Key, Proposal, ProposalIndexes<'a>> { - let indexes = ProposalIndexes { - status: MultiIndex::new( - |p| status_height_index(p.status, p.start_height), - b"proposals", - b"proposals__status", - ), - }; - IndexedMap::new(b"proposals", indexes) -} diff --git a/contracts/cw4-group/schema/query_msg.json b/contracts/cw4-group/schema/query_msg.json index 94c694c33..f8461d9a3 100644 --- a/contracts/cw4-group/schema/query_msg.json +++ b/contracts/cw4-group/schema/query_msg.json @@ -73,6 +73,14 @@ "properties": { "addr": { "$ref": "#/definitions/HumanAddr" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } } } diff --git a/contracts/cw4-group/src/contract.rs b/contracts/cw4-group/src/contract.rs index 3d4739058..4435df2b6 100644 --- a/contracts/cw4-group/src/contract.rs +++ b/contracts/cw4-group/src/contract.rs @@ -20,15 +20,20 @@ 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)?; + 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) -> StdResult<()> { +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)?; @@ -36,7 +41,7 @@ pub fn create(deps: DepsMut, admin: Option, members: Vec) -> for member in members.into_iter() { total += member.weight; let raw = deps.api.canonical_address(&member.addr)?; - MEMBERS.save(deps.storage, &raw, &member.weight)?; + MEMBERS.save(deps.storage, &raw, &member.weight, height)?; } TOTAL.save(deps.storage, &total)?; @@ -46,13 +51,15 @@ pub fn create(deps: DepsMut, admin: Option, members: Vec) -> // 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, + 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, info, add, remove), + 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), } @@ -83,12 +90,13 @@ pub fn update_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(), info.sender, add, remove)?; + 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() { @@ -101,6 +109,7 @@ pub fn handle_update_members( // 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, @@ -114,7 +123,7 @@ pub fn update_members( // 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, |old| -> StdResult<_> { + 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))); @@ -129,7 +138,7 @@ pub fn update_members( if let Some(weight) = old { diffs.push(MemberDiff::new(remove, Some(weight), None)); total -= weight; - MEMBERS.remove(deps.storage, &raw); + MEMBERS.remove(deps.storage, &raw, height)?; } } @@ -191,7 +200,10 @@ pub fn handle_remove_hook( pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Member { addr } => to_binary(&query_member(deps, addr)?), + 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)?) } @@ -217,9 +229,12 @@ fn query_total_weight(deps: Deps) -> StdResult { Ok(TotalWeightResponse { weight }) } -fn query_member(deps: Deps, addr: HumanAddr) -> StdResult { +fn query_member(deps: Deps, addr: HumanAddr, height: Option) -> StdResult { let raw = deps.api.canonical_address(&addr)?; - let weight = MEMBERS.may_load(deps.storage, &raw)?; + 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 }) } @@ -331,13 +346,13 @@ mod tests { let mut deps = mock_dependencies(&[]); do_init(deps.as_mut()); - let member1 = query_member(deps.as_ref(), USER1.into()).unwrap(); + 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()).unwrap(); + 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()).unwrap(); + 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(); @@ -350,27 +365,31 @@ mod tests { user1_weight: Option, user2_weight: Option, user3_weight: Option, + height: Option, ) { - let member1 = query_member(deps.as_ref(), USER1.into()).unwrap(); + 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()).unwrap(); + 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()).unwrap(); + let member3 = query_member(deps.as_ref(), USER3.into(), height).unwrap(); assert_eq!(member3.weight, user3_weight); - // 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(); + // 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()); + // TODO: more detailed compare? + let members = list_members(deps.as_ref(), None, None).unwrap(); + assert_eq!(count, members.members.len()); - let total = query_total_weight(deps.as_ref()).unwrap(); - assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 + let total = query_total_weight(deps.as_ref()).unwrap(); + assert_eq!(sum, total.weight); // 17 - 11 + 15 = 21 + } } #[test] @@ -386,16 +405,35 @@ mod tests { let remove = vec![USER1.into()]; // non-admin cannot update - let err = - update_members(deps.as_mut(), USER1.into(), add.clone(), remove.clone()).unwrap_err(); + 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(), ADMIN.into(), add, remove).unwrap(); - assert_users(&deps, None, Some(6), Some(15)); + 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] @@ -412,8 +450,9 @@ mod tests { let remove = vec![USER3.into()]; // admin updates properly - update_members(deps.as_mut(), ADMIN.into(), add, remove).unwrap(); - assert_users(&deps, Some(4), Some(6), None); + 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] @@ -436,8 +475,9 @@ mod tests { let remove = vec![USER1.into()]; // admin updates properly - update_members(deps.as_mut(), ADMIN.into(), add, remove).unwrap(); - assert_users(&deps, None, Some(6), Some(5)); + 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] @@ -586,9 +626,9 @@ mod tests { let msg = HandleMsg::UpdateMembers { remove, add }; // admin updates properly - assert_users(&deps, Some(11), Some(6), None); + 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)); + assert_users(&deps, Some(20), None, Some(5), None); // ensure 2 messages for the 2 hooks assert_eq!(res.messages.len(), 2); diff --git a/contracts/cw4-group/src/state.rs b/contracts/cw4-group/src/state.rs index ab8c5bf09..c2bcf66f1 100644 --- a/contracts/cw4-group/src/state.rs +++ b/contracts/cw4-group/src/state.rs @@ -1,10 +1,14 @@ use cosmwasm_std::{CanonicalAddr, HumanAddr}; -use cw4::{MEMBERS_KEY, TOTAL_KEY}; -use cw_storage_plus::{Item, Map}; +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); -pub const MEMBERS: Map<&[u8], u64> = Map::new(MEMBERS_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/packages/cw4/README.md b/packages/cw4/README.md index 2aeb31500..722708b56 100644 --- a/packages/cw4/README.md +++ b/packages/cw4/README.md @@ -56,10 +56,13 @@ problem, but we cover how to instantiate that in `TotalWeight{}` - Returns the total weight of all current members, this is very useful if some conditions are defined on a "percentage of members". -`Member{addr}` - Returns the weight of this voter if they are a member of the +`Member{addr, height}` - Returns the weight of this voter if they are a member of the group (may be 0), or `None` if they are not a member of the group. + If height is set and the cw4 implementation supports snapshots, + this will return the weight of that member at + the beginning of the block with the given height. - `MemberList{start_after, limit}` - Allows us to paginate over the list +`MemberList{start_after, limit}` - Allows us to paginate over the list of all members. 0-weight members will be included. Removed members will not. ### Raw diff --git a/packages/cw4/schema/cw4_query_msg.json b/packages/cw4/schema/cw4_query_msg.json index 46cb89e3c..7b1b3abc6 100644 --- a/packages/cw4/schema/cw4_query_msg.json +++ b/packages/cw4/schema/cw4_query_msg.json @@ -73,6 +73,14 @@ "properties": { "addr": { "$ref": "#/definitions/HumanAddr" + }, + "at_height": { + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } } } diff --git a/packages/cw4/src/helpers.rs b/packages/cw4/src/helpers.rs index e515b212b..2d9b5b55a 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, AdminResponse, Cw4QueryMsg, Member, MemberListResponse, 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 @@ -128,6 +130,21 @@ impl Cw4Contract { } } + /// Return the member's weight at the given snapshot - requires a smart query + pub fn member_at_height( + &self, + querier: &QuerierWrapper, + member: HumanAddr, + height: u64, + ) -> StdResult> { + let query = self.encode_smart_query(Cw4QueryMsg::Member { + addr: member, + at_height: Some(height), + })?; + let res: MemberResponse = querier.query(&query)?; + Ok(res.weight) + } + pub fn list_members( &self, querier: &QuerierWrapper, diff --git a/packages/cw4/src/query.rs b/packages/cw4/src/query.rs index 077547f73..a85fed92f 100644 --- a/packages/cw4/src/query.rs +++ b/packages/cw4/src/query.rs @@ -18,7 +18,10 @@ pub enum Cw4QueryMsg { limit: Option, }, /// Returns MemberResponse - Member { addr: HumanAddr }, + Member { + addr: HumanAddr, + at_height: Option, + }, /// Shows all registered hooks. Returns HooksResponse. Hooks {}, } diff --git a/packages/storage-plus/src/snapshot.rs b/packages/storage-plus/src/snapshot.rs index ca13bab7f..756e17edd 100644 --- a/packages/storage-plus/src/snapshot.rs +++ b/packages/storage-plus/src/snapshot.rs @@ -269,6 +269,7 @@ pub struct SnapshotNamespaces<'a> { #[macro_export] macro_rules! snapshot_names { ($var:expr) => { + #[allow(clippy::string_lit_as_bytes)] SnapshotNamespaces { pk: $var.as_bytes(), checkpoints: concat!($var, "__checkpoints").as_bytes(), @@ -284,10 +285,15 @@ mod tests { #[test] fn namespace_macro() { - let names = snapshot_names!("demo"); - assert_eq!(names.pk, b"demo"); - assert_eq!(names.checkpoints, b"demo__checkpoints"); - assert_eq!(names.changelog, b"demo__changelog"); + let check = |names: SnapshotNamespaces| { + assert_eq!(names.pk, b"demo"); + assert_eq!(names.checkpoints, b"demo__checkpoints"); + assert_eq!(names.changelog, b"demo__changelog"); + }; + // FIXME: we have to do this weird way due to the clippy allow statement + check(snapshot_names!("demo")); + // ex. this line fails to compile + // let names = snapshot_names!("demo"); } type TestMap = SnapshotMap<'static, &'static [u8], u64>;