diff --git a/contracts/cw3-flex-multisig/src/contract.rs b/contracts/cw3-flex-multisig/src/contract.rs index 84c65c9fa..33694c0d9 100644 --- a/contracts/cw3-flex-multisig/src/contract.rs +++ b/contracts/cw3-flex-multisig/src/contract.rs @@ -89,9 +89,14 @@ pub fn execute_propose( // only members of the multisig can create a proposal let cfg = CONFIG.load(deps.storage)?; + // Only members of the multisig can create a proposal + // Non-voting members are special - they are allowed to create a proposal and + // therefore "vote", but they aren't allowed to vote otherwise. + // Such vote is also special, because despite having 0 weight it still counts when + // counting threshold passing let vote_power = cfg .group_addr - .is_member(&deps.querier, &info.sender)? + .is_member(&deps.querier, &info.sender, None)? .ok_or(ContractError::Unauthorized {})?; // max expires also used as default @@ -112,7 +117,7 @@ pub fn execute_propose( expires, msgs, status: Status::Open, - votes: Votes::new(vote_power), + votes: Votes::yes(vote_power), threshold: cfg.threshold, total_weight: cfg.group_addr.total_weight(&deps.querier)?, }; @@ -153,10 +158,12 @@ pub fn execute_vote( return Err(ContractError::Expired {}); } + // Only voting members of the multisig can vote + // Additional check if weight >= 1 // use a snapshot of "start of proposal" let vote_power = cfg .group_addr - .member_at_height(&deps.querier, info.sender.clone(), prop.start_height)? + .is_voting_member(&deps.querier, &info.sender, prop.start_height)? .ok_or(ContractError::Unauthorized {})?; // cast vote if no vote previously cast @@ -399,7 +406,7 @@ fn list_votes( fn query_voter(deps: Deps, voter: String) -> StdResult { let cfg = CONFIG.load(deps.storage)?; let voter_addr = deps.api.addr_validate(&voter)?; - let weight = cfg.group_addr.is_member(&deps.querier, &voter_addr)?; + let weight = cfg.group_addr.is_member(&deps.querier, &voter_addr, None)?; Ok(VoterResponse { weight }) } @@ -488,6 +495,7 @@ mod tests { .unwrap() } + #[track_caller] fn instantiate_flex( app: &mut App, group: Addr, @@ -507,6 +515,7 @@ mod tests { // this will set up both contracts, instantiating the group with // all voters defined above, and the multisig pointing to it and given threshold criteria. // Returns (multisig address, group address). + #[track_caller] fn setup_test_case_fixed( app: &mut App, weight_needed: u64, @@ -525,6 +534,7 @@ mod tests { ) } + #[track_caller] fn setup_test_case( app: &mut App, threshold: Threshold, @@ -538,7 +548,7 @@ mod tests { member(VOTER1, 1), member(VOTER2, 2), member(VOTER3, 3), - member(VOTER4, 4), + member(VOTER4, 12), // so that he alone can pass a 50 / 52% threshold proposal member(VOTER5, 5), ]; let group_addr = instantiate_group(app, members); @@ -605,7 +615,10 @@ mod tests { // Zero required weight fails let instantiate_msg = InstantiateMsg { group_addr: group_addr.to_string(), - threshold: Threshold::AbsoluteCount { weight: 0 }, + threshold: Threshold::ThresholdQuorum { + threshold: Decimal::zero(), + quorum: Decimal::percent(1), + }, max_voting_period, }; let err = app @@ -618,7 +631,7 @@ mod tests { None, ) .unwrap_err(); - assert_eq!(ContractError::ZeroThreshold {}, err.downcast().unwrap()); + assert_eq!(ContractError::InvalidThreshold {}, err.downcast().unwrap()); // Total weight less than required weight not allowed let instantiate_msg = InstantiateMsg { @@ -636,10 +649,7 @@ mod tests { None, ) .unwrap_err(); - assert_eq!( - ContractError::UnreachableThreshold {}, - err.downcast().unwrap() - ); + assert_eq!(ContractError::UnreachableWeight {}, err.downcast().unwrap()); // All valid let instantiate_msg = InstantiateMsg { @@ -798,10 +808,12 @@ mod tests { let init_funds = coins(10, "BTC"); let mut app = mock_app(&init_funds); - let required_weight = 3; let voting_period = Duration::Time(2000000); - let (flex_addr, _) = - setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, false); + let threshold = Threshold::ThresholdQuorum { + threshold: Decimal::percent(80), + quorum: Decimal::percent(20), + }; + let (flex_addr, _) = setup_test_case(&mut app, threshold, voting_period, init_funds, false); // create proposal with 1 vote power let proposal = pay_somebody_proposal(); @@ -814,7 +826,7 @@ mod tests { app.update_block(next_block); let proposal = pay_somebody_proposal(); let res = app - .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[]) + .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &proposal, &[]) .unwrap(); let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -877,9 +889,10 @@ mod tests { msgs, expires: voting_period.after(&proposed_at), status: Status::Open, - threshold: ThresholdResponse::AbsoluteCount { - weight: 3, - total_weight: 15, + threshold: ThresholdResponse::ThresholdQuorum { + total_weight: 23, + threshold: Decimal::percent(80), + quorum: Decimal::percent(20), }, }; assert_eq!(&expected, &res.proposals[0]); @@ -890,10 +903,12 @@ mod tests { let init_funds = coins(10, "BTC"); let mut app = mock_app(&init_funds); - let required_weight = 3; + let threshold = Threshold::ThresholdQuorum { + threshold: Decimal::percent(51), + quorum: Decimal::percent(1), + }; let voting_period = Duration::Time(2000000); - let (flex_addr, _) = - setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, false); + let (flex_addr, _) = setup_test_case(&mut app, threshold, voting_period, init_funds, false); // create proposal with 0 vote power let proposal = pay_somebody_proposal(); @@ -904,7 +919,7 @@ mod tests { // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); - // Owner cannot vote (again) + // Owner with 0 voting power cannot vote let yes_vote = ExecuteMsg::Vote { proposal_id, vote: Vote::Yes, @@ -912,7 +927,7 @@ mod tests { let err = app .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &yes_vote, &[]) .unwrap_err(); - assert_eq!(ContractError::AlreadyVoted {}, err.downcast().unwrap()); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); // Only voters can vote let err = app @@ -934,6 +949,12 @@ mod tests { ], ); + // VOTER1 cannot vote again + let err = app + .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &yes_vote, &[]) + .unwrap_err(); + assert_eq!(ContractError::AlreadyVoted {}, err.downcast().unwrap()); + // No/Veto votes have no effect on the tally // Compute the current tally let tally = get_tally(&app, flex_addr.as_ref(), proposal_id); @@ -1038,10 +1059,12 @@ mod tests { let init_funds = coins(10, "BTC"); let mut app = mock_app(&init_funds); - let required_weight = 3; + let threshold = Threshold::ThresholdQuorum { + threshold: Decimal::percent(51), + quorum: Decimal::percent(1), + }; let voting_period = Duration::Time(2000000); - let (flex_addr, _) = - setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, true); + let (flex_addr, _) = setup_test_case(&mut app, threshold, voting_period, init_funds, true); // ensure we have cash to cover the proposal let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap(); @@ -1072,13 +1095,13 @@ mod tests { vote: Vote::Yes, }; let res = app - .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &vote, &[]) + .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[]) .unwrap(); assert_eq!( res.custom_attrs(1), [ ("action", "vote"), - ("sender", VOTER3), + ("sender", VOTER4), ("proposal_id", proposal_id.to_string().as_str()), ("status", "Passed"), ], @@ -1127,10 +1150,12 @@ mod tests { let init_funds = coins(10, "BTC"); let mut app = mock_app(&init_funds); - let required_weight = 3; + let threshold = Threshold::ThresholdQuorum { + threshold: Decimal::percent(51), + quorum: Decimal::percent(1), + }; let voting_period = Duration::Height(2000000); - let (flex_addr, _) = - setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, true); + let (flex_addr, _) = setup_test_case(&mut app, threshold, voting_period, init_funds, true); // create proposal with 0 vote power let proposal = pay_somebody_proposal(); @@ -1176,10 +1201,13 @@ mod tests { let init_funds = coins(10, "BTC"); let mut app = mock_app(&init_funds); - let required_weight = 4; + let threshold = Threshold::ThresholdQuorum { + threshold: Decimal::percent(51), + quorum: Decimal::percent(1), + }; let voting_period = Duration::Time(20000); let (flex_addr, group_addr) = - setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, false); + setup_test_case(&mut app, threshold, voting_period, init_funds, false); // VOTER1 starts a proposal to send some tokens (1/4 votes) let proposal = pay_somebody_proposal(); @@ -1205,9 +1233,10 @@ mod tests { .wrap() .query_wasm_smart(&flex_addr, &QueryMsg::Threshold {}) .unwrap(); - let expected_thresh = ThresholdResponse::AbsoluteCount { - weight: 4, - total_weight: 15, + let expected_thresh = ThresholdResponse::ThresholdQuorum { + total_weight: 23, + threshold: Decimal::percent(51), + quorum: Decimal::percent(1), }; assert_eq!(expected_thresh, threshold); @@ -1215,13 +1244,13 @@ mod tests { app.update_block(|block| block.height += 2); // admin changes the group - // updates VOTER2 power to 7 -> with snapshot, vote doesn't pass proposal + // updates VOTER2 power to 21 -> with snapshot, vote doesn't pass proposal // adds NEWBIE with 2 power -> with snapshot, invalid vote - // removes VOTER3 -> with snapshot, can vote and pass proposal + // removes VOTER3 -> with snapshot, can vote on proposal let newbie: &str = "newbie"; let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers { remove: vec![VOTER3.into()], - add: vec![member(VOTER2, 7), member(newbie, 2)], + add: vec![member(VOTER2, 21), member(newbie, 2)], }; app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[]) .unwrap(); @@ -1277,16 +1306,16 @@ mod tests { // previously removed VOTER3 can still vote, passing the proposal app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[]) .unwrap(); - assert_eq!(prop_status(&app, proposal_id), Status::Passed); // check current threshold (global) is updated let threshold: ThresholdResponse = app .wrap() .query_wasm_smart(&flex_addr, &QueryMsg::Threshold {}) .unwrap(); - let expected_thresh = ThresholdResponse::AbsoluteCount { - weight: 4, - total_weight: 19, + let expected_thresh = ThresholdResponse::ThresholdQuorum { + total_weight: 41, + threshold: Decimal::percent(51), + quorum: Decimal::percent(1), }; assert_eq!(expected_thresh, threshold); @@ -1417,19 +1446,16 @@ mod tests { let init_funds = coins(10, "BTC"); let mut app = mock_app(&init_funds); - // 33% required, which is 5 of the initial 15 + // 51% required, which is 12 of the initial 24 + let threshold = Threshold::ThresholdQuorum { + threshold: Decimal::percent(51), + quorum: Decimal::percent(1), + }; let voting_period = Duration::Time(20000); - let (flex_addr, group_addr) = setup_test_case( - &mut app, - Threshold::AbsolutePercentage { - percentage: Decimal::percent(33), - }, - voting_period, - init_funds, - false, - ); + let (flex_addr, group_addr) = + setup_test_case(&mut app, threshold, voting_period, init_funds, false); - // VOTER3 starts a proposal to send some tokens (3/5 votes) + // VOTER3 starts a proposal to send some tokens (3/12 votes) let proposal = pay_somebody_proposal(); let res = app .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[]) @@ -1445,17 +1471,17 @@ mod tests { prop.status }; - // 3/5 votes + // 3/12 votes assert_eq!(prop_status(&app), Status::Open); // a few blocks later... app.update_block(|block| block.height += 2); - // admin changes the group (3 -> 0, 2 -> 7, 0 -> 15) - total = 32, require 11 to pass + // admin changes the group (3 -> 0, 2 -> 9, 0 -> 29) - total = 56, require 29 to pass let newbie: &str = "newbie"; let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers { remove: vec![VOTER3.into()], - add: vec![member(VOTER2, 7), member(newbie, 15)], + add: vec![member(VOTER2, 9), member(newbie, 29)], }; app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[]) .unwrap(); @@ -1463,15 +1489,15 @@ mod tests { // a few blocks later... app.update_block(|block| block.height += 3); - // VOTER2 votes according to original weights: 3 + 2 = 5 / 5 => Passed - // with updated weights, it would be 3 + 7 = 10 / 11 => Open + // VOTER2 votes according to original weights: 3 + 2 = 5 / 12 => Open + // with updated weights, it would be 3 + 9 = 12 / 12 => Passed let yes_vote = ExecuteMsg::Vote { proposal_id, vote: Vote::Yes, }; app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[]) .unwrap(); - assert_eq!(prop_status(&app), Status::Passed); + assert_eq!(prop_status(&app), Status::Open); // new proposal can be passed single-handedly by newbie let proposal = pay_somebody_proposal(); @@ -1498,13 +1524,13 @@ mod tests { let init_funds = coins(10, "BTC"); let mut app = mock_app(&init_funds); - // 33% required for quora, which is 5 of the initial 15 - // 50% yes required to pass early (8 of the initial 15) + // 33% required for quora, which is 8 of the initial 24 + // 50% yes required to pass early (12 of the initial 24) let voting_period = Duration::Time(20000); let (flex_addr, group_addr) = setup_test_case( &mut app, Threshold::ThresholdQuorum { - threshold: Decimal::percent(50), + threshold: Decimal::percent(51), quorum: Decimal::percent(33), }, voting_period, @@ -1528,17 +1554,17 @@ mod tests { prop.status }; - // 3/5 votes - not expired + // 3/12 votes - not expired assert_eq!(prop_status(&app), Status::Open); // a few blocks later... app.update_block(|block| block.height += 2); - // admin changes the group (3 -> 0, 2 -> 7, 0 -> 15) - total = 32, require 11 to pass + // admin changes the group (3 -> 0, 2 -> 9, 0 -> 28) - total = 55, require 28 to pass let newbie: &str = "newbie"; let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers { remove: vec![VOTER3.into()], - add: vec![member(VOTER2, 7), member(newbie, 15)], + add: vec![member(VOTER2, 9), member(newbie, 29)], }; app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[]) .unwrap(); @@ -1546,20 +1572,20 @@ mod tests { // a few blocks later... app.update_block(|block| block.height += 3); - // VOTER2 votes no, according to original weights: 3 yes, 2 no, 5 total (will pass when expired) - // with updated weights, it would be 3 yes, 7 no, 10 total (will fail when expired) + // VOTER2 votes yes, according to original weights: 3 yes, 2 no, 5 total (will fail when expired) + // with updated weights, it would be 3 yes, 9 yes, 11 total (will pass when expired) let yes_vote = ExecuteMsg::Vote { proposal_id, - vote: Vote::No, + vote: Vote::Yes, }; app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[]) .unwrap(); // not expired yet assert_eq!(prop_status(&app), Status::Open); - // wait until the vote is over, and see it was passed (met quorum, and threshold of voters) + // wait until the vote is over, and see it was rejected app.update_block(expire(voting_period)); - assert_eq!(prop_status(&app), Status::Passed); + assert_eq!(prop_status(&app), Status::Rejected); } #[test] diff --git a/contracts/cw3-flex-multisig/src/error.rs b/contracts/cw3-flex-multisig/src/error.rs index 2a965644c..0c104222a 100644 --- a/contracts/cw3-flex-multisig/src/error.rs +++ b/contracts/cw3-flex-multisig/src/error.rs @@ -6,11 +6,20 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), - #[error("Required threshold cannot be zero")] - ZeroThreshold {}, + #[error("Invalid voting threshold percentage, must be in the 0.5-1.0 range")] + InvalidThreshold {}, - #[error("Not possible to reach required (passing) threshold")] - UnreachableThreshold {}, + #[error("Required quorum threshold cannot be zero")] + ZeroQuorumThreshold {}, + + #[error("Not possible to reach required quorum threshold")] + UnreachableQuorumThreshold {}, + + #[error("Required weight cannot be zero")] + ZeroWeight {}, + + #[error("Not possible to reach required (passing) weight")] + UnreachableWeight {}, #[error("Group contract invalid address '{addr}'")] InvalidGroup { addr: String }, diff --git a/contracts/cw3-flex-multisig/src/msg.rs b/contracts/cw3-flex-multisig/src/msg.rs index 5fda0de91..57e90e757 100644 --- a/contracts/cw3-flex-multisig/src/msg.rs +++ b/contracts/cw3-flex-multisig/src/msg.rs @@ -49,22 +49,22 @@ impl Threshold { weight: weight_needed, } => { if *weight_needed == 0 { - Err(ContractError::ZeroThreshold {}) + Err(ContractError::ZeroWeight {}) } else if *weight_needed > total_weight { - Err(ContractError::UnreachableThreshold {}) + Err(ContractError::UnreachableWeight {}) } else { Ok(()) } } Threshold::AbsolutePercentage { percentage: percentage_needed, - } => valid_percentage(percentage_needed), + } => valid_threshold(percentage_needed), Threshold::ThresholdQuorum { threshold, quorum: quroum, } => { - valid_percentage(threshold)?; - valid_percentage(quroum) + valid_threshold(threshold)?; + valid_quorum(quroum) } } } @@ -91,12 +91,21 @@ impl Threshold { } } -/// Asserts that the 0.0 < percent <= 1.0 -fn valid_percentage(percent: &Decimal) -> Result<(), ContractError> { +/// Asserts that the 0.5 < percent <= 1.0 +fn valid_threshold(percent: &Decimal) -> Result<(), ContractError> { + if *percent > Decimal::percent(100) || *percent < Decimal::percent(50) { + Err(ContractError::InvalidThreshold {}) + } else { + Ok(()) + } +} + +/// Asserts that the 0.5 < percent <= 1.0 +fn valid_quorum(percent: &Decimal) -> Result<(), ContractError> { if percent.is_zero() { - Err(ContractError::ZeroThreshold {}) + Err(ContractError::ZeroQuorumThreshold {}) } else if *percent > Decimal::one() { - Err(ContractError::UnreachableThreshold {}) + Err(ContractError::UnreachableQuorumThreshold {}) } else { Ok(()) } @@ -167,33 +176,44 @@ mod tests { use super::*; #[test] - fn validate_percentage() { + fn validate_quorum_percentage() { // TODO: test the error messages // 0 is never a valid percentage - let err = valid_percentage(&Decimal::zero()).unwrap_err(); - assert_eq!(err.to_string(), ContractError::ZeroThreshold {}.to_string()); + let err = valid_quorum(&Decimal::zero()).unwrap_err(); + assert_eq!( + err.to_string(), + ContractError::ZeroQuorumThreshold {}.to_string() + ); // 100% is - valid_percentage(&Decimal::one()).unwrap(); + valid_quorum(&Decimal::one()).unwrap(); // 101% is not - let err = valid_percentage(&Decimal::percent(101)).unwrap_err(); + let err = valid_quorum(&Decimal::percent(101)).unwrap_err(); assert_eq!( err.to_string(), - ContractError::UnreachableThreshold {}.to_string() + ContractError::UnreachableQuorumThreshold {}.to_string() ); // not 100.1% - let err = valid_percentage(&Decimal::permille(1001)).unwrap_err(); + let err = valid_quorum(&Decimal::permille(1001)).unwrap_err(); assert_eq!( err.to_string(), - ContractError::UnreachableThreshold {}.to_string() + ContractError::UnreachableQuorumThreshold {}.to_string() ); + } - // other values in between 0 and 1 are valid - valid_percentage(&Decimal::permille(1)).unwrap(); - valid_percentage(&Decimal::percent(17)).unwrap(); - valid_percentage(&Decimal::percent(99)).unwrap(); + #[test] + fn validate_threshold_percentage() { + // other values in between 0.5 and 1 are valid + valid_threshold(&Decimal::percent(51)).unwrap(); + valid_threshold(&Decimal::percent(67)).unwrap(); + valid_threshold(&Decimal::percent(99)).unwrap(); + let err = valid_threshold(&Decimal::percent(101)).unwrap_err(); + assert_eq!( + err.to_string(), + ContractError::InvalidThreshold {}.to_string() + ); } #[test] @@ -203,13 +223,13 @@ mod tests { .validate(5) .unwrap_err(); // TODO: remove to_string() when PartialEq implemented - assert_eq!(err.to_string(), ContractError::ZeroThreshold {}.to_string()); + assert_eq!(err.to_string(), ContractError::ZeroWeight {}.to_string()); let err = Threshold::AbsoluteCount { weight: 6 } .validate(5) .unwrap_err(); assert_eq!( err.to_string(), - ContractError::UnreachableThreshold {}.to_string() + ContractError::UnreachableWeight {}.to_string() ); Threshold::AbsoluteCount { weight: 1 }.validate(5).unwrap(); @@ -221,7 +241,10 @@ mod tests { } .validate(5) .unwrap_err(); - assert_eq!(err.to_string(), ContractError::ZeroThreshold {}.to_string()); + assert_eq!( + err.to_string(), + ContractError::InvalidThreshold {}.to_string() + ); Threshold::AbsolutePercentage { percentage: Decimal::percent(51), } @@ -243,7 +266,7 @@ mod tests { .unwrap_err(); assert_eq!( err.to_string(), - ContractError::UnreachableThreshold {}.to_string() + ContractError::InvalidThreshold {}.to_string() ); let err = Threshold::ThresholdQuorum { threshold: Decimal::percent(51), @@ -251,7 +274,10 @@ mod tests { } .validate(5) .unwrap_err(); - assert_eq!(err.to_string(), ContractError::ZeroThreshold {}.to_string()); + assert_eq!( + err.to_string(), + ContractError::ZeroQuorumThreshold {}.to_string() + ); } #[test] diff --git a/contracts/cw3-flex-multisig/src/state.rs b/contracts/cw3-flex-multisig/src/state.rs index a31223f5c..ba2a9ef20 100644 --- a/contracts/cw3-flex-multisig/src/state.rs +++ b/contracts/cw3-flex-multisig/src/state.rs @@ -57,7 +57,7 @@ impl Votes { } /// create it with a yes vote for this much - pub fn new(init_weight: u64) -> Self { + pub fn yes(init_weight: u64) -> Self { Votes { yes: init_weight, no: 0, @@ -178,7 +178,7 @@ mod test { #[test] fn count_votes() { - let mut votes = Votes::new(5); + let mut votes = Votes::yes(5); votes.add_vote(Vote::No, 10); votes.add_vote(Vote::Veto, 20); votes.add_vote(Vote::Yes, 30); @@ -233,7 +233,7 @@ mod test { #[test] fn proposal_passed_absolute_count() { let fixed = Threshold::AbsoluteCount { weight: 10 }; - let mut votes = Votes::new(7); + let mut votes = Votes::yes(7); votes.add_vote(Vote::Veto, 4); // same expired or not, total_weight or whatever assert!(!check_is_passed(fixed.clone(), votes.clone(), 30, false)); @@ -249,7 +249,7 @@ mod test { let percent = Threshold::AbsolutePercentage { percentage: Decimal::percent(50), }; - let mut votes = Votes::new(7); + let mut votes = Votes::yes(7); votes.add_vote(Vote::No, 4); votes.add_vote(Vote::Abstain, 2); // same expired or not, if yes >= ceiling(0.5 * (total - abstained)) diff --git a/packages/cw4/src/helpers.rs b/packages/cw4/src/helpers.rs index e278409ea..93514a36c 100644 --- a/packages/cw4/src/helpers.rs +++ b/packages/cw4/src/helpers.rs @@ -74,21 +74,45 @@ impl Cw4Contract { Item::new(TOTAL_KEY).query(querier, self.addr()) } - /// Check if this address is a member, and if so, with which weight - pub fn is_member(&self, querier: &QuerierWrapper, addr: &Addr) -> StdResult> { - Map::new(MEMBERS_KEY).query(querier, self.addr(), addr) + /// Check if this address is a member and returns its weight + pub fn is_member( + &self, + querier: &QuerierWrapper, + member: &Addr, + height: Option, + ) -> StdResult> { + match height { + Some(height) => self.member_at_height(querier, member.to_string(), height.into()), + None => Map::new(MEMBERS_KEY).query(querier, self.addr(), member), + } + } + + /// Check if this address is a member, and if its weight is >= 1 + /// Returns member's weight in positive case + pub fn is_voting_member( + &self, + querier: &QuerierWrapper, + member: &Addr, + height: impl Into>, + ) -> StdResult> { + if let Some(weight) = self.member_at_height(querier, member.to_string(), height.into())? { + if weight >= 1 { + return Ok(Some(weight)); + } + } + Ok(None) } /// Return the member's weight at the given snapshot - requires a smart query - pub fn member_at_height>( + pub fn member_at_height( &self, querier: &QuerierWrapper, - member: T, - height: u64, + member: impl Into, + at_height: Option, ) -> StdResult> { let query = self.encode_smart_query(Cw4QueryMsg::Member { addr: member.into(), - at_height: Some(height), + at_height, })?; let res: MemberResponse = querier.query(&query)?; Ok(res.weight) diff --git a/packages/cw4/src/lib.rs b/packages/cw4/src/lib.rs index c06e1d733..9185b6962 100644 --- a/packages/cw4/src/lib.rs +++ b/packages/cw4/src/lib.rs @@ -11,11 +11,3 @@ pub use crate::query::{ MemberResponse, TotalWeightResponse, MEMBERS_CHANGELOG, MEMBERS_CHECKPOINTS, MEMBERS_KEY, TOTAL_KEY, }; - -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - // test me - } -}