1use std::cmp::Ordering;
2
3#[cfg(not(feature = "library"))]
4use cosmwasm_std::entry_point;
5use cosmwasm_std::{
6 to_json_binary, Binary, BlockInfo, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order,
7 Response, StdResult,
8};
9
10use cw2::set_contract_version;
11use cw3::{
12 Ballot, Proposal, ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo,
13 VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse, Votes,
14};
15use cw_storage_plus::Bound;
16use cw_utils::{Expiration, ThresholdResponse};
17
18use crate::error::ContractError;
19use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
20use crate::state::{next_id, Config, BALLOTS, CONFIG, PROPOSALS, VOTERS};
21
22const CONTRACT_NAME: &str = "crates.io:cw3-fixed-multisig";
24const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
25
26#[cfg_attr(not(feature = "library"), entry_point)]
27pub fn instantiate(
28 deps: DepsMut,
29 _env: Env,
30 _info: MessageInfo,
31 msg: InstantiateMsg,
32) -> Result<Response, ContractError> {
33 if msg.voters.is_empty() {
34 return Err(ContractError::NoVoters {});
35 }
36 let total_weight = msg.voters.iter().map(|v| v.weight).sum();
37
38 msg.threshold.validate(total_weight)?;
39
40 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
41
42 let cfg = Config {
43 threshold: msg.threshold,
44 total_weight,
45 max_voting_period: msg.max_voting_period,
46 };
47 CONFIG.save(deps.storage, &cfg)?;
48
49 for voter in msg.voters.iter() {
51 let key = deps.api.addr_validate(&voter.addr)?;
52 VOTERS.save(deps.storage, &key, &voter.weight)?;
53 }
54 Ok(Response::default())
55}
56
57#[cfg_attr(not(feature = "library"), entry_point)]
58pub fn execute(
59 deps: DepsMut,
60 env: Env,
61 info: MessageInfo,
62 msg: ExecuteMsg,
63) -> Result<Response<Empty>, ContractError> {
64 match msg {
65 ExecuteMsg::Propose {
66 title,
67 description,
68 msgs,
69 latest,
70 } => execute_propose(deps, env, info, title, description, msgs, latest),
71 ExecuteMsg::Vote { proposal_id, vote } => execute_vote(deps, env, info, proposal_id, vote),
72 ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id),
73 ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id),
74 }
75}
76
77pub fn execute_propose(
78 deps: DepsMut,
79 env: Env,
80 info: MessageInfo,
81 title: String,
82 description: String,
83 msgs: Vec<CosmosMsg>,
84 latest: Option<Expiration>,
86) -> Result<Response<Empty>, ContractError> {
87 let vote_power = VOTERS
89 .may_load(deps.storage, &info.sender)?
90 .ok_or(ContractError::Unauthorized {})?;
91
92 let cfg = CONFIG.load(deps.storage)?;
93
94 let max_expires = cfg.max_voting_period.after(&env.block);
96 let mut expires = latest.unwrap_or(max_expires);
97 let comp = expires.partial_cmp(&max_expires);
98 if let Some(Ordering::Greater) = comp {
99 expires = max_expires;
100 } else if comp.is_none() {
101 return Err(ContractError::WrongExpiration {});
102 }
103
104 let mut prop = Proposal {
106 title,
107 description,
108 start_height: env.block.height,
109 expires,
110 msgs,
111 status: Status::Open,
112 votes: Votes::yes(vote_power),
113 threshold: cfg.threshold,
114 total_weight: cfg.total_weight,
115 proposer: info.sender.clone(),
116 deposit: None,
117 };
118 prop.update_status(&env.block);
119 let id = next_id(deps.storage)?;
120 PROPOSALS.save(deps.storage, id, &prop)?;
121
122 let ballot = Ballot {
124 weight: vote_power,
125 vote: Vote::Yes,
126 };
127 BALLOTS.save(deps.storage, (id, &info.sender), &ballot)?;
128
129 Ok(Response::new()
130 .add_attribute("action", "propose")
131 .add_attribute("sender", info.sender)
132 .add_attribute("proposal_id", id.to_string())
133 .add_attribute("status", format!("{:?}", prop.status)))
134}
135
136pub fn execute_vote(
137 deps: DepsMut,
138 env: Env,
139 info: MessageInfo,
140 proposal_id: u64,
141 vote: Vote,
142) -> Result<Response<Empty>, ContractError> {
143 let voter_power = VOTERS.may_load(deps.storage, &info.sender)?;
145 let vote_power = match voter_power {
146 Some(power) if power >= 1 => power,
147 _ => return Err(ContractError::Unauthorized {}),
148 };
149
150 let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
152 if ![Status::Open, Status::Passed, Status::Rejected].contains(&prop.status) {
154 return Err(ContractError::NotOpen {});
155 }
156 if prop.expires.is_expired(&env.block) {
158 return Err(ContractError::Expired {});
159 }
160
161 BALLOTS.update(deps.storage, (proposal_id, &info.sender), |bal| match bal {
163 Some(_) => Err(ContractError::AlreadyVoted {}),
164 None => Ok(Ballot {
165 weight: vote_power,
166 vote,
167 }),
168 })?;
169
170 prop.votes.add_vote(vote, vote_power);
172 prop.update_status(&env.block);
173 PROPOSALS.save(deps.storage, proposal_id, &prop)?;
174
175 Ok(Response::new()
176 .add_attribute("action", "vote")
177 .add_attribute("sender", info.sender)
178 .add_attribute("proposal_id", proposal_id.to_string())
179 .add_attribute("status", format!("{:?}", prop.status)))
180}
181
182pub fn execute_execute(
183 deps: DepsMut,
184 env: Env,
185 info: MessageInfo,
186 proposal_id: u64,
187) -> Result<Response, ContractError> {
188 let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
191 prop.update_status(&env.block);
194 if prop.status != Status::Passed {
195 return Err(ContractError::WrongExecuteStatus {});
196 }
197
198 prop.status = Status::Executed;
200 PROPOSALS.save(deps.storage, proposal_id, &prop)?;
201
202 Ok(Response::new()
204 .add_messages(prop.msgs)
205 .add_attribute("action", "execute")
206 .add_attribute("sender", info.sender)
207 .add_attribute("proposal_id", proposal_id.to_string()))
208}
209
210pub fn execute_close(
211 deps: DepsMut,
212 env: Env,
213 info: MessageInfo,
214 proposal_id: u64,
215) -> Result<Response<Empty>, ContractError> {
216 let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
219 if [Status::Executed, Status::Rejected, Status::Passed].contains(&prop.status) {
220 return Err(ContractError::WrongCloseStatus {});
221 }
222 if prop.current_status(&env.block) == Status::Passed {
224 return Err(ContractError::WrongCloseStatus {});
225 }
226 if !prop.expires.is_expired(&env.block) {
227 return Err(ContractError::NotExpired {});
228 }
229
230 prop.status = Status::Rejected;
232 PROPOSALS.save(deps.storage, proposal_id, &prop)?;
233
234 Ok(Response::new()
235 .add_attribute("action", "close")
236 .add_attribute("sender", info.sender)
237 .add_attribute("proposal_id", proposal_id.to_string()))
238}
239
240#[cfg_attr(not(feature = "library"), entry_point)]
241pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
242 match msg {
243 QueryMsg::Threshold {} => to_json_binary(&query_threshold(deps)?),
244 QueryMsg::Proposal { proposal_id } => {
245 to_json_binary(&query_proposal(deps, env, proposal_id)?)
246 }
247 QueryMsg::Vote { proposal_id, voter } => {
248 to_json_binary(&query_vote(deps, proposal_id, voter)?)
249 }
250 QueryMsg::ListProposals { start_after, limit } => {
251 to_json_binary(&list_proposals(deps, env, start_after, limit)?)
252 }
253 QueryMsg::ReverseProposals {
254 start_before,
255 limit,
256 } => to_json_binary(&reverse_proposals(deps, env, start_before, limit)?),
257 QueryMsg::ListVotes {
258 proposal_id,
259 start_after,
260 limit,
261 } => to_json_binary(&list_votes(deps, proposal_id, start_after, limit)?),
262 QueryMsg::Voter { address } => to_json_binary(&query_voter(deps, address)?),
263 QueryMsg::ListVoters { start_after, limit } => {
264 to_json_binary(&list_voters(deps, start_after, limit)?)
265 }
266 }
267}
268
269fn query_threshold(deps: Deps) -> StdResult<ThresholdResponse> {
270 let cfg = CONFIG.load(deps.storage)?;
271 Ok(cfg.threshold.to_response(cfg.total_weight))
272}
273
274fn query_proposal(deps: Deps, env: Env, id: u64) -> StdResult<ProposalResponse> {
275 let prop = PROPOSALS.load(deps.storage, id)?;
276 let status = prop.current_status(&env.block);
277 let threshold = prop.threshold.to_response(prop.total_weight);
278 Ok(ProposalResponse {
279 id,
280 title: prop.title,
281 description: prop.description,
282 msgs: prop.msgs,
283 status,
284 expires: prop.expires,
285 deposit: prop.deposit,
286 proposer: prop.proposer,
287 threshold,
288 })
289}
290
291const MAX_LIMIT: u32 = 30;
293const DEFAULT_LIMIT: u32 = 10;
294
295fn list_proposals(
296 deps: Deps,
297 env: Env,
298 start_after: Option<u64>,
299 limit: Option<u32>,
300) -> StdResult<ProposalListResponse> {
301 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
302 let start = start_after.map(Bound::exclusive);
303 let proposals = PROPOSALS
304 .range(deps.storage, start, None, Order::Ascending)
305 .take(limit)
306 .map(|p| map_proposal(&env.block, p))
307 .collect::<StdResult<_>>()?;
308
309 Ok(ProposalListResponse { proposals })
310}
311
312fn reverse_proposals(
313 deps: Deps,
314 env: Env,
315 start_before: Option<u64>,
316 limit: Option<u32>,
317) -> StdResult<ProposalListResponse> {
318 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
319 let end = start_before.map(Bound::exclusive);
320 let props: StdResult<Vec<_>> = PROPOSALS
321 .range(deps.storage, None, end, Order::Descending)
322 .take(limit)
323 .map(|p| map_proposal(&env.block, p))
324 .collect();
325
326 Ok(ProposalListResponse { proposals: props? })
327}
328
329fn map_proposal(
330 block: &BlockInfo,
331 item: StdResult<(u64, Proposal)>,
332) -> StdResult<ProposalResponse> {
333 item.map(|(id, prop)| {
334 let status = prop.current_status(block);
335 let threshold = prop.threshold.to_response(prop.total_weight);
336 ProposalResponse {
337 id,
338 title: prop.title,
339 description: prop.description,
340 msgs: prop.msgs,
341 status,
342 deposit: prop.deposit,
343 proposer: prop.proposer,
344 expires: prop.expires,
345 threshold,
346 }
347 })
348}
349
350fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult<VoteResponse> {
351 let voter = deps.api.addr_validate(&voter)?;
352 let ballot = BALLOTS.may_load(deps.storage, (proposal_id, &voter))?;
353 let vote = ballot.map(|b| VoteInfo {
354 proposal_id,
355 voter: voter.into(),
356 vote: b.vote,
357 weight: b.weight,
358 });
359 Ok(VoteResponse { vote })
360}
361
362fn list_votes(
363 deps: Deps,
364 proposal_id: u64,
365 start_after: Option<String>,
366 limit: Option<u32>,
367) -> StdResult<VoteListResponse> {
368 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
369 let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
370
371 let votes = BALLOTS
372 .prefix(proposal_id)
373 .range(deps.storage, start, None, Order::Ascending)
374 .take(limit)
375 .map(|item| {
376 item.map(|(addr, ballot)| VoteInfo {
377 proposal_id,
378 voter: addr.into(),
379 vote: ballot.vote,
380 weight: ballot.weight,
381 })
382 })
383 .collect::<StdResult<_>>()?;
384
385 Ok(VoteListResponse { votes })
386}
387
388fn query_voter(deps: Deps, voter: String) -> StdResult<VoterResponse> {
389 let voter = deps.api.addr_validate(&voter)?;
390 let weight = VOTERS.may_load(deps.storage, &voter)?;
391 Ok(VoterResponse { weight })
392}
393
394fn list_voters(
395 deps: Deps,
396 start_after: Option<String>,
397 limit: Option<u32>,
398) -> StdResult<VoterListResponse> {
399 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
400 let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
401
402 let voters = VOTERS
403 .range(deps.storage, start, None, Order::Ascending)
404 .take(limit)
405 .map(|item| {
406 item.map(|(addr, weight)| VoterDetail {
407 addr: addr.into(),
408 weight,
409 })
410 })
411 .collect::<StdResult<_>>()?;
412
413 Ok(VoterListResponse { voters })
414}
415
416#[cfg(test)]
417mod tests {
418 use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
419 use cosmwasm_std::{coin, from_json, BankMsg, Decimal};
420
421 use cw2::{get_contract_version, ContractVersion};
422 use cw_utils::{Duration, Threshold};
423
424 use easy_addr::addr;
425
426 use crate::msg::Voter;
427
428 use super::*;
429
430 fn mock_env_height(height_delta: u64) -> Env {
431 let mut env = mock_env();
432 env.block.height += height_delta;
433 env
434 }
435
436 fn mock_env_time(time_delta: u64) -> Env {
437 let mut env = mock_env();
438 env.block.time = env.block.time.plus_seconds(time_delta);
439 env
440 }
441
442 const OWNER: &str = addr!("admin0001");
443 const VOTER1: &str = addr!("voter0001");
444 const VOTER2: &str = addr!("voter0002");
445 const VOTER3: &str = addr!("voter0003");
446 const VOTER4: &str = addr!("voter0004");
447 const VOTER5: &str = addr!("voter0005");
448 const VOTER6: &str = addr!("voter0006");
449 const NOWEIGHT_VOTER: &str = addr!("voterxxxx");
450 const SOMEBODY: &str = addr!("somebody");
451
452 fn voter<T: Into<String>>(addr: T, weight: u64) -> Voter {
453 Voter {
454 addr: addr.into(),
455 weight,
456 }
457 }
458
459 #[track_caller]
461 fn setup_test_case(
462 deps: DepsMut,
463 info: MessageInfo,
464 threshold: Threshold,
465 max_voting_period: Duration,
466 ) -> Result<Response<Empty>, ContractError> {
467 let voters = vec![
469 voter(&info.sender, 1),
470 voter(VOTER1, 1),
471 voter(VOTER2, 2),
472 voter(VOTER3, 3),
473 voter(VOTER4, 4),
474 voter(VOTER5, 5),
475 voter(VOTER6, 1),
476 voter(NOWEIGHT_VOTER, 0),
477 ];
478
479 let instantiate_msg = InstantiateMsg {
480 voters,
481 threshold,
482 max_voting_period,
483 };
484 instantiate(deps, mock_env(), info, instantiate_msg)
485 }
486
487 fn get_tally(deps: Deps, proposal_id: u64) -> u64 {
488 let voters = QueryMsg::ListVotes {
490 proposal_id,
491 start_after: None,
492 limit: None,
493 };
494 let votes: VoteListResponse = from_json(query(deps, mock_env(), voters).unwrap()).unwrap();
495 votes
497 .votes
498 .iter()
499 .filter(|&v| v.vote == Vote::Yes)
500 .map(|v| v.weight)
501 .sum()
502 }
503
504 #[test]
505 fn test_instantiate_works() {
506 let mut deps = mock_dependencies();
507 let info = mock_info(OWNER, &[]);
508
509 let max_voting_period = Duration::Time(1234567);
510
511 let instantiate_msg = InstantiateMsg {
513 voters: vec![],
514 threshold: Threshold::ThresholdQuorum {
515 threshold: Decimal::zero(),
516 quorum: Decimal::percent(1),
517 },
518 max_voting_period,
519 };
520 let err = instantiate(
521 deps.as_mut(),
522 mock_env(),
523 info.clone(),
524 instantiate_msg.clone(),
525 )
526 .unwrap_err();
527 assert_eq!(err, ContractError::NoVoters {});
528
529 let instantiate_msg = InstantiateMsg {
531 voters: vec![voter(OWNER, 1)],
532 ..instantiate_msg
533 };
534 let err =
535 instantiate(deps.as_mut(), mock_env(), info.clone(), instantiate_msg).unwrap_err();
536 assert_eq!(
537 err,
538 ContractError::Threshold(cw_utils::ThresholdError::InvalidThreshold {})
539 );
540
541 let threshold = Threshold::AbsoluteCount { weight: 100 };
543 let err =
544 setup_test_case(deps.as_mut(), info.clone(), threshold, max_voting_period).unwrap_err();
545 assert_eq!(
546 err,
547 ContractError::Threshold(cw_utils::ThresholdError::UnreachableWeight {})
548 );
549
550 let threshold = Threshold::AbsoluteCount { weight: 1 };
552 setup_test_case(deps.as_mut(), info, threshold, max_voting_period).unwrap();
553
554 assert_eq!(
556 ContractVersion {
557 contract: CONTRACT_NAME.to_string(),
558 version: CONTRACT_VERSION.to_string(),
559 },
560 get_contract_version(&deps.storage).unwrap()
561 )
562 }
563
564 #[test]
567 fn zero_weight_member_cant_vote() {
568 let mut deps = mock_dependencies();
569
570 let threshold = Threshold::AbsoluteCount { weight: 4 };
571 let voting_period = Duration::Time(2000000);
572
573 let info = mock_info(OWNER, &[]);
574 setup_test_case(deps.as_mut(), info, threshold, voting_period).unwrap();
575
576 let bank_msg = BankMsg::Send {
577 to_address: SOMEBODY.into(),
578 amount: vec![coin(1, "BTC")],
579 };
580 let msgs = vec![CosmosMsg::Bank(bank_msg)];
581
582 let info = mock_info(NOWEIGHT_VOTER, &[]);
584 let proposal = ExecuteMsg::Propose {
585 title: "Rewarding somebody".to_string(),
586 description: "Do we reward her?".to_string(),
587 msgs,
588 latest: None,
589 };
590 let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
591
592 let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
594
595 let no_vote = ExecuteMsg::Vote {
597 proposal_id,
598 vote: Vote::No,
599 };
600 let info = mock_info(NOWEIGHT_VOTER, &[]);
602 let err = execute(deps.as_mut(), mock_env(), info, no_vote).unwrap_err();
603 assert_eq!(err, ContractError::Unauthorized {});
604 }
605
606 #[test]
607 fn test_propose_works() {
608 let mut deps = mock_dependencies();
609
610 let threshold = Threshold::AbsoluteCount { weight: 4 };
611 let voting_period = Duration::Time(2000000);
612
613 let info = mock_info(OWNER, &[]);
614 setup_test_case(deps.as_mut(), info, threshold, voting_period).unwrap();
615
616 let bank_msg = BankMsg::Send {
617 to_address: SOMEBODY.into(),
618 amount: vec![coin(1, "BTC")],
619 };
620 let msgs = vec![CosmosMsg::Bank(bank_msg)];
621
622 let info = mock_info(SOMEBODY, &[]);
624 let proposal = ExecuteMsg::Propose {
625 title: "Rewarding somebody".to_string(),
626 description: "Do we reward her?".to_string(),
627 msgs: msgs.clone(),
628 latest: None,
629 };
630 let err = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap_err();
631 assert_eq!(err, ContractError::Unauthorized {});
632
633 let info = mock_info(OWNER, &[]);
635 let proposal_wrong_exp = ExecuteMsg::Propose {
636 title: "Rewarding somebody".to_string(),
637 description: "Do we reward her?".to_string(),
638 msgs,
639 latest: Some(Expiration::AtHeight(123456)),
640 };
641 let err = execute(deps.as_mut(), mock_env(), info, proposal_wrong_exp).unwrap_err();
642 assert_eq!(err, ContractError::WrongExpiration {});
643
644 let info = mock_info(VOTER3, &[]);
646 let res = execute(deps.as_mut(), mock_env(), info, proposal.clone()).unwrap();
647
648 assert_eq!(
650 res,
651 Response::new()
652 .add_attribute("action", "propose")
653 .add_attribute("sender", VOTER3)
654 .add_attribute("proposal_id", 1.to_string())
655 .add_attribute("status", "Open")
656 );
657
658 let info = mock_info(VOTER4, &[]);
660 let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
661
662 assert_eq!(
664 res,
665 Response::new()
666 .add_attribute("action", "propose")
667 .add_attribute("sender", VOTER4)
668 .add_attribute("proposal_id", 2.to_string())
669 .add_attribute("status", "Passed")
670 );
671 }
672
673 #[test]
674 fn test_vote_works() {
675 let mut deps = mock_dependencies();
676
677 let threshold = Threshold::AbsoluteCount { weight: 3 };
678 let voting_period = Duration::Time(2000000);
679
680 let info = mock_info(OWNER, &[]);
681 setup_test_case(deps.as_mut(), info.clone(), threshold, voting_period).unwrap();
682
683 let bank_msg = BankMsg::Send {
685 to_address: SOMEBODY.into(),
686 amount: vec![coin(1, "BTC")],
687 };
688 let msgs = vec![CosmosMsg::Bank(bank_msg)];
689 let proposal = ExecuteMsg::Propose {
690 title: "Pay somebody".to_string(),
691 description: "Do I pay her?".to_string(),
692 msgs,
693 latest: None,
694 };
695 let res = execute(deps.as_mut(), mock_env(), info.clone(), proposal).unwrap();
696
697 let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
699
700 let yes_vote = ExecuteMsg::Vote {
702 proposal_id,
703 vote: Vote::Yes,
704 };
705 let err = execute(deps.as_mut(), mock_env(), info, yes_vote.clone()).unwrap_err();
706 assert_eq!(err, ContractError::AlreadyVoted {});
707
708 let info = mock_info(SOMEBODY, &[]);
710 let err = execute(deps.as_mut(), mock_env(), info, yes_vote.clone()).unwrap_err();
711 assert_eq!(err, ContractError::Unauthorized {});
712
713 let info = mock_info(VOTER1, &[]);
715 let res = execute(deps.as_mut(), mock_env(), info, yes_vote.clone()).unwrap();
716
717 assert_eq!(
719 res,
720 Response::new()
721 .add_attribute("action", "vote")
722 .add_attribute("sender", VOTER1)
723 .add_attribute("proposal_id", proposal_id.to_string())
724 .add_attribute("status", "Open")
725 );
726
727 let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
730
731 let tally = get_tally(deps.as_ref(), proposal_id);
733
734 let no_vote = ExecuteMsg::Vote {
736 proposal_id,
737 vote: Vote::No,
738 };
739 let info = mock_info(VOTER2, &[]);
740 execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
741
742 let veto_vote = ExecuteMsg::Vote {
744 proposal_id,
745 vote: Vote::Veto,
746 };
747 let info = mock_info(VOTER3, &[]);
748 execute(deps.as_mut(), mock_env(), info.clone(), veto_vote).unwrap();
749
750 assert_eq!(tally, get_tally(deps.as_ref(), proposal_id));
752
753 let err = execute(deps.as_mut(), mock_env(), info.clone(), yes_vote.clone()).unwrap_err();
755 assert_eq!(err, ContractError::AlreadyVoted {});
756 assert_eq!(tally, get_tally(deps.as_ref(), proposal_id));
757
758 let env = match voting_period {
760 Duration::Time(duration) => mock_env_time(duration + 1),
761 Duration::Height(duration) => mock_env_height(duration + 1),
762 };
763 let err = execute(deps.as_mut(), env, info, no_vote).unwrap_err();
764 assert_eq!(err, ContractError::Expired {});
765
766 let info = mock_info(VOTER4, &[]);
768 let res = execute(deps.as_mut(), mock_env(), info, yes_vote.clone()).unwrap();
769
770 assert_eq!(
772 res,
773 Response::new()
774 .add_attribute("action", "vote")
775 .add_attribute("sender", VOTER4)
776 .add_attribute("proposal_id", proposal_id.to_string())
777 .add_attribute("status", "Passed")
778 );
779
780 let info = mock_info(VOTER5, &[]);
782 let res = execute(deps.as_mut(), mock_env(), info, yes_vote).unwrap();
783
784 assert_eq!(
786 res,
787 Response::new()
788 .add_attribute("action", "vote")
789 .add_attribute("sender", VOTER5)
790 .add_attribute("proposal_id", proposal_id.to_string())
791 .add_attribute("status", "Passed")
792 );
793
794 let info = mock_info(OWNER, &[]);
796 let bank_msg = BankMsg::Send {
797 to_address: SOMEBODY.into(),
798 amount: vec![coin(1, "BTC")],
799 };
800 let msgs = vec![CosmosMsg::Bank(bank_msg)];
801 let proposal = ExecuteMsg::Propose {
802 title: "Pay somebody".to_string(),
803 description: "Do I pay her?".to_string(),
804 msgs,
805 latest: None,
806 };
807 let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
808
809 let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
811
812 let no_vote = ExecuteMsg::Vote {
814 proposal_id,
815 vote: Vote::No,
816 };
817 let info = mock_info(VOTER1, &[]);
819 let res = execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
820
821 assert_eq!(
823 res,
824 Response::new()
825 .add_attribute("action", "vote")
826 .add_attribute("sender", VOTER1)
827 .add_attribute("proposal_id", proposal_id.to_string())
828 .add_attribute("status", "Open")
829 );
830
831 let info = mock_info(VOTER4, &[]);
833 let res = execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
834
835 assert_eq!(
837 res,
838 Response::new()
839 .add_attribute("action", "vote")
840 .add_attribute("sender", VOTER4)
841 .add_attribute("proposal_id", proposal_id.to_string())
842 .add_attribute("status", "Open")
843 );
844
845 let info = mock_info(VOTER3, &[]);
847 let _res = execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
848
849 let info = mock_info(VOTER5, &[]);
851 let res = execute(deps.as_mut(), mock_env(), info, no_vote.clone()).unwrap();
852
853 assert_eq!(
855 res,
856 Response::new()
857 .add_attribute("action", "vote")
858 .add_attribute("sender", VOTER5)
859 .add_attribute("proposal_id", proposal_id.to_string())
860 .add_attribute("status", "Open")
861 );
862
863 let info = mock_info(VOTER2, &[]);
866 let res = execute(deps.as_mut(), mock_env(), info, no_vote).unwrap();
867
868 assert_eq!(
870 res,
871 Response::new()
872 .add_attribute("action", "vote")
873 .add_attribute("sender", VOTER2)
874 .add_attribute("proposal_id", proposal_id.to_string())
875 .add_attribute("status", "Rejected")
876 );
877
878 let info = mock_info(VOTER6, &[]);
880 let yes_vote = ExecuteMsg::Vote {
881 proposal_id,
882 vote: Vote::Yes,
883 };
884 let res = execute(deps.as_mut(), mock_env(), info, yes_vote).unwrap();
885
886 assert_eq!(
888 res,
889 Response::new()
890 .add_attribute("action", "vote")
891 .add_attribute("sender", VOTER6)
892 .add_attribute("proposal_id", proposal_id.to_string())
893 .add_attribute("status", "Rejected")
894 );
895 }
896
897 #[test]
898 fn test_execute_works() {
899 let mut deps = mock_dependencies();
900
901 let threshold = Threshold::AbsoluteCount { weight: 3 };
902 let voting_period = Duration::Time(2000000);
903
904 let info = mock_info(OWNER, &[]);
905 setup_test_case(deps.as_mut(), info.clone(), threshold, voting_period).unwrap();
906
907 let bank_msg = BankMsg::Send {
909 to_address: SOMEBODY.into(),
910 amount: vec![coin(1, "BTC")],
911 };
912 let msgs = vec![CosmosMsg::Bank(bank_msg)];
913 let proposal = ExecuteMsg::Propose {
914 title: "Pay somebody".to_string(),
915 description: "Do I pay her?".to_string(),
916 msgs: msgs.clone(),
917 latest: None,
918 };
919 let res = execute(deps.as_mut(), mock_env(), info.clone(), proposal).unwrap();
920
921 let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
923
924 let execution = ExecuteMsg::Execute { proposal_id };
926 let err = execute(deps.as_mut(), mock_env(), info, execution.clone()).unwrap_err();
927 assert_eq!(err, ContractError::WrongExecuteStatus {});
928
929 let vote = ExecuteMsg::Vote {
931 proposal_id,
932 vote: Vote::Yes,
933 };
934 let info = mock_info(VOTER3, &[]);
935 let res = execute(deps.as_mut(), mock_env(), info.clone(), vote).unwrap();
936
937 assert_eq!(
939 res,
940 Response::new()
941 .add_attribute("action", "vote")
942 .add_attribute("sender", VOTER3)
943 .add_attribute("proposal_id", proposal_id.to_string())
944 .add_attribute("status", "Passed")
945 );
946
947 let closing = ExecuteMsg::Close { proposal_id };
949 let err = execute(deps.as_mut(), mock_env(), info, closing).unwrap_err();
950 assert_eq!(err, ContractError::WrongCloseStatus {});
951
952 let info = mock_info(SOMEBODY, &[]);
954 let res = execute(deps.as_mut(), mock_env(), info.clone(), execution).unwrap();
955
956 assert_eq!(
958 res,
959 Response::new()
960 .add_messages(msgs)
961 .add_attribute("action", "execute")
962 .add_attribute("sender", SOMEBODY)
963 .add_attribute("proposal_id", proposal_id.to_string())
964 );
965
966 let closing = ExecuteMsg::Close { proposal_id };
968 let err = execute(deps.as_mut(), mock_env(), info, closing).unwrap_err();
969 assert_eq!(err, ContractError::WrongCloseStatus {});
970 }
971
972 #[test]
973 fn proposal_pass_on_expiration() {
974 let mut deps = mock_dependencies();
975
976 let threshold = Threshold::ThresholdQuorum {
977 threshold: Decimal::percent(51),
978 quorum: Decimal::percent(1),
979 };
980 let voting_period = Duration::Time(2000000);
981
982 let info = mock_info(OWNER, &[]);
983 setup_test_case(deps.as_mut(), info.clone(), threshold, voting_period).unwrap();
984
985 let bank_msg = BankMsg::Send {
987 to_address: SOMEBODY.into(),
988 amount: vec![coin(1, "BTC")],
989 };
990 let msgs = vec![CosmosMsg::Bank(bank_msg)];
991 let proposal = ExecuteMsg::Propose {
992 title: "Pay somebody".to_string(),
993 description: "Do I pay her?".to_string(),
994 msgs,
995 latest: None,
996 };
997 let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
998
999 let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
1001
1002 let vote = ExecuteMsg::Vote {
1004 proposal_id,
1005 vote: Vote::Yes,
1006 };
1007 let info = mock_info(VOTER3, &[]);
1008 let res = execute(deps.as_mut(), mock_env(), info, vote).unwrap();
1009 assert_eq!(
1010 res,
1011 Response::new()
1012 .add_attribute("action", "vote")
1013 .add_attribute("sender", VOTER3)
1014 .add_attribute("proposal_id", proposal_id.to_string())
1015 .add_attribute("status", "Open")
1016 );
1017
1018 let env = match voting_period {
1020 Duration::Time(duration) => mock_env_time(duration + 1),
1021 Duration::Height(duration) => mock_env_height(duration + 1),
1022 };
1023
1024 let prop: ProposalResponse = from_json(
1026 query(
1027 deps.as_ref(),
1028 env.clone(),
1029 QueryMsg::Proposal { proposal_id },
1030 )
1031 .unwrap(),
1032 )
1033 .unwrap();
1034 assert_eq!(prop.status, Status::Passed);
1035
1036 let info = mock_info(SOMEBODY, &[]);
1038 let err = execute(
1039 deps.as_mut(),
1040 env.clone(),
1041 info.clone(),
1042 ExecuteMsg::Close { proposal_id },
1043 )
1044 .unwrap_err();
1045 assert_eq!(err, ContractError::WrongCloseStatus {});
1046
1047 let res = execute(
1049 deps.as_mut(),
1050 env,
1051 info,
1052 ExecuteMsg::Execute { proposal_id },
1053 )
1054 .unwrap();
1055 assert_eq!(
1056 res.attributes,
1057 Response::<Empty>::new()
1058 .add_attribute("action", "execute")
1059 .add_attribute("sender", SOMEBODY)
1060 .add_attribute("proposal_id", proposal_id.to_string())
1061 .attributes
1062 )
1063 }
1064
1065 #[test]
1066 fn test_close_works() {
1067 let mut deps = mock_dependencies();
1068
1069 let threshold = Threshold::AbsoluteCount { weight: 3 };
1070 let voting_period = Duration::Height(2000000);
1071
1072 let info = mock_info(OWNER, &[]);
1073 setup_test_case(deps.as_mut(), info.clone(), threshold, voting_period).unwrap();
1074
1075 let bank_msg = BankMsg::Send {
1077 to_address: SOMEBODY.into(),
1078 amount: vec![coin(1, "BTC")],
1079 };
1080 let msgs = vec![CosmosMsg::Bank(bank_msg)];
1081 let proposal = ExecuteMsg::Propose {
1082 title: "Pay somebody".to_string(),
1083 description: "Do I pay her?".to_string(),
1084 msgs: msgs.clone(),
1085 latest: None,
1086 };
1087 let res = execute(deps.as_mut(), mock_env(), info, proposal).unwrap();
1088
1089 let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
1091
1092 let closing = ExecuteMsg::Close { proposal_id };
1093
1094 let info = mock_info(SOMEBODY, &[]);
1096
1097 let err = execute(deps.as_mut(), mock_env(), info, closing).unwrap_err();
1099 assert_eq!(err, ContractError::NotExpired {});
1100
1101 let info = mock_info(OWNER, &[]);
1103
1104 let proposal = ExecuteMsg::Propose {
1105 title: "(Try to) pay somebody".to_string(),
1106 description: "Pay somebody after time?".to_string(),
1107 msgs,
1108 latest: Some(Expiration::AtHeight(123456)),
1109 };
1110 let res = execute(deps.as_mut(), mock_env(), info.clone(), proposal).unwrap();
1111
1112 let proposal_id: u64 = res.attributes[2].value.parse().unwrap();
1114
1115 let closing = ExecuteMsg::Close { proposal_id };
1116
1117 let env = mock_env_height(1234567);
1119 let res = execute(
1120 deps.as_mut(),
1121 env,
1122 mock_info(SOMEBODY, &[]),
1123 closing.clone(),
1124 )
1125 .unwrap();
1126
1127 assert_eq!(
1129 res,
1130 Response::new()
1131 .add_attribute("action", "close")
1132 .add_attribute("sender", SOMEBODY)
1133 .add_attribute("proposal_id", proposal_id.to_string())
1134 );
1135
1136 let err = execute(deps.as_mut(), mock_env(), info, closing).unwrap_err();
1138 assert_eq!(err, ContractError::WrongCloseStatus {});
1139 }
1140}