Skip to content

Commit

Permalink
Move Threshold and coexisting implementations into packages/utils
Browse files Browse the repository at this point in the history
  • Loading branch information
ueco-jb committed Dec 21, 2021
1 parent e18c23e commit 45f3731
Show file tree
Hide file tree
Showing 10 changed files with 373 additions and 360 deletions.
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

0 comments on commit 45f3731

Please sign in to comment.