Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Threshold and coexisting implementations into packages/utils #590

Merged
merged 1 commit into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions contracts/cw3-fixed-multisig/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ use cosmwasm_std::{

use cw2::set_contract_version;
use cw3::{
ProposalListResponse, ProposalResponse, Status, ThresholdResponse, Vote, VoteInfo,
VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse,
ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo, VoteListResponse, VoteResponse,
VoterDetail, VoterListResponse, VoterResponse,
};
use cw_storage_plus::Bound;
use utils::Expiration;
use utils::{Expiration, ThresholdResponse};

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
Expand Down
19 changes: 12 additions & 7 deletions contracts/cw3-flex-multisig/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ use cosmwasm_std::{

use cw2::set_contract_version;
use cw3::{
ProposalListResponse, ProposalResponse, Status, ThresholdResponse, Vote, VoteInfo,
VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse,
ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo, VoteListResponse, VoteResponse,
VoterDetail, VoterListResponse, VoterResponse,
};
use cw4::{Cw4Contract, MemberChangedHookMsg, MemberDiff};
use cw_storage_plus::Bound;
use utils::{maybe_addr, Expiration};
use utils::{maybe_addr, Expiration, ThresholdResponse};

use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
Expand Down Expand Up @@ -431,10 +431,9 @@ mod tests {
use cw4::{Cw4ExecuteMsg, Member};
use cw4_group::helpers::Cw4GroupContract;
use cw_multi_test::{next_block, App, AppBuilder, Contract, ContractWrapper, Executor};
use utils::Duration;
use utils::{Duration, Threshold};

use super::*;
use crate::msg::Threshold;

const OWNER: &str = "admin0001";
const VOTER1: &str = "voter0001";
Expand Down Expand Up @@ -625,7 +624,10 @@ mod tests {
None,
)
.unwrap_err();
assert_eq!(ContractError::InvalidThreshold {}, err.downcast().unwrap());
assert_eq!(
ContractError::Threshold(utils::ThresholdError::InvalidThreshold {}),
err.downcast().unwrap()
);

// Total weight less than required weight not allowed
let instantiate_msg = InstantiateMsg {
Expand All @@ -643,7 +645,10 @@ mod tests {
None,
)
.unwrap_err();
assert_eq!(ContractError::UnreachableWeight {}, err.downcast().unwrap());
assert_eq!(
ContractError::Threshold(utils::ThresholdError::UnreachableWeight {}),
err.downcast().unwrap()
);

// All valid
let instantiate_msg = InstantiateMsg {
Expand Down
18 changes: 4 additions & 14 deletions contracts/cw3-flex-multisig/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
use cosmwasm_std::StdError;
use utils::ThresholdError;

use thiserror::Error;

#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("{0}")]
Std(#[from] StdError),

#[error("Invalid voting threshold percentage, must be in the 0.5-1.0 range")]
InvalidThreshold {},

#[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("{0}")]
Threshold(#[from] ThresholdError),

#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
Expand Down
253 changes: 3 additions & 250 deletions contracts/cw3-flex-multisig/src/msg.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::error::ContractError;
use cosmwasm_std::{CosmosMsg, Decimal, Empty};
use cw3::{ThresholdResponse, Vote};
use cosmwasm_std::{CosmosMsg, Empty};
use cw3::Vote;
use cw4::MemberChangedHookMsg;
use utils::{Duration, Expiration};
use utils::{Duration, Expiration, Threshold};

#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct InstantiateMsg {
Expand All @@ -15,102 +14,6 @@ pub struct InstantiateMsg {
pub max_voting_period: Duration,
}

/// This defines the different ways tallies can happen.
///
/// The total_weight used for calculating success as well as the weights of each
/// individual voter used in tallying should be snapshotted at the beginning of
/// the block at which the proposal starts (this is likely the responsibility of a
/// correct cw4 implementation).
/// See also `ThresholdResponse` in the cw3 spec.
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Threshold {
/// Declares that a fixed weight of Yes votes is needed to pass.
/// See `ThresholdResponse.AbsoluteCount` in the cw3 spec for details.
AbsoluteCount { weight: u64 },

/// Declares a percentage of the total weight that must cast Yes votes in order for
/// a proposal to pass.
/// See `ThresholdResponse.AbsolutePercentage` in the cw3 spec for details.
AbsolutePercentage { percentage: Decimal },

/// Declares a `quorum` of the total votes that must participate in the election in order
/// for the vote to be considered at all.
/// See `ThresholdResponse.ThresholdQuorum` in the cw3 spec for details.
ThresholdQuorum { threshold: Decimal, quorum: Decimal },
}

impl Threshold {
/// returns error if this is an unreachable value,
/// given a total weight of all members in the group
pub fn validate(&self, total_weight: u64) -> Result<(), ContractError> {
match self {
Threshold::AbsoluteCount {
weight: weight_needed,
} => {
if *weight_needed == 0 {
Err(ContractError::ZeroWeight {})
} else if *weight_needed > total_weight {
Err(ContractError::UnreachableWeight {})
} else {
Ok(())
}
}
Threshold::AbsolutePercentage {
percentage: percentage_needed,
} => valid_threshold(percentage_needed),
Threshold::ThresholdQuorum {
threshold,
quorum: quroum,
} => {
valid_threshold(threshold)?;
valid_quorum(quroum)
}
}
}

/// Creates a response from the saved data, just missing the total_weight info
pub fn to_response(&self, total_weight: u64) -> ThresholdResponse {
match self.clone() {
Threshold::AbsoluteCount { weight } => ThresholdResponse::AbsoluteCount {
weight,
total_weight,
},
Threshold::AbsolutePercentage { percentage } => ThresholdResponse::AbsolutePercentage {
percentage,
total_weight,
},
Threshold::ThresholdQuorum { threshold, quorum } => {
ThresholdResponse::ThresholdQuorum {
threshold,
quorum,
total_weight,
}
}
}
}
}

/// 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::ZeroQuorumThreshold {})
} else if *percent > Decimal::one() {
Err(ContractError::UnreachableQuorumThreshold {})
} else {
Ok(())
}
}

// TODO: add some T variants? Maybe good enough as fixed Empty for now
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
Expand Down Expand Up @@ -170,153 +73,3 @@ pub enum QueryMsg {
limit: Option<u32>,
},
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn validate_quorum_percentage() {
// TODO: test the error messages

// 0 is never a valid percentage
let err = valid_quorum(&Decimal::zero()).unwrap_err();
assert_eq!(
err.to_string(),
ContractError::ZeroQuorumThreshold {}.to_string()
);

// 100% is
valid_quorum(&Decimal::one()).unwrap();

// 101% is not
let err = valid_quorum(&Decimal::percent(101)).unwrap_err();
assert_eq!(
err.to_string(),
ContractError::UnreachableQuorumThreshold {}.to_string()
);
// not 100.1%
let err = valid_quorum(&Decimal::permille(1001)).unwrap_err();
assert_eq!(
err.to_string(),
ContractError::UnreachableQuorumThreshold {}.to_string()
);
}

#[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]
fn validate_threshold() {
// absolute count ensures 0 < required <= total_weight
let err = Threshold::AbsoluteCount { weight: 0 }
.validate(5)
.unwrap_err();
// TODO: remove to_string() when PartialEq implemented
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::UnreachableWeight {}.to_string()
);

Threshold::AbsoluteCount { weight: 1 }.validate(5).unwrap();
Threshold::AbsoluteCount { weight: 5 }.validate(5).unwrap();

// AbsolutePercentage just enforces valid_percentage (tested above)
let err = Threshold::AbsolutePercentage {
percentage: Decimal::zero(),
}
.validate(5)
.unwrap_err();
assert_eq!(
err.to_string(),
ContractError::InvalidThreshold {}.to_string()
);
Threshold::AbsolutePercentage {
percentage: Decimal::percent(51),
}
.validate(5)
.unwrap();

// Quorum enforces both valid just enforces valid_percentage (tested above)
Threshold::ThresholdQuorum {
threshold: Decimal::percent(51),
quorum: Decimal::percent(40),
}
.validate(5)
.unwrap();
let err = Threshold::ThresholdQuorum {
threshold: Decimal::percent(101),
quorum: Decimal::percent(40),
}
.validate(5)
.unwrap_err();
assert_eq!(
err.to_string(),
ContractError::InvalidThreshold {}.to_string()
);
let err = Threshold::ThresholdQuorum {
threshold: Decimal::percent(51),
quorum: Decimal::percent(0),
}
.validate(5)
.unwrap_err();
assert_eq!(
err.to_string(),
ContractError::ZeroQuorumThreshold {}.to_string()
);
}

#[test]
fn threshold_response() {
let total_weight: u64 = 100;

let res = Threshold::AbsoluteCount { weight: 42 }.to_response(total_weight);
assert_eq!(
res,
ThresholdResponse::AbsoluteCount {
weight: 42,
total_weight
}
);

let res = Threshold::AbsolutePercentage {
percentage: Decimal::percent(51),
}
.to_response(total_weight);
assert_eq!(
res,
ThresholdResponse::AbsolutePercentage {
percentage: Decimal::percent(51),
total_weight
}
);

let res = Threshold::ThresholdQuorum {
threshold: Decimal::percent(66),
quorum: Decimal::percent(50),
}
.to_response(total_weight);
assert_eq!(
res,
ThresholdResponse::ThresholdQuorum {
threshold: Decimal::percent(66),
quorum: Decimal::percent(50),
total_weight
}
);
}
}
4 changes: 1 addition & 3 deletions contracts/cw3-flex-multisig/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ use cosmwasm_std::{Addr, BlockInfo, CosmosMsg, Decimal, Empty, StdResult, Storag
use cw3::{Status, Vote};
use cw4::Cw4Contract;
use cw_storage_plus::{Item, Map};
use utils::{Duration, Expiration};

use crate::msg::Threshold;
use utils::{Duration, Expiration, Threshold};

// we multiply by this when calculating needed_votes in order to round up properly
// Note: `10u128.pow(9)` fails as "u128::pow` is not yet stable as a const fn"
Expand Down
5 changes: 3 additions & 2 deletions packages/cw3/examples/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ use std::fs::create_dir_all;
use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for};

use cw3::{
Cw3ExecuteMsg, Cw3QueryMsg, ProposalListResponse, ProposalResponse, ThresholdResponse,
VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse,
Cw3ExecuteMsg, Cw3QueryMsg, ProposalListResponse, ProposalResponse, VoteListResponse,
VoteResponse, VoterDetail, VoterListResponse, VoterResponse,
};
use utils::ThresholdResponse;

fn main() {
let mut out_dir = current_dir().unwrap();
Expand Down
Loading