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;
11
12use cw3::{
13 Ballot, Proposal, ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo,
14 VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse, Votes,
15};
16use cw3_fixed_multisig::state::{next_id, BALLOTS, PROPOSALS};
17use cw4::{Cw4Contract, MemberChangedHookMsg, MemberDiff};
18use cw_storage_plus::Bound;
19use cw_utils::{maybe_addr, Expiration, ThresholdResponse};
20
21use crate::error::ContractError;
22use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
23use crate::state::{Config, CONFIG};
24
25const CONTRACT_NAME: &str = "crates.io:cw3-flex-multisig";
27const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
28
29#[cfg_attr(not(feature = "library"), entry_point)]
30pub fn instantiate(
31 deps: DepsMut,
32 _env: Env,
33 _info: MessageInfo,
34 msg: InstantiateMsg,
35) -> Result<Response, ContractError> {
36 let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| {
37 ContractError::InvalidGroup {
38 addr: msg.group_addr.clone(),
39 }
40 })?);
41 let total_weight = group_addr.total_weight(&deps.querier)?;
42 msg.threshold.validate(total_weight)?;
43
44 let proposal_deposit = msg
45 .proposal_deposit
46 .map(|deposit| deposit.into_checked(deps.as_ref()))
47 .transpose()?;
48
49 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
50
51 let cfg = Config {
52 threshold: msg.threshold,
53 max_voting_period: msg.max_voting_period,
54 group_addr,
55 executor: msg.executor,
56 proposal_deposit,
57 };
58 CONFIG.save(deps.storage, &cfg)?;
59
60 Ok(Response::default())
61}
62
63#[cfg_attr(not(feature = "library"), entry_point)]
64pub fn execute(
65 deps: DepsMut,
66 env: Env,
67 info: MessageInfo,
68 msg: ExecuteMsg,
69) -> Result<Response<Empty>, ContractError> {
70 match msg {
71 ExecuteMsg::Propose {
72 title,
73 description,
74 msgs,
75 latest,
76 } => execute_propose(deps, env, info, title, description, msgs, latest),
77 ExecuteMsg::Vote { proposal_id, vote } => execute_vote(deps, env, info, proposal_id, vote),
78 ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id),
79 ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id),
80 ExecuteMsg::MemberChangedHook(MemberChangedHookMsg { diffs }) => {
81 execute_membership_hook(deps, env, info, diffs)
82 }
83 }
84}
85
86pub fn execute_propose(
87 deps: DepsMut,
88 env: Env,
89 info: MessageInfo,
90 title: String,
91 description: String,
92 msgs: Vec<CosmosMsg>,
93 latest: Option<Expiration>,
95) -> Result<Response<Empty>, ContractError> {
96 let cfg = CONFIG.load(deps.storage)?;
98
99 if let Some(deposit) = cfg.proposal_deposit.as_ref() {
101 deposit.check_native_deposit_paid(&info)?;
102 }
103
104 let vote_power = cfg
110 .group_addr
111 .is_member(&deps.querier, &info.sender, None)?
112 .ok_or(ContractError::Unauthorized {})?;
113
114 let max_expires = cfg.max_voting_period.after(&env.block);
116 let mut expires = latest.unwrap_or(max_expires);
117 let comp = expires.partial_cmp(&max_expires);
118 if let Some(Ordering::Greater) = comp {
119 expires = max_expires;
120 } else if comp.is_none() {
121 return Err(ContractError::WrongExpiration {});
122 }
123
124 let take_deposit_msg = if let Some(deposit_info) = cfg.proposal_deposit.as_ref() {
128 deposit_info.get_take_deposit_messages(&info.sender, &env.contract.address)?
129 } else {
130 vec![]
131 };
132
133 let mut prop = Proposal {
135 title,
136 description,
137 start_height: env.block.height,
138 expires,
139 msgs,
140 status: Status::Open,
141 votes: Votes::yes(vote_power),
142 threshold: cfg.threshold,
143 total_weight: cfg.group_addr.total_weight(&deps.querier)?,
144 proposer: info.sender.clone(),
145 deposit: cfg.proposal_deposit,
146 };
147 prop.update_status(&env.block);
148 let id = next_id(deps.storage)?;
149 PROPOSALS.save(deps.storage, id, &prop)?;
150
151 let ballot = Ballot {
153 weight: vote_power,
154 vote: Vote::Yes,
155 };
156 BALLOTS.save(deps.storage, (id, &info.sender), &ballot)?;
157
158 Ok(Response::new()
159 .add_messages(take_deposit_msg)
160 .add_attribute("action", "propose")
161 .add_attribute("sender", info.sender)
162 .add_attribute("proposal_id", id.to_string())
163 .add_attribute("status", format!("{:?}", prop.status)))
164}
165
166pub fn execute_vote(
167 deps: DepsMut,
168 env: Env,
169 info: MessageInfo,
170 proposal_id: u64,
171 vote: Vote,
172) -> Result<Response<Empty>, ContractError> {
173 let cfg = CONFIG.load(deps.storage)?;
175
176 let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
178 if ![Status::Open, Status::Passed, Status::Rejected].contains(&prop.status) {
180 return Err(ContractError::NotOpen {});
181 }
182 if prop.expires.is_expired(&env.block) {
184 return Err(ContractError::Expired {});
185 }
186
187 let vote_power = cfg
191 .group_addr
192 .is_voting_member(&deps.querier, &info.sender, prop.start_height)?
193 .ok_or(ContractError::Unauthorized {})?;
194
195 BALLOTS.update(deps.storage, (proposal_id, &info.sender), |bal| match bal {
197 Some(_) => Err(ContractError::AlreadyVoted {}),
198 None => Ok(Ballot {
199 weight: vote_power,
200 vote,
201 }),
202 })?;
203
204 prop.votes.add_vote(vote, vote_power);
206 prop.update_status(&env.block);
207 PROPOSALS.save(deps.storage, proposal_id, &prop)?;
208
209 Ok(Response::new()
210 .add_attribute("action", "vote")
211 .add_attribute("sender", info.sender)
212 .add_attribute("proposal_id", proposal_id.to_string())
213 .add_attribute("status", format!("{:?}", prop.status)))
214}
215
216pub fn execute_execute(
217 deps: DepsMut,
218 env: Env,
219 info: MessageInfo,
220 proposal_id: u64,
221) -> Result<Response, ContractError> {
222 let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
223 prop.update_status(&env.block);
226 if prop.status != Status::Passed {
227 return Err(ContractError::WrongExecuteStatus {});
228 }
229
230 let cfg = CONFIG.load(deps.storage)?;
231 cfg.authorize(&deps.querier, &info.sender)?;
232
233 prop.status = Status::Executed;
235 PROPOSALS.save(deps.storage, proposal_id, &prop)?;
236
237 let response = match prop.deposit {
239 Some(deposit) => {
240 Response::new().add_message(deposit.get_return_deposit_message(&prop.proposer)?)
241 }
242 None => Response::new(),
243 };
244
245 Ok(response
247 .add_messages(prop.msgs)
248 .add_attribute("action", "execute")
249 .add_attribute("sender", info.sender)
250 .add_attribute("proposal_id", proposal_id.to_string()))
251}
252
253pub fn execute_close(
254 deps: DepsMut,
255 env: Env,
256 info: MessageInfo,
257 proposal_id: u64,
258) -> Result<Response<Empty>, ContractError> {
259 let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
262 if [Status::Executed, Status::Rejected, Status::Passed].contains(&prop.status) {
263 return Err(ContractError::WrongCloseStatus {});
264 }
265 if prop.current_status(&env.block) == Status::Passed {
267 return Err(ContractError::WrongCloseStatus {});
268 }
269 if !prop.expires.is_expired(&env.block) {
270 return Err(ContractError::NotExpired {});
271 }
272
273 prop.status = Status::Rejected;
275 PROPOSALS.save(deps.storage, proposal_id, &prop)?;
276
277 let mut response = Response::new();
279 if let Some(deposit) = prop.deposit {
280 if deposit.refund_failed_proposals {
281 response = response.add_message(deposit.get_return_deposit_message(&prop.proposer)?)
282 }
283 }
284
285 Ok(response
286 .add_attribute("action", "close")
287 .add_attribute("sender", info.sender)
288 .add_attribute("proposal_id", proposal_id.to_string()))
289}
290
291pub fn execute_membership_hook(
292 deps: DepsMut,
293 _env: Env,
294 info: MessageInfo,
295 _diffs: Vec<MemberDiff>,
296) -> Result<Response<Empty>, ContractError> {
297 let cfg = CONFIG.load(deps.storage)?;
300 if info.sender != cfg.group_addr.0 {
301 return Err(ContractError::Unauthorized {});
302 }
303
304 Ok(Response::default())
305}
306
307#[cfg_attr(not(feature = "library"), entry_point)]
308pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
309 match msg {
310 QueryMsg::Threshold {} => to_json_binary(&query_threshold(deps)?),
311 QueryMsg::Proposal { proposal_id } => {
312 to_json_binary(&query_proposal(deps, env, proposal_id)?)
313 }
314 QueryMsg::Vote { proposal_id, voter } => {
315 to_json_binary(&query_vote(deps, proposal_id, voter)?)
316 }
317 QueryMsg::ListProposals { start_after, limit } => {
318 to_json_binary(&list_proposals(deps, env, start_after, limit)?)
319 }
320 QueryMsg::ReverseProposals {
321 start_before,
322 limit,
323 } => to_json_binary(&reverse_proposals(deps, env, start_before, limit)?),
324 QueryMsg::ListVotes {
325 proposal_id,
326 start_after,
327 limit,
328 } => to_json_binary(&list_votes(deps, proposal_id, start_after, limit)?),
329 QueryMsg::Voter { address } => to_json_binary(&query_voter(deps, address)?),
330 QueryMsg::ListVoters { start_after, limit } => {
331 to_json_binary(&list_voters(deps, start_after, limit)?)
332 }
333 QueryMsg::Config {} => to_json_binary(&query_config(deps)?),
334 }
335}
336
337fn query_threshold(deps: Deps) -> StdResult<ThresholdResponse> {
338 let cfg = CONFIG.load(deps.storage)?;
339 let total_weight = cfg.group_addr.total_weight(&deps.querier)?;
340 Ok(cfg.threshold.to_response(total_weight))
341}
342
343fn query_config(deps: Deps) -> StdResult<Config> {
344 CONFIG.load(deps.storage)
345}
346
347fn query_proposal(deps: Deps, env: Env, id: u64) -> StdResult<ProposalResponse> {
348 let prop = PROPOSALS.load(deps.storage, id)?;
349 let status = prop.current_status(&env.block);
350 let threshold = prop.threshold.to_response(prop.total_weight);
351 Ok(ProposalResponse {
352 id,
353 title: prop.title,
354 description: prop.description,
355 msgs: prop.msgs,
356 status,
357 expires: prop.expires,
358 proposer: prop.proposer,
359 deposit: prop.deposit,
360 threshold,
361 })
362}
363
364const MAX_LIMIT: u32 = 30;
366const DEFAULT_LIMIT: u32 = 10;
367
368fn list_proposals(
369 deps: Deps,
370 env: Env,
371 start_after: Option<u64>,
372 limit: Option<u32>,
373) -> StdResult<ProposalListResponse> {
374 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
375 let start = start_after.map(Bound::exclusive);
376 let proposals = PROPOSALS
377 .range(deps.storage, start, None, Order::Ascending)
378 .take(limit)
379 .map(|p| map_proposal(&env.block, p))
380 .collect::<StdResult<_>>()?;
381
382 Ok(ProposalListResponse { proposals })
383}
384
385fn reverse_proposals(
386 deps: Deps,
387 env: Env,
388 start_before: Option<u64>,
389 limit: Option<u32>,
390) -> StdResult<ProposalListResponse> {
391 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
392 let end = start_before.map(Bound::exclusive);
393 let props: StdResult<Vec<_>> = PROPOSALS
394 .range(deps.storage, None, end, Order::Descending)
395 .take(limit)
396 .map(|p| map_proposal(&env.block, p))
397 .collect();
398
399 Ok(ProposalListResponse { proposals: props? })
400}
401
402fn map_proposal(
403 block: &BlockInfo,
404 item: StdResult<(u64, Proposal)>,
405) -> StdResult<ProposalResponse> {
406 item.map(|(id, prop)| {
407 let status = prop.current_status(block);
408 let threshold = prop.threshold.to_response(prop.total_weight);
409 ProposalResponse {
410 id,
411 title: prop.title,
412 description: prop.description,
413 msgs: prop.msgs,
414 status,
415 expires: prop.expires,
416 deposit: prop.deposit,
417 proposer: prop.proposer,
418 threshold,
419 }
420 })
421}
422
423fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult<VoteResponse> {
424 let voter_addr = deps.api.addr_validate(&voter)?;
425 let prop = BALLOTS.may_load(deps.storage, (proposal_id, &voter_addr))?;
426 let vote = prop.map(|b| VoteInfo {
427 proposal_id,
428 voter,
429 vote: b.vote,
430 weight: b.weight,
431 });
432 Ok(VoteResponse { vote })
433}
434
435fn list_votes(
436 deps: Deps,
437 proposal_id: u64,
438 start_after: Option<String>,
439 limit: Option<u32>,
440) -> StdResult<VoteListResponse> {
441 let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
442 let addr = maybe_addr(deps.api, start_after)?;
443 let start = addr.as_ref().map(Bound::exclusive);
444
445 let votes = BALLOTS
446 .prefix(proposal_id)
447 .range(deps.storage, start, None, Order::Ascending)
448 .take(limit)
449 .map(|item| {
450 item.map(|(addr, ballot)| VoteInfo {
451 proposal_id,
452 voter: addr.into(),
453 vote: ballot.vote,
454 weight: ballot.weight,
455 })
456 })
457 .collect::<StdResult<_>>()?;
458
459 Ok(VoteListResponse { votes })
460}
461
462fn query_voter(deps: Deps, voter: String) -> StdResult<VoterResponse> {
463 let cfg = CONFIG.load(deps.storage)?;
464 let voter_addr = deps.api.addr_validate(&voter)?;
465 let weight = cfg.group_addr.is_member(&deps.querier, &voter_addr, None)?;
466
467 Ok(VoterResponse { weight })
468}
469
470fn list_voters(
471 deps: Deps,
472 start_after: Option<String>,
473 limit: Option<u32>,
474) -> StdResult<VoterListResponse> {
475 let cfg = CONFIG.load(deps.storage)?;
476 let voters = cfg
477 .group_addr
478 .list_members(&deps.querier, start_after, limit)?
479 .into_iter()
480 .map(|member| VoterDetail {
481 addr: member.addr,
482 weight: member.weight,
483 })
484 .collect();
485 Ok(VoterListResponse { voters })
486}
487
488#[cfg(test)]
489mod tests {
490 use cosmwasm_std::{coin, coins, Addr, BankMsg, Coin, Decimal, Timestamp, Uint128};
491
492 use cw2::{query_contract_info, ContractVersion};
493 use cw20::{Cw20Coin, UncheckedDenom};
494 use cw3::{DepositError, UncheckedDepositInfo};
495 use cw4::{Cw4ExecuteMsg, Member};
496 use cw4_group::helpers::Cw4GroupContract;
497 use cw_multi_test::{
498 next_block, App, AppBuilder, BankSudo, Contract, ContractWrapper, Executor, SudoMsg,
499 };
500 use cw_utils::{Duration, Threshold};
501
502 use easy_addr::addr;
503
504 use super::*;
505
506 const OWNER: &str = addr!("admin0001");
507 const VOTER1: &str = addr!("voter0001");
508 const VOTER2: &str = addr!("voter0002");
509 const VOTER3: &str = addr!("voter0003");
510 const VOTER4: &str = addr!("voter0004");
511 const VOTER5: &str = addr!("voter0005");
512 const SOMEBODY: &str = addr!("somebody");
513 const NEWBIE: &str = addr!("newbie");
514
515 fn member<T: Into<String>>(addr: T, weight: u64) -> Member {
516 Member {
517 addr: addr.into(),
518 weight,
519 }
520 }
521
522 pub fn contract_flex() -> Box<dyn Contract<Empty>> {
523 let contract = ContractWrapper::new(
524 crate::contract::execute,
525 crate::contract::instantiate,
526 crate::contract::query,
527 );
528 Box::new(contract)
529 }
530
531 pub fn contract_group() -> Box<dyn Contract<Empty>> {
532 let contract = ContractWrapper::new(
533 cw4_group::contract::execute,
534 cw4_group::contract::instantiate,
535 cw4_group::contract::query,
536 );
537 Box::new(contract)
538 }
539
540 fn contract_cw20() -> Box<dyn Contract<Empty>> {
541 let contract = ContractWrapper::new(
542 cw20_base::contract::execute,
543 cw20_base::contract::instantiate,
544 cw20_base::contract::query,
545 );
546 Box::new(contract)
547 }
548
549 fn mock_app(init_funds: &[Coin]) -> App {
550 AppBuilder::new().build(|router, _, storage| {
551 router
552 .bank
553 .init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec())
554 .unwrap();
555 })
556 }
557
558 fn instantiate_group(app: &mut App, members: Vec<Member>) -> Addr {
560 let group_id = app.store_code(contract_group());
561 let msg = cw4_group::msg::InstantiateMsg {
562 admin: Some(OWNER.into()),
563 members,
564 };
565 app.instantiate_contract(group_id, Addr::unchecked(OWNER), &msg, &[], "group", None)
566 .unwrap()
567 }
568
569 #[track_caller]
570 fn instantiate_flex(
571 app: &mut App,
572 group: Addr,
573 threshold: Threshold,
574 max_voting_period: Duration,
575 executor: Option<crate::state::Executor>,
576 proposal_deposit: Option<UncheckedDepositInfo>,
577 ) -> Addr {
578 let flex_id = app.store_code(contract_flex());
579 let msg = crate::msg::InstantiateMsg {
580 group_addr: group.to_string(),
581 threshold,
582 max_voting_period,
583 executor,
584 proposal_deposit,
585 };
586 app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "flex", None)
587 .unwrap()
588 }
589
590 #[track_caller]
594 fn setup_test_case_fixed(
595 app: &mut App,
596 weight_needed: u64,
597 max_voting_period: Duration,
598 init_funds: Vec<Coin>,
599 multisig_as_group_admin: bool,
600 ) -> (Addr, Addr) {
601 setup_test_case(
602 app,
603 Threshold::AbsoluteCount {
604 weight: weight_needed,
605 },
606 max_voting_period,
607 init_funds,
608 multisig_as_group_admin,
609 None,
610 None,
611 )
612 }
613
614 #[track_caller]
615 fn setup_test_case(
616 app: &mut App,
617 threshold: Threshold,
618 max_voting_period: Duration,
619 init_funds: Vec<Coin>,
620 multisig_as_group_admin: bool,
621 executor: Option<crate::state::Executor>,
622 proposal_deposit: Option<UncheckedDepositInfo>,
623 ) -> (Addr, Addr) {
624 let members = vec![
626 member(OWNER, 0),
627 member(VOTER1, 1),
628 member(VOTER2, 2),
629 member(VOTER3, 3),
630 member(VOTER4, 12), member(VOTER5, 5),
632 ];
633 let group_addr = instantiate_group(app, members);
634 app.update_block(next_block);
635
636 let flex_addr = instantiate_flex(
638 app,
639 group_addr.clone(),
640 threshold,
641 max_voting_period,
642 executor,
643 proposal_deposit,
644 );
645 app.update_block(next_block);
646
647 if multisig_as_group_admin {
649 let update_admin = Cw4ExecuteMsg::UpdateAdmin {
650 admin: Some(flex_addr.to_string()),
651 };
652 app.execute_contract(
653 Addr::unchecked(OWNER),
654 group_addr.clone(),
655 &update_admin,
656 &[],
657 )
658 .unwrap();
659 app.update_block(next_block);
660 }
661
662 if !init_funds.is_empty() {
664 app.send_tokens(Addr::unchecked(OWNER), flex_addr.clone(), &init_funds)
665 .unwrap();
666 }
667 (flex_addr, group_addr)
668 }
669
670 fn proposal_info() -> (Vec<CosmosMsg<Empty>>, String, String) {
671 let bank_msg = BankMsg::Send {
672 to_address: SOMEBODY.into(),
673 amount: coins(1, "BTC"),
674 };
675 let msgs = vec![bank_msg.into()];
676 let title = "Pay somebody".to_string();
677 let description = "Do I pay her?".to_string();
678 (msgs, title, description)
679 }
680
681 fn pay_somebody_proposal() -> ExecuteMsg {
682 let (msgs, title, description) = proposal_info();
683 ExecuteMsg::Propose {
684 title,
685 description,
686 msgs,
687 latest: None,
688 }
689 }
690
691 fn text_proposal() -> ExecuteMsg {
692 let (_, title, description) = proposal_info();
693 ExecuteMsg::Propose {
694 title,
695 description,
696 msgs: vec![],
697 latest: None,
698 }
699 }
700
701 #[test]
702 fn test_instantiate_works() {
703 let mut app = mock_app(&[]);
704
705 let group_addr = instantiate_group(&mut app, vec![member(OWNER, 1)]);
707 let flex_id = app.store_code(contract_flex());
708
709 let max_voting_period = Duration::Time(1234567);
710
711 let instantiate_msg = InstantiateMsg {
713 group_addr: group_addr.to_string(),
714 threshold: Threshold::ThresholdQuorum {
715 threshold: Decimal::zero(),
716 quorum: Decimal::percent(1),
717 },
718 max_voting_period,
719 executor: None,
720 proposal_deposit: None,
721 };
722 let err = app
723 .instantiate_contract(
724 flex_id,
725 Addr::unchecked(OWNER),
726 &instantiate_msg,
727 &[],
728 "zero required weight",
729 None,
730 )
731 .unwrap_err();
732 assert_eq!(
733 ContractError::Threshold(cw_utils::ThresholdError::InvalidThreshold {}),
734 err.downcast().unwrap()
735 );
736
737 let instantiate_msg = InstantiateMsg {
739 group_addr: group_addr.to_string(),
740 threshold: Threshold::AbsoluteCount { weight: 100 },
741 max_voting_period,
742 executor: None,
743 proposal_deposit: None,
744 };
745 let err = app
746 .instantiate_contract(
747 flex_id,
748 Addr::unchecked(OWNER),
749 &instantiate_msg,
750 &[],
751 "high required weight",
752 None,
753 )
754 .unwrap_err();
755 assert_eq!(
756 ContractError::Threshold(cw_utils::ThresholdError::UnreachableWeight {}),
757 err.downcast().unwrap()
758 );
759
760 let instantiate_msg = InstantiateMsg {
762 group_addr: group_addr.to_string(),
763 threshold: Threshold::AbsoluteCount { weight: 1 },
764 max_voting_period,
765 executor: None,
766 proposal_deposit: None,
767 };
768 let flex_addr = app
769 .instantiate_contract(
770 flex_id,
771 Addr::unchecked(OWNER),
772 &instantiate_msg,
773 &[],
774 "all good",
775 None,
776 )
777 .unwrap();
778
779 let version = query_contract_info(&app.wrap(), flex_addr.clone()).unwrap();
781 assert_eq!(
782 ContractVersion {
783 contract: CONTRACT_NAME.to_string(),
784 version: CONTRACT_VERSION.to_string(),
785 },
786 version,
787 );
788
789 let voters: VoterListResponse = app
791 .wrap()
792 .query_wasm_smart(
793 &flex_addr,
794 &QueryMsg::ListVoters {
795 start_after: None,
796 limit: None,
797 },
798 )
799 .unwrap();
800 assert_eq!(
801 voters.voters,
802 vec![VoterDetail {
803 addr: OWNER.into(),
804 weight: 1
805 }]
806 );
807 }
808
809 #[test]
810 fn test_propose_works() {
811 let init_funds = coins(10, "BTC");
812 let mut app = mock_app(&init_funds);
813
814 let required_weight = 4;
815 let voting_period = Duration::Time(2000000);
816 let (flex_addr, _) =
817 setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, false);
818
819 let proposal = pay_somebody_proposal();
820 let err = app
822 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &proposal, &[])
823 .unwrap_err();
824 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
825
826 let msgs = match proposal.clone() {
828 ExecuteMsg::Propose { msgs, .. } => msgs,
829 _ => panic!("Wrong variant"),
830 };
831 let proposal_wrong_exp = ExecuteMsg::Propose {
832 title: "Rewarding somebody".to_string(),
833 description: "Do we reward her?".to_string(),
834 msgs,
835 latest: Some(Expiration::AtHeight(123456)),
836 };
837 let err = app
838 .execute_contract(
839 Addr::unchecked(OWNER),
840 flex_addr.clone(),
841 &proposal_wrong_exp,
842 &[],
843 )
844 .unwrap_err();
845 assert_eq!(ContractError::WrongExpiration {}, err.downcast().unwrap());
846
847 let res = app
849 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
850 .unwrap();
851 assert_eq!(
852 res.custom_attrs(1),
853 [
854 ("action", "propose"),
855 ("sender", VOTER3),
856 ("proposal_id", "1"),
857 ("status", "Open"),
858 ],
859 );
860
861 let res = app
863 .execute_contract(Addr::unchecked(VOTER4), flex_addr, &proposal, &[])
864 .unwrap();
865 assert_eq!(
866 res.custom_attrs(1),
867 [
868 ("action", "propose"),
869 ("sender", VOTER4),
870 ("proposal_id", "2"),
871 ("status", "Passed"),
872 ],
873 );
874 }
875
876 fn get_tally(app: &App, flex_addr: &str, proposal_id: u64) -> u64 {
877 let voters = QueryMsg::ListVotes {
879 proposal_id,
880 start_after: None,
881 limit: None,
882 };
883 let votes: VoteListResponse = app.wrap().query_wasm_smart(flex_addr, &voters).unwrap();
884 votes
886 .votes
887 .iter()
888 .filter(|&v| v.vote == Vote::Yes)
889 .map(|v| v.weight)
890 .sum()
891 }
892
893 fn expire(voting_period: Duration) -> impl Fn(&mut BlockInfo) {
894 move |block: &mut BlockInfo| {
895 match voting_period {
896 Duration::Time(duration) => block.time = block.time.plus_seconds(duration + 1),
897 Duration::Height(duration) => block.height += duration + 1,
898 };
899 }
900 }
901
902 fn unexpire(voting_period: Duration) -> impl Fn(&mut BlockInfo) {
903 move |block: &mut BlockInfo| {
904 match voting_period {
905 Duration::Time(duration) => {
906 block.time =
907 Timestamp::from_nanos(block.time.nanos() - (duration * 1_000_000_000));
908 }
909 Duration::Height(duration) => block.height -= duration,
910 };
911 }
912 }
913
914 #[test]
915 fn test_proposal_queries() {
916 let init_funds = coins(10, "BTC");
917 let mut app = mock_app(&init_funds);
918
919 let voting_period = Duration::Time(2000000);
920 let threshold = Threshold::ThresholdQuorum {
921 threshold: Decimal::percent(80),
922 quorum: Decimal::percent(20),
923 };
924 let (flex_addr, _) = setup_test_case(
925 &mut app,
926 threshold,
927 voting_period,
928 init_funds,
929 false,
930 None,
931 None,
932 );
933
934 let proposal = pay_somebody_proposal();
936 let res = app
937 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[])
938 .unwrap();
939 let proposal_id1: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
940
941 app.update_block(next_block);
943 let proposal = pay_somebody_proposal();
944 let res = app
945 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &proposal, &[])
946 .unwrap();
947 let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
948
949 app.update_block(expire(voting_period));
951
952 let proposal = pay_somebody_proposal();
954 let res = app
955 .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &proposal, &[])
956 .unwrap();
957 let proposal_id3: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
958 let proposed_at = app.block_info();
959
960 app.update_block(next_block);
962 let list_query = QueryMsg::ListProposals {
963 start_after: None,
964 limit: None,
965 };
966 let res: ProposalListResponse = app
967 .wrap()
968 .query_wasm_smart(&flex_addr, &list_query)
969 .unwrap();
970 assert_eq!(3, res.proposals.len());
971
972 let info: Vec<_> = res.proposals.iter().map(|p| (p.id, p.status)).collect();
974 let expected_info = vec![
975 (proposal_id1, Status::Rejected),
976 (proposal_id2, Status::Passed),
977 (proposal_id3, Status::Open),
978 ];
979 assert_eq!(expected_info, info);
980
981 let (expected_msgs, expected_title, expected_description) = proposal_info();
983 for prop in res.proposals {
984 assert_eq!(prop.title, expected_title);
985 assert_eq!(prop.description, expected_description);
986 assert_eq!(prop.msgs, expected_msgs);
987 }
988
989 let list_query = QueryMsg::ReverseProposals {
991 start_before: None,
992 limit: Some(1),
993 };
994 let res: ProposalListResponse = app
995 .wrap()
996 .query_wasm_smart(&flex_addr, &list_query)
997 .unwrap();
998 assert_eq!(1, res.proposals.len());
999
1000 let (msgs, title, description) = proposal_info();
1001 let expected = ProposalResponse {
1002 id: proposal_id3,
1003 title,
1004 description,
1005 msgs,
1006 expires: voting_period.after(&proposed_at),
1007 status: Status::Open,
1008 threshold: ThresholdResponse::ThresholdQuorum {
1009 total_weight: 23,
1010 threshold: Decimal::percent(80),
1011 quorum: Decimal::percent(20),
1012 },
1013 proposer: Addr::unchecked(VOTER2),
1014 deposit: None,
1015 };
1016 assert_eq!(&expected, &res.proposals[0]);
1017 }
1018
1019 #[test]
1020 fn test_vote_works() {
1021 let init_funds = coins(10, "BTC");
1022 let mut app = mock_app(&init_funds);
1023
1024 let threshold = Threshold::ThresholdQuorum {
1025 threshold: Decimal::percent(51),
1026 quorum: Decimal::percent(1),
1027 };
1028 let voting_period = Duration::Time(2000000);
1029 let (flex_addr, _) = setup_test_case(
1030 &mut app,
1031 threshold,
1032 voting_period,
1033 init_funds,
1034 false,
1035 None,
1036 None,
1037 );
1038
1039 let proposal = pay_somebody_proposal();
1041 let res = app
1042 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1043 .unwrap();
1044
1045 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1047
1048 let yes_vote = ExecuteMsg::Vote {
1050 proposal_id,
1051 vote: Vote::Yes,
1052 };
1053 let err = app
1054 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &yes_vote, &[])
1055 .unwrap_err();
1056 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1057
1058 let err = app
1060 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &yes_vote, &[])
1061 .unwrap_err();
1062 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1063
1064 let res = app
1066 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &yes_vote, &[])
1067 .unwrap();
1068 assert_eq!(
1069 res.custom_attrs(1),
1070 [
1071 ("action", "vote"),
1072 ("sender", VOTER1),
1073 ("proposal_id", proposal_id.to_string().as_str()),
1074 ("status", "Open"),
1075 ],
1076 );
1077
1078 let err = app
1080 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &yes_vote, &[])
1081 .unwrap_err();
1082 assert_eq!(ContractError::AlreadyVoted {}, err.downcast().unwrap());
1083
1084 let tally = get_tally(&app, flex_addr.as_ref(), proposal_id);
1087 assert_eq!(tally, 1);
1088
1089 let no_vote = ExecuteMsg::Vote {
1091 proposal_id,
1092 vote: Vote::No,
1093 };
1094 let _ = app
1095 .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &no_vote, &[])
1096 .unwrap();
1097
1098 let veto_vote = ExecuteMsg::Vote {
1100 proposal_id,
1101 vote: Vote::Veto,
1102 };
1103 let _ = app
1104 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &veto_vote, &[])
1105 .unwrap();
1106
1107 assert_eq!(tally, get_tally(&app, flex_addr.as_ref(), proposal_id));
1109
1110 let err = app
1111 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1112 .unwrap_err();
1113 assert_eq!(ContractError::AlreadyVoted {}, err.downcast().unwrap());
1114
1115 app.update_block(expire(voting_period));
1117 let err = app
1118 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1119 .unwrap_err();
1120 assert_eq!(ContractError::Expired {}, err.downcast().unwrap());
1121 app.update_block(unexpire(voting_period));
1122
1123 let res = app
1125 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1126 .unwrap();
1127 assert_eq!(
1128 res.custom_attrs(1),
1129 [
1130 ("action", "vote"),
1131 ("sender", VOTER4),
1132 ("proposal_id", proposal_id.to_string().as_str()),
1133 ("status", "Passed"),
1134 ],
1135 );
1136
1137 let res = app
1139 .execute_contract(Addr::unchecked(VOTER5), flex_addr.clone(), &yes_vote, &[])
1140 .unwrap();
1141 assert_eq!(
1143 res.custom_attrs(1),
1144 [
1145 ("action", "vote"),
1146 ("sender", VOTER5),
1147 ("proposal_id", proposal_id.to_string().as_str()),
1148 ("status", "Passed")
1149 ]
1150 );
1151
1152 let voter = OWNER.into();
1155 let vote: VoteResponse = app
1156 .wrap()
1157 .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1158 .unwrap();
1159 assert_eq!(
1160 vote.vote.unwrap(),
1161 VoteInfo {
1162 proposal_id,
1163 voter: OWNER.into(),
1164 vote: Vote::Yes,
1165 weight: 0
1166 }
1167 );
1168
1169 let voter = VOTER2.into();
1171 let vote: VoteResponse = app
1172 .wrap()
1173 .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1174 .unwrap();
1175 assert_eq!(
1176 vote.vote.unwrap(),
1177 VoteInfo {
1178 proposal_id,
1179 voter: VOTER2.into(),
1180 vote: Vote::No,
1181 weight: 2
1182 }
1183 );
1184
1185 let voter = SOMEBODY.into();
1187 let vote: VoteResponse = app
1188 .wrap()
1189 .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1190 .unwrap();
1191 assert!(vote.vote.is_none());
1192
1193 let proposal = pay_somebody_proposal();
1195 let res = app
1196 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1197 .unwrap();
1198
1199 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1201
1202 let no_vote = ExecuteMsg::Vote {
1204 proposal_id,
1205 vote: Vote::No,
1206 };
1207 let _ = app
1208 .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &no_vote, &[])
1209 .unwrap();
1210
1211 let res = app
1213 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &no_vote, &[])
1214 .unwrap();
1215
1216 assert_eq!(
1217 res.custom_attrs(1),
1218 [
1219 ("action", "vote"),
1220 ("sender", VOTER4),
1221 ("proposal_id", proposal_id.to_string().as_str()),
1222 ("status", "Rejected"),
1223 ],
1224 );
1225
1226 let yes_vote = ExecuteMsg::Vote {
1228 proposal_id,
1229 vote: Vote::Yes,
1230 };
1231 let res = app
1232 .execute_contract(Addr::unchecked(VOTER5), flex_addr, &yes_vote, &[])
1233 .unwrap();
1234
1235 assert_eq!(
1236 res.custom_attrs(1),
1237 [
1238 ("action", "vote"),
1239 ("sender", VOTER5),
1240 ("proposal_id", proposal_id.to_string().as_str()),
1241 ("status", "Rejected"),
1242 ],
1243 );
1244 }
1245
1246 #[test]
1247 fn test_execute_works() {
1248 let init_funds = coins(10, "BTC");
1249 let mut app = mock_app(&init_funds);
1250
1251 let threshold = Threshold::ThresholdQuorum {
1252 threshold: Decimal::percent(51),
1253 quorum: Decimal::percent(1),
1254 };
1255 let voting_period = Duration::Time(2000000);
1256 let (flex_addr, _) = setup_test_case(
1257 &mut app,
1258 threshold,
1259 voting_period,
1260 init_funds,
1261 true,
1262 None,
1263 None,
1264 );
1265
1266 let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1268 assert_eq!(contract_bal, coin(10, "BTC"));
1269
1270 let proposal = pay_somebody_proposal();
1272 let res = app
1273 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1274 .unwrap();
1275
1276 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1278
1279 let execution = ExecuteMsg::Execute { proposal_id };
1281 let err = app
1282 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &execution, &[])
1283 .unwrap_err();
1284 assert_eq!(
1285 ContractError::WrongExecuteStatus {},
1286 err.downcast().unwrap()
1287 );
1288
1289 let vote = ExecuteMsg::Vote {
1291 proposal_id,
1292 vote: Vote::Yes,
1293 };
1294 let res = app
1295 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1296 .unwrap();
1297 assert_eq!(
1298 res.custom_attrs(1),
1299 [
1300 ("action", "vote"),
1301 ("sender", VOTER4),
1302 ("proposal_id", proposal_id.to_string().as_str()),
1303 ("status", "Passed"),
1304 ],
1305 );
1306
1307 let closing = ExecuteMsg::Close { proposal_id };
1309 let err = app
1310 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &closing, &[])
1311 .unwrap_err();
1312 assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1313
1314 let res = app
1316 .execute_contract(
1317 Addr::unchecked(SOMEBODY),
1318 flex_addr.clone(),
1319 &execution,
1320 &[],
1321 )
1322 .unwrap();
1323 assert_eq!(
1324 res.custom_attrs(1),
1325 [
1326 ("action", "execute"),
1327 ("sender", SOMEBODY),
1328 ("proposal_id", proposal_id.to_string().as_str()),
1329 ],
1330 );
1331
1332 let some_bal = app.wrap().query_balance(SOMEBODY, "BTC").unwrap();
1334 assert_eq!(some_bal, coin(1, "BTC"));
1335 let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1336 assert_eq!(contract_bal, coin(9, "BTC"));
1337
1338 let err = app
1340 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &closing, &[])
1341 .unwrap_err();
1342 assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1343
1344 let err = app
1346 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr, &execution, &[])
1347 .unwrap_err();
1348 assert_eq!(
1349 ContractError::WrongExecuteStatus {},
1350 err.downcast().unwrap()
1351 );
1352 }
1353
1354 #[test]
1355 fn execute_with_executor_member() {
1356 let init_funds = coins(10, "BTC");
1357 let mut app = mock_app(&init_funds);
1358
1359 let threshold = Threshold::ThresholdQuorum {
1360 threshold: Decimal::percent(51),
1361 quorum: Decimal::percent(1),
1362 };
1363 let voting_period = Duration::Time(2000000);
1364 let (flex_addr, _) = setup_test_case(
1365 &mut app,
1366 threshold,
1367 voting_period,
1368 init_funds,
1369 true,
1370 Some(crate::state::Executor::Member), None,
1372 );
1373
1374 let proposal = pay_somebody_proposal();
1376 let res = app
1377 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1378 .unwrap();
1379
1380 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1382
1383 let vote = ExecuteMsg::Vote {
1385 proposal_id,
1386 vote: Vote::Yes,
1387 };
1388 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1389 .unwrap();
1390
1391 let execution = ExecuteMsg::Execute { proposal_id };
1392 let err = app
1393 .execute_contract(
1394 Addr::unchecked(Addr::unchecked("anyone")), flex_addr.clone(),
1396 &execution,
1397 &[],
1398 )
1399 .unwrap_err();
1400 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1401
1402 app.execute_contract(
1403 Addr::unchecked(Addr::unchecked(VOTER2)), flex_addr,
1405 &execution,
1406 &[],
1407 )
1408 .unwrap();
1409 }
1410
1411 #[test]
1412 fn execute_with_executor_only() {
1413 let init_funds = coins(10, "BTC");
1414 let mut app = mock_app(&init_funds);
1415
1416 let threshold = Threshold::ThresholdQuorum {
1417 threshold: Decimal::percent(51),
1418 quorum: Decimal::percent(1),
1419 };
1420 let voting_period = Duration::Time(2000000);
1421 let (flex_addr, _) = setup_test_case(
1422 &mut app,
1423 threshold,
1424 voting_period,
1425 init_funds,
1426 true,
1427 Some(crate::state::Executor::Only(Addr::unchecked(VOTER3))), None,
1429 );
1430
1431 let proposal = pay_somebody_proposal();
1433 let res = app
1434 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1435 .unwrap();
1436
1437 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1439
1440 let vote = ExecuteMsg::Vote {
1442 proposal_id,
1443 vote: Vote::Yes,
1444 };
1445 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1446 .unwrap();
1447
1448 let execution = ExecuteMsg::Execute { proposal_id };
1449 let err = app
1450 .execute_contract(
1451 Addr::unchecked(Addr::unchecked("anyone")), flex_addr.clone(),
1453 &execution,
1454 &[],
1455 )
1456 .unwrap_err();
1457 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1458
1459 let err = app
1460 .execute_contract(
1461 Addr::unchecked(Addr::unchecked(VOTER1)), flex_addr.clone(),
1463 &execution,
1464 &[],
1465 )
1466 .unwrap_err();
1467 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1468
1469 app.execute_contract(
1470 Addr::unchecked(Addr::unchecked(VOTER3)), flex_addr,
1472 &execution,
1473 &[],
1474 )
1475 .unwrap();
1476 }
1477
1478 #[test]
1479 fn proposal_pass_on_expiration() {
1480 let init_funds = coins(10, "BTC");
1481 let mut app = mock_app(&init_funds);
1482
1483 let threshold = Threshold::ThresholdQuorum {
1484 threshold: Decimal::percent(51),
1485 quorum: Decimal::percent(1),
1486 };
1487 let voting_period = 2000000;
1488 let (flex_addr, _) = setup_test_case(
1489 &mut app,
1490 threshold,
1491 Duration::Time(voting_period),
1492 init_funds,
1493 true,
1494 None,
1495 None,
1496 );
1497
1498 let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1500 assert_eq!(contract_bal, coin(10, "BTC"));
1501
1502 let proposal = pay_somebody_proposal();
1504 let res = app
1505 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1506 .unwrap();
1507
1508 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1510
1511 let vote = ExecuteMsg::Vote {
1513 proposal_id,
1514 vote: Vote::Yes,
1515 };
1516 let res = app
1517 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &vote, &[])
1518 .unwrap();
1519 assert_eq!(
1520 res.custom_attrs(1),
1521 [
1522 ("action", "vote"),
1523 ("sender", VOTER3),
1524 ("proposal_id", proposal_id.to_string().as_str()),
1525 ("status", "Open"),
1526 ],
1527 );
1528
1529 app.update_block(|block| {
1531 block.time = block.time.plus_seconds(voting_period);
1532 block.height += std::cmp::max(1, voting_period / 5);
1533 });
1534
1535 let prop: ProposalResponse = app
1537 .wrap()
1538 .query_wasm_smart(&flex_addr, &QueryMsg::Proposal { proposal_id })
1539 .unwrap();
1540 assert_eq!(prop.status, Status::Passed);
1541
1542 let err = app
1544 .execute_contract(
1545 Addr::unchecked(SOMEBODY),
1546 flex_addr.clone(),
1547 &ExecuteMsg::Close { proposal_id },
1548 &[],
1549 )
1550 .unwrap_err();
1551 assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1552
1553 let res = app
1555 .execute_contract(
1556 Addr::unchecked(SOMEBODY),
1557 flex_addr,
1558 &ExecuteMsg::Execute { proposal_id },
1559 &[],
1560 )
1561 .unwrap();
1562 assert_eq!(
1563 res.custom_attrs(1),
1564 [
1565 ("action", "execute"),
1566 ("sender", SOMEBODY),
1567 ("proposal_id", proposal_id.to_string().as_str()),
1568 ],
1569 );
1570 }
1571
1572 #[test]
1573 fn test_close_works() {
1574 let init_funds = coins(10, "BTC");
1575 let mut app = mock_app(&init_funds);
1576
1577 let threshold = Threshold::ThresholdQuorum {
1578 threshold: Decimal::percent(51),
1579 quorum: Decimal::percent(1),
1580 };
1581 let voting_period = Duration::Height(2000000);
1582 let (flex_addr, _) = setup_test_case(
1583 &mut app,
1584 threshold,
1585 voting_period,
1586 init_funds,
1587 true,
1588 None,
1589 None,
1590 );
1591
1592 let proposal = pay_somebody_proposal();
1594 let res = app
1595 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1596 .unwrap();
1597
1598 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1600
1601 let closing = ExecuteMsg::Close { proposal_id };
1603 let err = app
1604 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &closing, &[])
1605 .unwrap_err();
1606 assert_eq!(ContractError::NotExpired {}, err.downcast().unwrap());
1607
1608 app.update_block(expire(voting_period));
1610 let res = app
1611 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &closing, &[])
1612 .unwrap();
1613 assert_eq!(
1614 res.custom_attrs(1),
1615 [
1616 ("action", "close"),
1617 ("sender", SOMEBODY),
1618 ("proposal_id", proposal_id.to_string().as_str()),
1619 ],
1620 );
1621
1622 let closing = ExecuteMsg::Close { proposal_id };
1624 let err = app
1625 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr, &closing, &[])
1626 .unwrap_err();
1627 assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1628 }
1629
1630 #[test]
1632 fn execute_group_changes_from_external() {
1633 let init_funds = coins(10, "BTC");
1634 let mut app = mock_app(&init_funds);
1635
1636 let threshold = Threshold::ThresholdQuorum {
1637 threshold: Decimal::percent(51),
1638 quorum: Decimal::percent(1),
1639 };
1640 let voting_period = Duration::Time(20000);
1641 let (flex_addr, group_addr) = setup_test_case(
1642 &mut app,
1643 threshold,
1644 voting_period,
1645 init_funds,
1646 false,
1647 None,
1648 None,
1649 );
1650
1651 let proposal = pay_somebody_proposal();
1653 let res = app
1654 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[])
1655 .unwrap();
1656 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1658 let prop_status = |app: &App, proposal_id: u64| -> Status {
1659 let query_prop = QueryMsg::Proposal { proposal_id };
1660 let prop: ProposalResponse = app
1661 .wrap()
1662 .query_wasm_smart(&flex_addr, &query_prop)
1663 .unwrap();
1664 prop.status
1665 };
1666
1667 assert_eq!(prop_status(&app, proposal_id), Status::Open);
1669
1670 let threshold: ThresholdResponse = app
1672 .wrap()
1673 .query_wasm_smart(&flex_addr, &QueryMsg::Threshold {})
1674 .unwrap();
1675 let expected_thresh = ThresholdResponse::ThresholdQuorum {
1676 total_weight: 23,
1677 threshold: Decimal::percent(51),
1678 quorum: Decimal::percent(1),
1679 };
1680 assert_eq!(expected_thresh, threshold);
1681
1682 app.update_block(|block| block.height += 2);
1684
1685 let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
1690 remove: vec![VOTER3.into()],
1691 add: vec![member(VOTER2, 21), member(NEWBIE, 2)],
1692 };
1693 app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[])
1694 .unwrap();
1695
1696 let query_voter = QueryMsg::Voter {
1698 address: VOTER3.into(),
1699 };
1700 let power: VoterResponse = app
1701 .wrap()
1702 .query_wasm_smart(&flex_addr, &query_voter)
1703 .unwrap();
1704 assert_eq!(power.weight, None);
1705
1706 assert_eq!(prop_status(&app, proposal_id), Status::Open);
1708
1709 app.update_block(|block| block.height += 3);
1711
1712 let proposal2 = pay_somebody_proposal();
1714 let res = app
1715 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal2, &[])
1716 .unwrap();
1717 let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1719
1720 let yes_vote = ExecuteMsg::Vote {
1722 proposal_id: proposal_id2,
1723 vote: Vote::Yes,
1724 };
1725 app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1726 .unwrap();
1727 assert_eq!(prop_status(&app, proposal_id2), Status::Passed);
1728
1729 let yes_vote = ExecuteMsg::Vote {
1731 proposal_id,
1732 vote: Vote::Yes,
1733 };
1734 app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1735 .unwrap();
1736 assert_eq!(prop_status(&app, proposal_id), Status::Open);
1737
1738 let err = app
1740 .execute_contract(Addr::unchecked(NEWBIE), flex_addr.clone(), &yes_vote, &[])
1741 .unwrap_err();
1742 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1743
1744 app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1746 .unwrap();
1747
1748 let threshold: ThresholdResponse = app
1750 .wrap()
1751 .query_wasm_smart(&flex_addr, &QueryMsg::Threshold {})
1752 .unwrap();
1753 let expected_thresh = ThresholdResponse::ThresholdQuorum {
1754 total_weight: 41,
1755 threshold: Decimal::percent(51),
1756 quorum: Decimal::percent(1),
1757 };
1758 assert_eq!(expected_thresh, threshold);
1759
1760 }
1762
1763 #[test]
1767 fn execute_group_changes_from_proposal() {
1768 let init_funds = coins(10, "BTC");
1769 let mut app = mock_app(&init_funds);
1770
1771 let required_weight = 4;
1772 let voting_period = Duration::Time(20000);
1773 let (flex_addr, group_addr) =
1774 setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, true);
1775
1776 let update_msg = Cw4GroupContract::new(group_addr)
1778 .update_members(vec![VOTER3.into()], vec![])
1779 .unwrap();
1780 let update_proposal = ExecuteMsg::Propose {
1781 title: "Kick out VOTER3".to_string(),
1782 description: "He's trying to steal our money".to_string(),
1783 msgs: vec![update_msg],
1784 latest: None,
1785 };
1786 let res = app
1787 .execute_contract(
1788 Addr::unchecked(VOTER1),
1789 flex_addr.clone(),
1790 &update_proposal,
1791 &[],
1792 )
1793 .unwrap();
1794 let update_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1796
1797 app.update_block(|b| b.height += 1);
1799
1800 let cash_proposal = pay_somebody_proposal();
1802 let res = app
1803 .execute_contract(
1804 Addr::unchecked(VOTER1),
1805 flex_addr.clone(),
1806 &cash_proposal,
1807 &[],
1808 )
1809 .unwrap();
1810 let cash_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1812 assert_ne!(cash_proposal_id, update_proposal_id);
1813
1814 let prop_status = |app: &App, proposal_id: u64| -> Status {
1816 let query_prop = QueryMsg::Proposal { proposal_id };
1817 let prop: ProposalResponse = app
1818 .wrap()
1819 .query_wasm_smart(&flex_addr, &query_prop)
1820 .unwrap();
1821 prop.status
1822 };
1823 assert_eq!(prop_status(&app, cash_proposal_id), Status::Open);
1824 assert_eq!(prop_status(&app, update_proposal_id), Status::Open);
1825
1826 app.update_block(|b| b.height += 1);
1828
1829 let yes_vote = ExecuteMsg::Vote {
1831 proposal_id: update_proposal_id,
1832 vote: Vote::Yes,
1833 };
1834 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1835 .unwrap();
1836 let execution = ExecuteMsg::Execute {
1837 proposal_id: update_proposal_id,
1838 };
1839 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &execution, &[])
1840 .unwrap();
1841
1842 assert_eq!(prop_status(&app, update_proposal_id), Status::Executed);
1844 assert_eq!(prop_status(&app, cash_proposal_id), Status::Open);
1845
1846 app.update_block(|b| b.height += 1);
1848
1849 let yes_vote = ExecuteMsg::Vote {
1852 proposal_id: cash_proposal_id,
1853 vote: Vote::Yes,
1854 };
1855 app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1856 .unwrap();
1857 assert_eq!(prop_status(&app, cash_proposal_id), Status::Passed);
1858
1859 let cash_proposal = pay_somebody_proposal();
1861 let err = app
1862 .execute_contract(
1863 Addr::unchecked(VOTER3),
1864 flex_addr.clone(),
1865 &cash_proposal,
1866 &[],
1867 )
1868 .unwrap_err();
1869 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1870
1871 let hook_hack = ExecuteMsg::MemberChangedHook(MemberChangedHookMsg {
1873 diffs: vec![MemberDiff::new(VOTER1, Some(1), None)],
1874 });
1875 let err = app
1876 .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &hook_hack, &[])
1877 .unwrap_err();
1878 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1879 }
1880
1881 #[test]
1883 fn percentage_handles_group_changes() {
1884 let init_funds = coins(10, "BTC");
1885 let mut app = mock_app(&init_funds);
1886
1887 let threshold = Threshold::ThresholdQuorum {
1889 threshold: Decimal::percent(51),
1890 quorum: Decimal::percent(1),
1891 };
1892 let voting_period = Duration::Time(20000);
1893 let (flex_addr, group_addr) = setup_test_case(
1894 &mut app,
1895 threshold,
1896 voting_period,
1897 init_funds,
1898 false,
1899 None,
1900 None,
1901 );
1902
1903 let proposal = pay_somebody_proposal();
1905 let res = app
1906 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
1907 .unwrap();
1908 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1910 let prop_status = |app: &App| -> Status {
1911 let query_prop = QueryMsg::Proposal { proposal_id };
1912 let prop: ProposalResponse = app
1913 .wrap()
1914 .query_wasm_smart(&flex_addr, &query_prop)
1915 .unwrap();
1916 prop.status
1917 };
1918
1919 assert_eq!(prop_status(&app), Status::Open);
1921
1922 app.update_block(|block| block.height += 2);
1924
1925 let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
1927 remove: vec![VOTER3.into()],
1928 add: vec![member(VOTER2, 9), member(NEWBIE, 29)],
1929 };
1930 app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[])
1931 .unwrap();
1932
1933 app.update_block(|block| block.height += 3);
1935
1936 let yes_vote = ExecuteMsg::Vote {
1939 proposal_id,
1940 vote: Vote::Yes,
1941 };
1942 app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1943 .unwrap();
1944 assert_eq!(prop_status(&app), Status::Open);
1945
1946 let proposal = pay_somebody_proposal();
1948 let res = app
1949 .execute_contract(Addr::unchecked(NEWBIE), flex_addr.clone(), &proposal, &[])
1950 .unwrap();
1951 let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1953
1954 let query_prop = QueryMsg::Proposal {
1956 proposal_id: proposal_id2,
1957 };
1958 let prop: ProposalResponse = app
1959 .wrap()
1960 .query_wasm_smart(&flex_addr, &query_prop)
1961 .unwrap();
1962 assert_eq!(Status::Passed, prop.status);
1963 }
1964
1965 #[test]
1967 fn quorum_handles_group_changes() {
1968 let init_funds = coins(10, "BTC");
1969 let mut app = mock_app(&init_funds);
1970
1971 let voting_period = Duration::Time(20000);
1974 let (flex_addr, group_addr) = setup_test_case(
1975 &mut app,
1976 Threshold::ThresholdQuorum {
1977 threshold: Decimal::percent(51),
1978 quorum: Decimal::percent(33),
1979 },
1980 voting_period,
1981 init_funds,
1982 false,
1983 None,
1984 None,
1985 );
1986
1987 let proposal = pay_somebody_proposal();
1989 let res = app
1990 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
1991 .unwrap();
1992 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1994 let prop_status = |app: &App| -> Status {
1995 let query_prop = QueryMsg::Proposal { proposal_id };
1996 let prop: ProposalResponse = app
1997 .wrap()
1998 .query_wasm_smart(&flex_addr, &query_prop)
1999 .unwrap();
2000 prop.status
2001 };
2002
2003 assert_eq!(prop_status(&app), Status::Open);
2005
2006 app.update_block(|block| block.height += 2);
2008
2009 let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
2011 remove: vec![VOTER3.into()],
2012 add: vec![member(VOTER2, 9), member(NEWBIE, 29)],
2013 };
2014 app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[])
2015 .unwrap();
2016
2017 app.update_block(|block| block.height += 3);
2019
2020 let yes_vote = ExecuteMsg::Vote {
2023 proposal_id,
2024 vote: Vote::Yes,
2025 };
2026 app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
2027 .unwrap();
2028 assert_eq!(prop_status(&app), Status::Open);
2030
2031 app.update_block(expire(voting_period));
2033 assert_eq!(prop_status(&app), Status::Rejected);
2034 }
2035
2036 #[test]
2037 fn quorum_enforced_even_if_absolute_threshold_met() {
2038 let init_funds = coins(10, "BTC");
2039 let mut app = mock_app(&init_funds);
2040
2041 let voting_period = Duration::Time(20000);
2044 let (flex_addr, _) = setup_test_case(
2045 &mut app,
2046 Threshold::ThresholdQuorum {
2048 threshold: Decimal::percent(60),
2049 quorum: Decimal::percent(80),
2050 },
2051 voting_period,
2052 init_funds,
2053 false,
2054 None,
2055 None,
2056 );
2057
2058 let proposal = pay_somebody_proposal();
2060 let res = app
2061 .execute_contract(Addr::unchecked(VOTER5), flex_addr.clone(), &proposal, &[])
2062 .unwrap();
2063 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
2065 let prop_status = |app: &App| -> Status {
2066 let query_prop = QueryMsg::Proposal { proposal_id };
2067 let prop: ProposalResponse = app
2068 .wrap()
2069 .query_wasm_smart(&flex_addr, &query_prop)
2070 .unwrap();
2071 prop.status
2072 };
2073 assert_eq!(prop_status(&app), Status::Open);
2074 app.update_block(|block| block.height += 3);
2075
2076 let yes_vote = ExecuteMsg::Vote {
2078 proposal_id,
2079 vote: Vote::Yes,
2080 };
2081 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
2082 .unwrap();
2083 assert_eq!(prop_status(&app), Status::Open);
2085
2086 let no_vote = ExecuteMsg::Vote {
2088 proposal_id,
2089 vote: Vote::No,
2090 };
2091 app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &no_vote, &[])
2092 .unwrap();
2093 assert_eq!(prop_status(&app), Status::Passed);
2094 }
2095
2096 #[test]
2097 fn test_instantiate_with_invalid_deposit() {
2098 let mut app = App::default();
2099
2100 let flex_id = app.store_code(contract_flex());
2101
2102 let group_addr = instantiate_group(
2103 &mut app,
2104 vec![Member {
2105 addr: OWNER.to_string(),
2106 weight: 10,
2107 }],
2108 );
2109
2110 let instantiate = InstantiateMsg {
2112 group_addr: group_addr.to_string(),
2113 threshold: Threshold::AbsoluteCount { weight: 10 },
2114 max_voting_period: Duration::Time(10),
2115 executor: None,
2116 proposal_deposit: Some(UncheckedDepositInfo {
2117 amount: Uint128::new(1),
2118 refund_failed_proposals: true,
2119 denom: UncheckedDenom::Cw20(group_addr.to_string()),
2120 }),
2121 };
2122
2123 let err: ContractError = app
2124 .instantiate_contract(
2125 flex_id,
2126 Addr::unchecked(OWNER),
2127 &instantiate,
2128 &[],
2129 "Bad cw20",
2130 None,
2131 )
2132 .unwrap_err()
2133 .downcast()
2134 .unwrap();
2135
2136 assert_eq!(err, ContractError::Deposit(DepositError::InvalidCw20 {}));
2137
2138 let instantiate = InstantiateMsg {
2140 group_addr: group_addr.to_string(),
2141 threshold: Threshold::AbsoluteCount { weight: 10 },
2142 max_voting_period: Duration::Time(10),
2143 executor: None,
2144 proposal_deposit: Some(UncheckedDepositInfo {
2145 amount: Uint128::zero(),
2146 refund_failed_proposals: true,
2147 denom: UncheckedDenom::Native("native".to_string()),
2148 }),
2149 };
2150
2151 let err: ContractError = app
2152 .instantiate_contract(
2153 flex_id,
2154 Addr::unchecked(OWNER),
2155 &instantiate,
2156 &[],
2157 "Bad cw20",
2158 None,
2159 )
2160 .unwrap_err()
2161 .downcast()
2162 .unwrap();
2163
2164 assert_eq!(err, ContractError::Deposit(DepositError::ZeroDeposit {}))
2165 }
2166
2167 #[test]
2168 fn test_cw20_proposal_deposit() {
2169 let mut app = App::default();
2170
2171 let cw20_id = app.store_code(contract_cw20());
2172
2173 let cw20_addr = app
2174 .instantiate_contract(
2175 cw20_id,
2176 Addr::unchecked(OWNER),
2177 &cw20_base::msg::InstantiateMsg {
2178 name: "Token".to_string(),
2179 symbol: "TOKEN".to_string(),
2180 decimals: 6,
2181 initial_balances: vec![
2182 Cw20Coin {
2183 address: VOTER4.to_string(),
2184 amount: Uint128::new(10),
2185 },
2186 Cw20Coin {
2187 address: OWNER.to_string(),
2188 amount: Uint128::new(10),
2189 },
2190 ],
2191 mint: None,
2192 marketing: None,
2193 },
2194 &[],
2195 "Token",
2196 None,
2197 )
2198 .unwrap();
2199
2200 let (flex_addr, _) = setup_test_case(
2201 &mut app,
2202 Threshold::AbsoluteCount { weight: 10 },
2203 Duration::Height(10),
2204 vec![],
2205 true,
2206 None,
2207 Some(UncheckedDepositInfo {
2208 amount: Uint128::new(10),
2209 denom: UncheckedDenom::Cw20(cw20_addr.to_string()),
2210 refund_failed_proposals: true,
2211 }),
2212 );
2213
2214 app.execute_contract(
2215 Addr::unchecked(VOTER4),
2216 cw20_addr.clone(),
2217 &cw20::Cw20ExecuteMsg::IncreaseAllowance {
2218 spender: flex_addr.to_string(),
2219 amount: Uint128::new(10),
2220 expires: None,
2221 },
2222 &[],
2223 )
2224 .unwrap();
2225
2226 let proposal = text_proposal();
2228 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &proposal, &[])
2229 .unwrap();
2230
2231 let balance: cw20::BalanceResponse = app
2233 .wrap()
2234 .query_wasm_smart(
2235 cw20_addr.clone(),
2236 &cw20::Cw20QueryMsg::Balance {
2237 address: VOTER4.to_string(),
2238 },
2239 )
2240 .unwrap();
2241 assert_eq!(balance.balance, Uint128::zero());
2242
2243 let balance: cw20::BalanceResponse = app
2244 .wrap()
2245 .query_wasm_smart(
2246 cw20_addr.clone(),
2247 &cw20::Cw20QueryMsg::Balance {
2248 address: flex_addr.to_string(),
2249 },
2250 )
2251 .unwrap();
2252 assert_eq!(balance.balance, Uint128::new(10));
2253
2254 app.execute_contract(
2255 Addr::unchecked(VOTER4),
2256 flex_addr.clone(),
2257 &ExecuteMsg::Execute { proposal_id: 1 },
2258 &[],
2259 )
2260 .unwrap();
2261
2262 let balance: cw20::BalanceResponse = app
2264 .wrap()
2265 .query_wasm_smart(
2266 cw20_addr.clone(),
2267 &cw20::Cw20QueryMsg::Balance {
2268 address: VOTER4.to_string(),
2269 },
2270 )
2271 .unwrap();
2272 assert_eq!(balance.balance, Uint128::new(10));
2273
2274 let balance: cw20::BalanceResponse = app
2275 .wrap()
2276 .query_wasm_smart(
2277 cw20_addr.clone(),
2278 &cw20::Cw20QueryMsg::Balance {
2279 address: flex_addr.to_string(),
2280 },
2281 )
2282 .unwrap();
2283 assert_eq!(balance.balance, Uint128::zero());
2284
2285 app.execute_contract(
2286 Addr::unchecked(OWNER),
2287 cw20_addr.clone(),
2288 &cw20::Cw20ExecuteMsg::IncreaseAllowance {
2289 spender: flex_addr.to_string(),
2290 amount: Uint128::new(10),
2291 expires: None,
2292 },
2293 &[],
2294 )
2295 .unwrap();
2296
2297 let proposal = text_proposal();
2299 app.execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
2300 .unwrap();
2301
2302 let balance: cw20::BalanceResponse = app
2304 .wrap()
2305 .query_wasm_smart(
2306 cw20_addr.clone(),
2307 &cw20::Cw20QueryMsg::Balance {
2308 address: flex_addr.to_string(),
2309 },
2310 )
2311 .unwrap();
2312 assert_eq!(balance.balance, Uint128::new(10));
2313
2314 app.execute_contract(
2316 Addr::unchecked(VOTER4),
2317 flex_addr.clone(),
2318 &ExecuteMsg::Vote {
2319 proposal_id: 2,
2320 vote: Vote::No,
2321 },
2322 &[],
2323 )
2324 .unwrap();
2325
2326 app.update_block(|b| b.height += 10);
2328
2329 app.execute_contract(
2330 Addr::unchecked(VOTER4),
2331 flex_addr,
2332 &ExecuteMsg::Close { proposal_id: 2 },
2333 &[],
2334 )
2335 .unwrap();
2336
2337 let balance: cw20::BalanceResponse = app
2339 .wrap()
2340 .query_wasm_smart(
2341 cw20_addr,
2342 &cw20::Cw20QueryMsg::Balance {
2343 address: VOTER4.to_string(),
2344 },
2345 )
2346 .unwrap();
2347 assert_eq!(balance.balance, Uint128::new(10));
2348 }
2349
2350 #[test]
2351 fn proposal_deposit_no_failed_refunds() {
2352 let mut app = App::default();
2353
2354 let (flex_addr, _) = setup_test_case(
2355 &mut app,
2356 Threshold::AbsoluteCount { weight: 10 },
2357 Duration::Height(10),
2358 vec![],
2359 true,
2360 None,
2361 Some(UncheckedDepositInfo {
2362 amount: Uint128::new(10),
2363 denom: UncheckedDenom::Native("TOKEN".to_string()),
2364 refund_failed_proposals: false,
2365 }),
2366 );
2367
2368 app.sudo(SudoMsg::Bank(BankSudo::Mint {
2369 to_address: OWNER.to_string(),
2370 amount: vec![Coin {
2371 amount: Uint128::new(10),
2372 denom: "TOKEN".to_string(),
2373 }],
2374 }))
2375 .unwrap();
2376
2377 let proposal = text_proposal();
2379 app.execute_contract(
2380 Addr::unchecked(OWNER),
2381 flex_addr.clone(),
2382 &proposal,
2383 &[Coin {
2384 amount: Uint128::new(10),
2385 denom: "TOKEN".to_string(),
2386 }],
2387 )
2388 .unwrap();
2389
2390 let balance = app
2392 .wrap()
2393 .query_balance(OWNER, "TOKEN".to_string())
2394 .unwrap();
2395 assert_eq!(balance.amount, Uint128::zero());
2396
2397 app.execute_contract(
2399 Addr::unchecked(VOTER4),
2400 flex_addr.clone(),
2401 &ExecuteMsg::Vote {
2402 proposal_id: 1,
2403 vote: Vote::No,
2404 },
2405 &[],
2406 )
2407 .unwrap();
2408
2409 app.update_block(|b| b.height += 10);
2411
2412 app.execute_contract(
2413 Addr::unchecked(VOTER4),
2414 flex_addr,
2415 &ExecuteMsg::Close { proposal_id: 1 },
2416 &[],
2417 )
2418 .unwrap();
2419
2420 let balance = app
2422 .wrap()
2423 .query_balance(OWNER, "TOKEN".to_string())
2424 .unwrap();
2425 assert_eq!(balance.amount, Uint128::zero());
2426 }
2427
2428 #[test]
2429 fn test_native_proposal_deposit() {
2430 let mut app = App::default();
2431
2432 app.sudo(SudoMsg::Bank(BankSudo::Mint {
2433 to_address: VOTER4.to_string(),
2434 amount: vec![Coin {
2435 amount: Uint128::new(10),
2436 denom: "TOKEN".to_string(),
2437 }],
2438 }))
2439 .unwrap();
2440
2441 app.sudo(SudoMsg::Bank(BankSudo::Mint {
2442 to_address: OWNER.to_string(),
2443 amount: vec![Coin {
2444 amount: Uint128::new(10),
2445 denom: "TOKEN".to_string(),
2446 }],
2447 }))
2448 .unwrap();
2449
2450 let (flex_addr, _) = setup_test_case(
2451 &mut app,
2452 Threshold::AbsoluteCount { weight: 10 },
2453 Duration::Height(10),
2454 vec![],
2455 true,
2456 None,
2457 Some(UncheckedDepositInfo {
2458 amount: Uint128::new(10),
2459 denom: UncheckedDenom::Native("TOKEN".to_string()),
2460 refund_failed_proposals: true,
2461 }),
2462 );
2463
2464 let proposal = text_proposal();
2466 app.execute_contract(
2467 Addr::unchecked(VOTER4),
2468 flex_addr.clone(),
2469 &proposal,
2470 &[Coin {
2471 amount: Uint128::new(10),
2472 denom: "TOKEN".to_string(),
2473 }],
2474 )
2475 .unwrap();
2476
2477 let balance = app
2479 .wrap()
2480 .query_balance(flex_addr.clone(), "TOKEN")
2481 .unwrap();
2482 assert_eq!(balance.amount, Uint128::new(10));
2483
2484 app.execute_contract(
2485 Addr::unchecked(VOTER4),
2486 flex_addr.clone(),
2487 &ExecuteMsg::Execute { proposal_id: 1 },
2488 &[],
2489 )
2490 .unwrap();
2491
2492 let balance = app.wrap().query_balance(VOTER4, "TOKEN").unwrap();
2494 assert_eq!(balance.amount, Uint128::new(10));
2495
2496 let proposal = text_proposal();
2498 app.execute_contract(
2499 Addr::unchecked(OWNER),
2500 flex_addr.clone(),
2501 &proposal,
2502 &[Coin {
2503 amount: Uint128::new(10),
2504 denom: "TOKEN".to_string(),
2505 }],
2506 )
2507 .unwrap();
2508
2509 let balance = app
2510 .wrap()
2511 .query_balance(flex_addr.clone(), "TOKEN")
2512 .unwrap();
2513 assert_eq!(balance.amount, Uint128::new(10));
2514
2515 app.execute_contract(
2517 Addr::unchecked(VOTER4),
2518 flex_addr.clone(),
2519 &ExecuteMsg::Vote {
2520 proposal_id: 2,
2521 vote: Vote::No,
2522 },
2523 &[],
2524 )
2525 .unwrap();
2526
2527 app.update_block(|b| b.height += 10);
2529
2530 app.execute_contract(
2531 Addr::unchecked(VOTER4),
2532 flex_addr,
2533 &ExecuteMsg::Close { proposal_id: 2 },
2534 &[],
2535 )
2536 .unwrap();
2537
2538 let balance = app.wrap().query_balance(OWNER, "TOKEN").unwrap();
2540 assert_eq!(balance.amount, Uint128::new(10));
2541 }
2542}