use std::cmp::Ordering;
#[cfg(not(feature = "library"))]
use cosmwasm_std::entry_point;
use cosmwasm_std::{
to_json_binary, Binary, BlockInfo, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order,
Response, StdResult,
};
use abstract_cw2::set_contract_version;
use abstract_cw3::{
Ballot, Proposal, ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo,
VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse, Votes,
};
use cw_storage_plus::Bound;
use cw_utils::{Expiration, ThresholdResponse};
use crate::error::ContractError;
use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
use crate::state::{next_id, Config, BALLOTS, CONFIG, PROPOSALS, VOTERS};
const CONTRACT_NAME: &str = "crates.io:cw3-fixed-multisig";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn instantiate(
deps: DepsMut,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
if msg.voters.is_empty() {
return Err(ContractError::NoVoters {});
}
let total_weight = msg.voters.iter().map(|v| v.weight).sum();
msg.threshold.validate(total_weight)?;
set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
let cfg = Config {
threshold: msg.threshold,
total_weight,
max_voting_period: msg.max_voting_period,
};
CONFIG.save(deps.storage, &cfg)?;
for voter in msg.voters.iter() {
let key = deps.api.addr_validate(&voter.addr)?;
VOTERS.save(deps.storage, &key, &voter.weight)?;
}
Ok(Response::default())
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response<Empty>, ContractError> {
match msg {
ExecuteMsg::Propose {
title,
description,
msgs,
latest,
} => execute_propose(deps, env, info, title, description, msgs, latest),
ExecuteMsg::Vote { proposal_id, vote } => execute_vote(deps, env, info, proposal_id, vote),
ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id),
ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id),
}
}
pub fn execute_propose(
deps: DepsMut,
env: Env,
info: MessageInfo,
title: String,
description: String,
msgs: Vec<CosmosMsg>,
latest: Option<Expiration>,
) -> Result<Response<Empty>, ContractError> {
let vote_power = VOTERS
.may_load(deps.storage, &info.sender)?
.ok_or(ContractError::Unauthorized {})?;
let cfg = CONFIG.load(deps.storage)?;
let max_expires = cfg.max_voting_period.after(&env.block);
let mut expires = latest.unwrap_or(max_expires);
let comp = expires.partial_cmp(&max_expires);
if let Some(Ordering::Greater) = comp {
expires = max_expires;
} else if comp.is_none() {
return Err(ContractError::WrongExpiration {});
}
let mut prop = Proposal {
title,
description,
start_height: env.block.height,
expires,
msgs,
status: Status::Open,
votes: Votes::yes(vote_power),
threshold: cfg.threshold,
total_weight: cfg.total_weight,
proposer: info.sender.clone(),
deposit: None,
};
prop.update_status(&env.block);
let id = next_id(deps.storage)?;
PROPOSALS.save(deps.storage, id, &prop)?;
let ballot = Ballot {
weight: vote_power,
vote: Vote::Yes,
};
BALLOTS.save(deps.storage, (id, &info.sender), &ballot)?;
Ok(Response::new()
.add_attribute("action", "propose")
.add_attribute("sender", info.sender)
.add_attribute("proposal_id", id.to_string())
.add_attribute("status", format!("{:?}", prop.status)))
}
pub fn execute_vote(
deps: DepsMut,
env: Env,
info: MessageInfo,
proposal_id: u64,
vote: Vote,
) -> Result<Response<Empty>, ContractError> {
let voter_power = VOTERS.may_load(deps.storage, &info.sender)?;
let vote_power = match voter_power {
Some(power) if power >= 1 => power,
_ => return Err(ContractError::Unauthorized {}),
};
let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
if ![Status::Open, Status::Passed, Status::Rejected].contains(&prop.status) {
return Err(ContractError::NotOpen {});
}
if prop.expires.is_expired(&env.block) {
return Err(ContractError::Expired {});
}
BALLOTS.update(deps.storage, (proposal_id, &info.sender), |bal| match bal {
Some(_) => Err(ContractError::AlreadyVoted {}),
None => Ok(Ballot {
weight: vote_power,
vote,
}),
})?;
prop.votes.add_vote(vote, vote_power);
prop.update_status(&env.block);
PROPOSALS.save(deps.storage, proposal_id, &prop)?;
Ok(Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", info.sender)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", format!("{:?}", prop.status)))
}
pub fn execute_execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
proposal_id: u64,
) -> Result<Response, ContractError> {
let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
prop.update_status(&env.block);
if prop.status != Status::Passed {
return Err(ContractError::WrongExecuteStatus {});
}
prop.status = Status::Executed;
PROPOSALS.save(deps.storage, proposal_id, &prop)?;
Ok(Response::new()
.add_messages(prop.msgs)
.add_attribute("action", "execute")
.add_attribute("sender", info.sender)
.add_attribute("proposal_id", proposal_id.to_string()))
}
pub fn execute_close(
deps: DepsMut,
env: Env,
info: MessageInfo,
proposal_id: u64,
) -> Result<Response<Empty>, ContractError> {
let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
if [Status::Executed, Status::Rejected, Status::Passed].contains(&prop.status) {
return Err(ContractError::WrongCloseStatus {});
}
if prop.current_status(&env.block) == Status::Passed {
return Err(ContractError::WrongCloseStatus {});
}
if !prop.expires.is_expired(&env.block) {
return Err(ContractError::NotExpired {});
}
prop.status = Status::Rejected;
PROPOSALS.save(deps.storage, proposal_id, &prop)?;
Ok(Response::new()
.add_attribute("action", "close")
.add_attribute("sender", info.sender)
.add_attribute("proposal_id", proposal_id.to_string()))
}
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Threshold {} => to_json_binary(&query_threshold(deps)?),
QueryMsg::Proposal { proposal_id } => {
to_json_binary(&query_proposal(deps, env, proposal_id)?)
}
QueryMsg::Vote { proposal_id, voter } => {
to_json_binary(&query_vote(deps, proposal_id, voter)?)
}
QueryMsg::ListProposals { start_after, limit } => {
to_json_binary(&list_proposals(deps, env, start_after, limit)?)
}
QueryMsg::ReverseProposals {
start_before,
limit,
} => to_json_binary(&reverse_proposals(deps, env, start_before, limit)?),
QueryMsg::ListVotes {
proposal_id,
start_after,
limit,
} => to_json_binary(&list_votes(deps, proposal_id, start_after, limit)?),
QueryMsg::Voter { address } => to_json_binary(&query_voter(deps, address)?),
QueryMsg::ListVoters { start_after, limit } => {
to_json_binary(&list_voters(deps, start_after, limit)?)
}
}
}
fn query_threshold(deps: Deps) -> StdResult<ThresholdResponse> {
let cfg = CONFIG.load(deps.storage)?;
Ok(cfg.threshold.to_response(cfg.total_weight))
}
fn query_proposal(deps: Deps, env: Env, id: u64) -> StdResult<ProposalResponse> {
let prop = PROPOSALS.load(deps.storage, id)?;
let status = prop.current_status(&env.block);
let threshold = prop.threshold.to_response(prop.total_weight);
Ok(ProposalResponse {
id,
title: prop.title,
description: prop.description,
msgs: prop.msgs,
status,
expires: prop.expires,
deposit: prop.deposit,
proposer: prop.proposer,
threshold,
})
}
const MAX_LIMIT: u32 = 30;
const DEFAULT_LIMIT: u32 = 10;
fn list_proposals(
deps: Deps,
env: Env,
start_after: Option<u64>,
limit: Option<u32>,
) -> StdResult<ProposalListResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let proposals = PROPOSALS
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|p| map_proposal(&env.block, p))
.collect::<StdResult<_>>()?;
Ok(ProposalListResponse { proposals })
}
fn reverse_proposals(
deps: Deps,
env: Env,
start_before: Option<u64>,
limit: Option<u32>,
) -> StdResult<ProposalListResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let end = start_before.map(Bound::exclusive);
let props: StdResult<Vec<_>> = PROPOSALS
.range(deps.storage, None, end, Order::Descending)
.take(limit)
.map(|p| map_proposal(&env.block, p))
.collect();
Ok(ProposalListResponse { proposals: props? })
}
fn map_proposal(
block: &BlockInfo,
item: StdResult<(u64, Proposal)>,
) -> StdResult<ProposalResponse> {
item.map(|(id, prop)| {
let status = prop.current_status(block);
let threshold = prop.threshold.to_response(prop.total_weight);
ProposalResponse {
id,
title: prop.title,
description: prop.description,
msgs: prop.msgs,
status,
deposit: prop.deposit,
proposer: prop.proposer,
expires: prop.expires,
threshold,
}
})
}
fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult<VoteResponse> {
let voter = deps.api.addr_validate(&voter)?;
let ballot = BALLOTS.may_load(deps.storage, (proposal_id, &voter))?;
let vote = ballot.map(|b| VoteInfo {
proposal_id,
voter: voter.into(),
vote: b.vote,
weight: b.weight,
});
Ok(VoteResponse { vote })
}
fn list_votes(
deps: Deps,
proposal_id: u64,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<VoteListResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
let votes = BALLOTS
.prefix(proposal_id)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|item| {
item.map(|(addr, ballot)| VoteInfo {
proposal_id,
voter: addr.into(),
vote: ballot.vote,
weight: ballot.weight,
})
})
.collect::<StdResult<_>>()?;
Ok(VoteListResponse { votes })
}
fn query_voter(deps: Deps, voter: String) -> StdResult<VoterResponse> {
let voter = deps.api.addr_validate(&voter)?;
let weight = VOTERS.may_load(deps.storage, &voter)?;
Ok(VoterResponse { weight })
}
fn list_voters(
deps: Deps,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<VoterListResponse> {
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
let voters = VOTERS
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|item| {
item.map(|(addr, weight)| VoterDetail {
addr: addr.into(),
weight,
})
})
.collect::<StdResult<_>>()?;
Ok(VoterListResponse { voters })
}
#[cfg(test)]
mod tests {
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coin, from_json, BankMsg, Decimal};
use abstract_cw2::{get_contract_version, ContractVersion};
use cw_utils::{Duration, Threshold};
use crate::msg::Voter;
use super::*;
fn mock_env_height(height_delta: u64) -> Env {
let mut env = mock_env();
env.block.height += height_delta;
env
}
fn mock_env_time(time_delta: u64) -> Env {
let mut env = mock_env();
env.block.time = env.block.time.plus_seconds(time_delta);
env
}
const OWNER: &str = "admin0001";
const VOTER1: &str = "voter0001";
const VOTER2: &str = "voter0002";
const VOTER3: &str = "voter0003";
const VOTER4: &str = "voter0004";
const VOTER5: &str = "voter0005";
const VOTER6: &str = "voter0006";
const NOWEIGHT_VOTER: &str = "voterxxxx";
const SOMEBODY: &str = "somebody";
fn voter<T: Into<String>>(addr: T, weight: u64) -> Voter {
Voter {
addr: addr.into(),
weight,
}
}
#[track_caller]
fn setup_test_case(
deps: DepsMut,
info: MessageInfo,
threshold: Threshold,
max_voting_period: Duration,
) -> Result<Response<Empty>, ContractError> {
let voters = vec![
voter(&info.sender, 1),
voter(VOTER1, 1),
voter(VOTER2, 2),
voter(VOTER3, 3),
voter(VOTER4, 4),
voter(VOTER5, 5),
voter(VOTER6, 1),
voter(NOWEIGHT_VOTER, 0),
];
let instantiate_msg = InstantiateMsg {
voters,
threshold,
max_voting_period,
};
instantiate(deps, mock_env(), info, instantiate_msg)
}
fn get_tally(deps: Deps, proposal_id: u64) -> u64 {
let voters = QueryMsg::ListVotes {
proposal_id,
start_after: None,
limit: None,
};
let votes: VoteListResponse = from_json(&query(deps, mock_env(), voters).unwrap()).unwrap();
votes
.votes
.iter()
.filter(|&v| v.vote == Vote::Yes)
.map(|v| v.weight)
.sum()
}
#[test]
fn test_instantiate_works() {
let mut deps = mock_dependencies();
let info = mock_info(OWNER, &[]);
let max_voting_period = Duration::Time(1234567);
let instantiate_msg = InstantiateMsg {
voters: vec![],
threshold: Threshold::ThresholdQuorum {
threshold: Decimal::zero(),
quorum: Decimal::percent(1),
},
max_voting_period,
};
let err = instantiate(
deps.as_mut(),
mock_env(),
info.clone(),
instantiate_msg.clone(),
)
.unwrap_err();
assert_eq!(err, ContractError::NoVoters {});
let instantiate_msg = InstantiateMsg {
voters: vec![voter(OWNER, 1)],
..instantiate_msg
};
let err =
instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap_err();
assert_eq!(
err,
ContractError::Threshold(cw_utils::ThresholdError::InvalidThreshold {})
);
let threshold = Threshold::AbsoluteCount { weight: 100 };
let err =
setup_test_case(deps.as_mut(), info.clone(), threshold, max_voting_period).unwrap_err();
assert_eq!(
err,
ContractError::Threshold(cw_utils::ThresholdError::UnreachableWeight {})
);
let threshold = Threshold::AbsoluteCount { weight: 1 };
setup_test_case(deps.as_mut(), info, threshold, max_voting_period).unwrap();
assert_eq!(
ContractVersion {
contract: CONTRACT_NAME.to_string(),
version: CONTRACT_VERSION.to_string(),
},
get_contract_version(&deps.storage).unwrap()
)
}
#[test]
fn zero_weight_member_cant_vote() {
let mut deps = mock_dependencies();
let threshold = Threshold::AbsoluteCount { weight: 4 };
let voting_period = Duration::Time(2000000);
let info = mock_info(OWNER, &[]);
setup_test_case(deps.as_mut(), info, threshold, voting_period).unwrap();
let bank_msg = BankMsg::Send {
to_address: SOMEBODY.into(),
amount: vec![coin(1, "BTC")],
};
let msgs = vec![CosmosMsg::Bank(bank_msg)];
let info = mock_info(NOWEIGHT_VOTER, &[]);
let proposal = ExecuteMsg::Propose {
title: "Rewarding somebody".to_string(),
description: "Do we reward her?".to_string(),
msgs,
latest: None,
};
let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
let no_vote = ExecuteMsg::Vote {
proposal_id,
vote: Vote::No,
};
let info = mock_info(NOWEIGHT_VOTER, &[]);
let err = execute(deps.as_mut(), mock_env(), info, no_vote).unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
}
#[test]
fn test_propose_works() {
let mut deps = mock_dependencies();
let threshold = Threshold::AbsoluteCount { weight: 4 };
let voting_period = Duration::Time(2000000);
let info = mock_info(OWNER, &[]);
setup_test_case(deps.as_mut(), info, threshold, voting_period).unwrap();
let bank_msg = BankMsg::Send {
to_address: SOMEBODY.into(),
amount: vec![coin(1, "BTC")],
};
let msgs = vec![CosmosMsg::Bank(bank_msg)];
let info = mock_info(SOMEBODY, &[]);
let proposal = ExecuteMsg::Propose {
title: "Rewarding somebody".to_string(),
description: "Do we reward her?".to_string(),
msgs: msgs.clone(),
latest: None,
};
let err = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
let info = mock_info(OWNER, &[]);
let proposal_wrong_exp = ExecuteMsg::Propose {
title: "Rewarding somebody".to_string(),
description: "Do we reward her?".to_string(),
msgs,
latest: Some(Expiration::AtHeight(123456)),
};
let err = execute(deps.as_mut(), mock_env(), info, proposal_wrong_exp).unwrap_err();
assert_eq!(err, ContractError::WrongExpiration {});
let info = mock_info(VOTER3, &[]);
let res = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "propose")
.add_attribute("sender", VOTER3)
.add_attribute("proposal_id", 1.to_string())
.add_attribute("status", "Open")
);
let info = mock_info(VOTER4, &[]);
let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "propose")
.add_attribute("sender", VOTER4)
.add_attribute("proposal_id", 2.to_string())
.add_attribute("status", "Passed")
);
}
#[test]
fn test_vote_works() {
let mut deps = mock_dependencies();
let threshold = Threshold::AbsoluteCount { weight: 3 };
let voting_period = Duration::Time(2000000);
let info = mock_info(OWNER, &[]);
setup_test_case(deps.as_mut(), info.clone(), threshold, voting_period).unwrap();
let bank_msg = BankMsg::Send {
to_address: SOMEBODY.into(),
amount: vec![coin(1, "BTC")],
};
let msgs = vec![CosmosMsg::Bank(bank_msg)];
let proposal = ExecuteMsg::Propose {
title: "Pay somebody".to_string(),
description: "Do I pay her?".to_string(),
msgs,
latest: None,
};
let res = execute(deps.as_mut(), mock_env(), info.clone(), proposal).unwrap();
let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
let yes_vote = ExecuteMsg::Vote {
proposal_id,
vote: Vote::Yes,
};
let err = execute(deps.as_mut(), mock_env(), info, yes_vote.clone()).unwrap_err();
assert_eq!(err, ContractError::AlreadyVoted {});
let info = mock_info(SOMEBODY, &[]);
let err = execute(deps.as_mut(), mock_env(), info, yes_vote.clone()).unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
let info = mock_info(VOTER1, &[]);
let res = execute(deps.as_mut(), mock_env(), info, yes_vote.clone()).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER1)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Open")
);
let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
let tally = get_tally(deps.as_ref(), proposal_id);
let no_vote = ExecuteMsg::Vote {
proposal_id,
vote: Vote::No,
};
let info = mock_info(VOTER2, &[]);
execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
let veto_vote = ExecuteMsg::Vote {
proposal_id,
vote: Vote::Veto,
};
let info = mock_info(VOTER3, &[]);
execute(deps.as_mut(), mock_env(), info.clone(), veto_vote).unwrap();
assert_eq!(tally, get_tally(deps.as_ref(), proposal_id));
let err = execute(deps.as_mut(), mock_env(), info.clone(), yes_vote.clone()).unwrap_err();
assert_eq!(err, ContractError::AlreadyVoted {});
assert_eq!(tally, get_tally(deps.as_ref(), proposal_id));
let env = match voting_period {
Duration::Time(duration) => mock_env_time(duration + 1),
Duration::Height(duration) => mock_env_height(duration + 1),
};
let err = execute(deps.as_mut(), env, info, no_vote).unwrap_err();
assert_eq!(err, ContractError::Expired {});
let info = mock_info(VOTER4, &[]);
let res = execute(deps.as_mut(), mock_env(), info, yes_vote.clone()).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER4)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Passed")
);
let info = mock_info(VOTER5, &[]);
let res = execute(deps.as_mut(), mock_env(), info, yes_vote).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER5)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Passed")
);
let info = mock_info(OWNER, &[]);
let bank_msg = BankMsg::Send {
to_address: SOMEBODY.into(),
amount: vec![coin(1, "BTC")],
};
let msgs = vec![CosmosMsg::Bank(bank_msg)];
let proposal = ExecuteMsg::Propose {
title: "Pay somebody".to_string(),
description: "Do I pay her?".to_string(),
msgs,
latest: None,
};
let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
let no_vote = ExecuteMsg::Vote {
proposal_id,
vote: Vote::No,
};
let info = mock_info(VOTER1, &[]);
let res = execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER1)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Open")
);
let info = mock_info(VOTER4, &[]);
let res = execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER4)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Open")
);
let info = mock_info(VOTER3, &[]);
let _res = execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
let info = mock_info(VOTER5, &[]);
let res = execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER5)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Open")
);
let info = mock_info(VOTER2, &[]);
let res = execute(deps.as_mut(), mock_env(), info, no_vote).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER2)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Rejected")
);
let info = mock_info(VOTER6, &[]);
let yes_vote = ExecuteMsg::Vote {
proposal_id,
vote: Vote::Yes,
};
let res = execute(deps.as_mut(), mock_env(), info, yes_vote).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER6)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Rejected")
);
}
#[test]
fn test_execute_works() {
let mut deps = mock_dependencies();
let threshold = Threshold::AbsoluteCount { weight: 3 };
let voting_period = Duration::Time(2000000);
let info = mock_info(OWNER, &[]);
setup_test_case(deps.as_mut(), info.clone(), threshold, voting_period).unwrap();
let bank_msg = BankMsg::Send {
to_address: SOMEBODY.into(),
amount: vec![coin(1, "BTC")],
};
let msgs = vec![CosmosMsg::Bank(bank_msg)];
let proposal = ExecuteMsg::Propose {
title: "Pay somebody".to_string(),
description: "Do I pay her?".to_string(),
msgs: msgs.clone(),
latest: None,
};
let res = execute(deps.as_mut(), mock_env(), info.clone(), proposal).unwrap();
let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
let execution = ExecuteMsg::Execute { proposal_id };
let err = execute(deps.as_mut(), mock_env(), info, execution.clone()).unwrap_err();
assert_eq!(err, ContractError::WrongExecuteStatus {});
let vote = ExecuteMsg::Vote {
proposal_id,
vote: Vote::Yes,
};
let info = mock_info(VOTER3, &[]);
let res = execute(deps.as_mut(), mock_env(), info.clone(), vote).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER3)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Passed")
);
let closing = ExecuteMsg::Close { proposal_id };
let err = execute(deps.as_mut(), mock_env(), info, closing).unwrap_err();
assert_eq!(err, ContractError::WrongCloseStatus {});
let info = mock_info(SOMEBODY, &[]);
let res = execute(deps.as_mut(), mock_env(), info.clone(), execution).unwrap();
assert_eq!(
res,
Response::new()
.add_messages(msgs)
.add_attribute("action", "execute")
.add_attribute("sender", SOMEBODY)
.add_attribute("proposal_id", proposal_id.to_string())
);
let closing = ExecuteMsg::Close { proposal_id };
let err = execute(deps.as_mut(), mock_env(), info, closing).unwrap_err();
assert_eq!(err, ContractError::WrongCloseStatus {});
}
#[test]
fn proposal_pass_on_expiration() {
let mut deps = mock_dependencies();
let threshold = Threshold::ThresholdQuorum {
threshold: Decimal::percent(51),
quorum: Decimal::percent(1),
};
let voting_period = Duration::Time(2000000);
let info = mock_info(OWNER, &[]);
setup_test_case(deps.as_mut(), info.clone(), threshold, voting_period).unwrap();
let bank_msg = BankMsg::Send {
to_address: SOMEBODY.into(),
amount: vec![coin(1, "BTC")],
};
let msgs = vec![CosmosMsg::Bank(bank_msg)];
let proposal = ExecuteMsg::Propose {
title: "Pay somebody".to_string(),
description: "Do I pay her?".to_string(),
msgs,
latest: None,
};
let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
let vote = ExecuteMsg::Vote {
proposal_id,
vote: Vote::Yes,
};
let info = mock_info(VOTER3, &[]);
let res = execute(deps.as_mut(), mock_env(), info, vote).unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "vote")
.add_attribute("sender", VOTER3)
.add_attribute("proposal_id", proposal_id.to_string())
.add_attribute("status", "Open")
);
let env = match voting_period {
Duration::Time(duration) => mock_env_time(duration + 1),
Duration::Height(duration) => mock_env_height(duration + 1),
};
let prop: ProposalResponse = from_json(
&query(
deps.as_ref(),
env.clone(),
QueryMsg::Proposal { proposal_id },
)
.unwrap(),
)
.unwrap();
assert_eq!(prop.status, Status::Passed);
let info = mock_info(SOMEBODY, &[]);
let err = execute(
deps.as_mut(),
env.clone(),
info.clone(),
ExecuteMsg::Close { proposal_id },
)
.unwrap_err();
assert_eq!(err, ContractError::WrongCloseStatus {});
let res = execute(
deps.as_mut(),
env,
info,
ExecuteMsg::Execute { proposal_id },
)
.unwrap();
assert_eq!(
res.attributes,
Response::<Empty>::new()
.add_attribute("action", "execute")
.add_attribute("sender", SOMEBODY)
.add_attribute("proposal_id", proposal_id.to_string())
.attributes
)
}
#[test]
fn test_close_works() {
let mut deps = mock_dependencies();
let threshold = Threshold::AbsoluteCount { weight: 3 };
let voting_period = Duration::Height(2000000);
let info = mock_info(OWNER, &[]);
setup_test_case(deps.as_mut(), info.clone(), threshold, voting_period).unwrap();
let bank_msg = BankMsg::Send {
to_address: SOMEBODY.into(),
amount: vec![coin(1, "BTC")],
};
let msgs = vec![CosmosMsg::Bank(bank_msg)];
let proposal = ExecuteMsg::Propose {
title: "Pay somebody".to_string(),
description: "Do I pay her?".to_string(),
msgs: msgs.clone(),
latest: None,
};
let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
let closing = ExecuteMsg::Close { proposal_id };
let info = mock_info(SOMEBODY, &[]);
let err = execute(deps.as_mut(), mock_env(), info, closing).unwrap_err();
assert_eq!(err, ContractError::NotExpired {});
let info = mock_info(OWNER, &[]);
let proposal = ExecuteMsg::Propose {
title: "(Try to) pay somebody".to_string(),
description: "Pay somebody after time?".to_string(),
msgs,
latest: Some(Expiration::AtHeight(123456)),
};
let res = execute(deps.as_mut(), mock_env(), info.clone(), proposal).unwrap();
let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
let closing = ExecuteMsg::Close { proposal_id };
let env = mock_env_height(1234567);
let res = execute(
deps.as_mut(),
env,
mock_info(SOMEBODY, &[]),
closing.clone(),
)
.unwrap();
assert_eq!(
res,
Response::new()
.add_attribute("action", "close")
.add_attribute("sender", SOMEBODY)
.add_attribute("proposal_id", proposal_id.to_string())
);
let err = execute(deps.as_mut(), mock_env(), info, closing).unwrap_err();
assert_eq!(err, ContractError::WrongCloseStatus {});
}
}