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 abstract_cw2::set_contract_version;
11
12use abstract_cw3::{
13 Ballot, Proposal, ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo,
14 VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse, Votes,
15};
16use abstract_cw3_fixed_multisig::state::{next_id, BALLOTS, PROPOSALS};
17use abstract_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 abstract_cw2::{query_contract_info, ContractVersion};
493 use abstract_cw20::{Cw20Coin, UncheckedDenom};
494 use abstract_cw3::{DepositError, UncheckedDepositInfo};
495 use abstract_cw4::{Cw4ExecuteMsg, Member};
496 use abstract_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 super::*;
503
504 const OWNER: &str = "admin0001";
505 const VOTER1: &str = "voter0001";
506 const VOTER2: &str = "voter0002";
507 const VOTER3: &str = "voter0003";
508 const VOTER4: &str = "voter0004";
509 const VOTER5: &str = "voter0005";
510 const SOMEBODY: &str = "somebody";
511
512 fn member<T: Into<String>>(addr: T, weight: u64) -> Member {
513 Member {
514 addr: addr.into(),
515 weight,
516 }
517 }
518
519 pub fn contract_flex() -> Box<dyn Contract<Empty>> {
520 let contract = ContractWrapper::new(
521 crate::contract::execute,
522 crate::contract::instantiate,
523 crate::contract::query,
524 );
525 Box::new(contract)
526 }
527
528 pub fn contract_group() -> Box<dyn Contract<Empty>> {
529 let contract = ContractWrapper::new(
530 abstract_cw4_group::contract::execute,
531 abstract_cw4_group::contract::instantiate,
532 abstract_cw4_group::contract::query,
533 );
534 Box::new(contract)
535 }
536
537 fn contract_cw20() -> Box<dyn Contract<Empty>> {
538 let contract = ContractWrapper::new(
539 abstract_cw20_base::contract::execute,
540 abstract_cw20_base::contract::instantiate,
541 abstract_cw20_base::contract::query,
542 );
543 Box::new(contract)
544 }
545
546 fn mock_app(init_funds: &[Coin]) -> App {
547 AppBuilder::new().build(|router, _, storage| {
548 router
549 .bank
550 .init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec())
551 .unwrap();
552 })
553 }
554
555 fn instantiate_group(app: &mut App, members: Vec<Member>) -> Addr {
557 let group_id = app.store_code(contract_group());
558 let msg = abstract_cw4_group::msg::InstantiateMsg {
559 admin: Some(OWNER.into()),
560 members,
561 };
562 app.instantiate_contract(group_id, Addr::unchecked(OWNER), &msg, &[], "group", None)
563 .unwrap()
564 }
565
566 #[track_caller]
567 fn instantiate_flex(
568 app: &mut App,
569 group: Addr,
570 threshold: Threshold,
571 max_voting_period: Duration,
572 executor: Option<crate::state::Executor>,
573 proposal_deposit: Option<UncheckedDepositInfo>,
574 ) -> Addr {
575 let flex_id = app.store_code(contract_flex());
576 let msg = crate::msg::InstantiateMsg {
577 group_addr: group.to_string(),
578 threshold,
579 max_voting_period,
580 executor,
581 proposal_deposit,
582 };
583 app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "flex", None)
584 .unwrap()
585 }
586
587 #[track_caller]
591 fn setup_test_case_fixed(
592 app: &mut App,
593 weight_needed: u64,
594 max_voting_period: Duration,
595 init_funds: Vec<Coin>,
596 multisig_as_group_admin: bool,
597 ) -> (Addr, Addr) {
598 setup_test_case(
599 app,
600 Threshold::AbsoluteCount {
601 weight: weight_needed,
602 },
603 max_voting_period,
604 init_funds,
605 multisig_as_group_admin,
606 None,
607 None,
608 )
609 }
610
611 #[track_caller]
612 fn setup_test_case(
613 app: &mut App,
614 threshold: Threshold,
615 max_voting_period: Duration,
616 init_funds: Vec<Coin>,
617 multisig_as_group_admin: bool,
618 executor: Option<crate::state::Executor>,
619 proposal_deposit: Option<UncheckedDepositInfo>,
620 ) -> (Addr, Addr) {
621 let members = vec![
623 member(OWNER, 0),
624 member(VOTER1, 1),
625 member(VOTER2, 2),
626 member(VOTER3, 3),
627 member(VOTER4, 12), member(VOTER5, 5),
629 ];
630 let group_addr = instantiate_group(app, members);
631 app.update_block(next_block);
632
633 let flex_addr = instantiate_flex(
635 app,
636 group_addr.clone(),
637 threshold,
638 max_voting_period,
639 executor,
640 proposal_deposit,
641 );
642 app.update_block(next_block);
643
644 if multisig_as_group_admin {
646 let update_admin = Cw4ExecuteMsg::UpdateAdmin {
647 admin: Some(flex_addr.to_string()),
648 };
649 app.execute_contract(
650 Addr::unchecked(OWNER),
651 group_addr.clone(),
652 &update_admin,
653 &[],
654 )
655 .unwrap();
656 app.update_block(next_block);
657 }
658
659 if !init_funds.is_empty() {
661 app.send_tokens(Addr::unchecked(OWNER), flex_addr.clone(), &init_funds)
662 .unwrap();
663 }
664 (flex_addr, group_addr)
665 }
666
667 fn proposal_info() -> (Vec<CosmosMsg<Empty>>, String, String) {
668 let bank_msg = BankMsg::Send {
669 to_address: SOMEBODY.into(),
670 amount: coins(1, "BTC"),
671 };
672 let msgs = vec![bank_msg.into()];
673 let title = "Pay somebody".to_string();
674 let description = "Do I pay her?".to_string();
675 (msgs, title, description)
676 }
677
678 fn pay_somebody_proposal() -> ExecuteMsg {
679 let (msgs, title, description) = proposal_info();
680 ExecuteMsg::Propose {
681 title,
682 description,
683 msgs,
684 latest: None,
685 }
686 }
687
688 fn text_proposal() -> ExecuteMsg {
689 let (_, title, description) = proposal_info();
690 ExecuteMsg::Propose {
691 title,
692 description,
693 msgs: vec![],
694 latest: None,
695 }
696 }
697
698 #[test]
699 fn test_instantiate_works() {
700 let mut app = mock_app(&[]);
701
702 let group_addr = instantiate_group(&mut app, vec![member(OWNER, 1)]);
704 let flex_id = app.store_code(contract_flex());
705
706 let max_voting_period = Duration::Time(1234567);
707
708 let instantiate_msg = InstantiateMsg {
710 group_addr: group_addr.to_string(),
711 threshold: Threshold::ThresholdQuorum {
712 threshold: Decimal::zero(),
713 quorum: Decimal::percent(1),
714 },
715 max_voting_period,
716 executor: None,
717 proposal_deposit: None,
718 };
719 let err = app
720 .instantiate_contract(
721 flex_id,
722 Addr::unchecked(OWNER),
723 &instantiate_msg,
724 &[],
725 "zero required weight",
726 None,
727 )
728 .unwrap_err();
729 assert_eq!(
730 ContractError::Threshold(cw_utils::ThresholdError::InvalidThreshold {}),
731 err.downcast().unwrap()
732 );
733
734 let instantiate_msg = InstantiateMsg {
736 group_addr: group_addr.to_string(),
737 threshold: Threshold::AbsoluteCount { weight: 100 },
738 max_voting_period,
739 executor: None,
740 proposal_deposit: None,
741 };
742 let err = app
743 .instantiate_contract(
744 flex_id,
745 Addr::unchecked(OWNER),
746 &instantiate_msg,
747 &[],
748 "high required weight",
749 None,
750 )
751 .unwrap_err();
752 assert_eq!(
753 ContractError::Threshold(cw_utils::ThresholdError::UnreachableWeight {}),
754 err.downcast().unwrap()
755 );
756
757 let instantiate_msg = InstantiateMsg {
759 group_addr: group_addr.to_string(),
760 threshold: Threshold::AbsoluteCount { weight: 1 },
761 max_voting_period,
762 executor: None,
763 proposal_deposit: None,
764 };
765 let flex_addr = app
766 .instantiate_contract(
767 flex_id,
768 Addr::unchecked(OWNER),
769 &instantiate_msg,
770 &[],
771 "all good",
772 None,
773 )
774 .unwrap();
775
776 let version = query_contract_info(&app.wrap(), flex_addr.clone()).unwrap();
778 assert_eq!(
779 ContractVersion {
780 contract: CONTRACT_NAME.to_string(),
781 version: CONTRACT_VERSION.to_string(),
782 },
783 version,
784 );
785
786 let voters: VoterListResponse = app
788 .wrap()
789 .query_wasm_smart(
790 &flex_addr,
791 &QueryMsg::ListVoters {
792 start_after: None,
793 limit: None,
794 },
795 )
796 .unwrap();
797 assert_eq!(
798 voters.voters,
799 vec![VoterDetail {
800 addr: OWNER.into(),
801 weight: 1
802 }]
803 );
804 }
805
806 #[test]
807 fn test_propose_works() {
808 let init_funds = coins(10, "BTC");
809 let mut app = mock_app(&init_funds);
810
811 let required_weight = 4;
812 let voting_period = Duration::Time(2000000);
813 let (flex_addr, _) =
814 setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, false);
815
816 let proposal = pay_somebody_proposal();
817 let err = app
819 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &proposal, &[])
820 .unwrap_err();
821 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
822
823 let msgs = match proposal.clone() {
825 ExecuteMsg::Propose { msgs, .. } => msgs,
826 _ => panic!("Wrong variant"),
827 };
828 let proposal_wrong_exp = ExecuteMsg::Propose {
829 title: "Rewarding somebody".to_string(),
830 description: "Do we reward her?".to_string(),
831 msgs,
832 latest: Some(Expiration::AtHeight(123456)),
833 };
834 let err = app
835 .execute_contract(
836 Addr::unchecked(OWNER),
837 flex_addr.clone(),
838 &proposal_wrong_exp,
839 &[],
840 )
841 .unwrap_err();
842 assert_eq!(ContractError::WrongExpiration {}, err.downcast().unwrap());
843
844 let res = app
846 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
847 .unwrap();
848 assert_eq!(
849 res.custom_attrs(1),
850 [
851 ("action", "propose"),
852 ("sender", VOTER3),
853 ("proposal_id", "1"),
854 ("status", "Open"),
855 ],
856 );
857
858 let res = app
860 .execute_contract(Addr::unchecked(VOTER4), flex_addr, &proposal, &[])
861 .unwrap();
862 assert_eq!(
863 res.custom_attrs(1),
864 [
865 ("action", "propose"),
866 ("sender", VOTER4),
867 ("proposal_id", "2"),
868 ("status", "Passed"),
869 ],
870 );
871 }
872
873 fn get_tally(app: &App, flex_addr: &str, proposal_id: u64) -> u64 {
874 let voters = QueryMsg::ListVotes {
876 proposal_id,
877 start_after: None,
878 limit: None,
879 };
880 let votes: VoteListResponse = app.wrap().query_wasm_smart(flex_addr, &voters).unwrap();
881 votes
883 .votes
884 .iter()
885 .filter(|&v| v.vote == Vote::Yes)
886 .map(|v| v.weight)
887 .sum()
888 }
889
890 fn expire(voting_period: Duration) -> impl Fn(&mut BlockInfo) {
891 move |block: &mut BlockInfo| {
892 match voting_period {
893 Duration::Time(duration) => block.time = block.time.plus_seconds(duration + 1),
894 Duration::Height(duration) => block.height += duration + 1,
895 };
896 }
897 }
898
899 fn unexpire(voting_period: Duration) -> impl Fn(&mut BlockInfo) {
900 move |block: &mut BlockInfo| {
901 match voting_period {
902 Duration::Time(duration) => {
903 block.time =
904 Timestamp::from_nanos(block.time.nanos() - (duration * 1_000_000_000));
905 }
906 Duration::Height(duration) => block.height -= duration,
907 };
908 }
909 }
910
911 #[test]
912 fn test_proposal_queries() {
913 let init_funds = coins(10, "BTC");
914 let mut app = mock_app(&init_funds);
915
916 let voting_period = Duration::Time(2000000);
917 let threshold = Threshold::ThresholdQuorum {
918 threshold: Decimal::percent(80),
919 quorum: Decimal::percent(20),
920 };
921 let (flex_addr, _) = setup_test_case(
922 &mut app,
923 threshold,
924 voting_period,
925 init_funds,
926 false,
927 None,
928 None,
929 );
930
931 let proposal = pay_somebody_proposal();
933 let res = app
934 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[])
935 .unwrap();
936 let proposal_id1: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
937
938 app.update_block(next_block);
940 let proposal = pay_somebody_proposal();
941 let res = app
942 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &proposal, &[])
943 .unwrap();
944 let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
945
946 app.update_block(expire(voting_period));
948
949 let proposal = pay_somebody_proposal();
951 let res = app
952 .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &proposal, &[])
953 .unwrap();
954 let proposal_id3: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
955 let proposed_at = app.block_info();
956
957 app.update_block(next_block);
959 let list_query = QueryMsg::ListProposals {
960 start_after: None,
961 limit: None,
962 };
963 let res: ProposalListResponse = app
964 .wrap()
965 .query_wasm_smart(&flex_addr, &list_query)
966 .unwrap();
967 assert_eq!(3, res.proposals.len());
968
969 let info: Vec<_> = res.proposals.iter().map(|p| (p.id, p.status)).collect();
971 let expected_info = vec![
972 (proposal_id1, Status::Rejected),
973 (proposal_id2, Status::Passed),
974 (proposal_id3, Status::Open),
975 ];
976 assert_eq!(expected_info, info);
977
978 let (expected_msgs, expected_title, expected_description) = proposal_info();
980 for prop in res.proposals {
981 assert_eq!(prop.title, expected_title);
982 assert_eq!(prop.description, expected_description);
983 assert_eq!(prop.msgs, expected_msgs);
984 }
985
986 let list_query = QueryMsg::ReverseProposals {
988 start_before: None,
989 limit: Some(1),
990 };
991 let res: ProposalListResponse = app
992 .wrap()
993 .query_wasm_smart(&flex_addr, &list_query)
994 .unwrap();
995 assert_eq!(1, res.proposals.len());
996
997 let (msgs, title, description) = proposal_info();
998 let expected = ProposalResponse {
999 id: proposal_id3,
1000 title,
1001 description,
1002 msgs,
1003 expires: voting_period.after(&proposed_at),
1004 status: Status::Open,
1005 threshold: ThresholdResponse::ThresholdQuorum {
1006 total_weight: 23,
1007 threshold: Decimal::percent(80),
1008 quorum: Decimal::percent(20),
1009 },
1010 proposer: Addr::unchecked(VOTER2),
1011 deposit: None,
1012 };
1013 assert_eq!(&expected, &res.proposals[0]);
1014 }
1015
1016 #[test]
1017 fn test_vote_works() {
1018 let init_funds = coins(10, "BTC");
1019 let mut app = mock_app(&init_funds);
1020
1021 let threshold = Threshold::ThresholdQuorum {
1022 threshold: Decimal::percent(51),
1023 quorum: Decimal::percent(1),
1024 };
1025 let voting_period = Duration::Time(2000000);
1026 let (flex_addr, _) = setup_test_case(
1027 &mut app,
1028 threshold,
1029 voting_period,
1030 init_funds,
1031 false,
1032 None,
1033 None,
1034 );
1035
1036 let proposal = pay_somebody_proposal();
1038 let res = app
1039 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1040 .unwrap();
1041
1042 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1044
1045 let yes_vote = ExecuteMsg::Vote {
1047 proposal_id,
1048 vote: Vote::Yes,
1049 };
1050 let err = app
1051 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &yes_vote, &[])
1052 .unwrap_err();
1053 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1054
1055 let err = app
1057 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &yes_vote, &[])
1058 .unwrap_err();
1059 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1060
1061 let res = app
1063 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &yes_vote, &[])
1064 .unwrap();
1065 assert_eq!(
1066 res.custom_attrs(1),
1067 [
1068 ("action", "vote"),
1069 ("sender", VOTER1),
1070 ("proposal_id", proposal_id.to_string().as_str()),
1071 ("status", "Open"),
1072 ],
1073 );
1074
1075 let err = app
1077 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &yes_vote, &[])
1078 .unwrap_err();
1079 assert_eq!(ContractError::AlreadyVoted {}, err.downcast().unwrap());
1080
1081 let tally = get_tally(&app, flex_addr.as_ref(), proposal_id);
1084 assert_eq!(tally, 1);
1085
1086 let no_vote = ExecuteMsg::Vote {
1088 proposal_id,
1089 vote: Vote::No,
1090 };
1091 let _ = app
1092 .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &no_vote, &[])
1093 .unwrap();
1094
1095 let veto_vote = ExecuteMsg::Vote {
1097 proposal_id,
1098 vote: Vote::Veto,
1099 };
1100 let _ = app
1101 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &veto_vote, &[])
1102 .unwrap();
1103
1104 assert_eq!(tally, get_tally(&app, flex_addr.as_ref(), proposal_id));
1106
1107 let err = app
1108 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1109 .unwrap_err();
1110 assert_eq!(ContractError::AlreadyVoted {}, err.downcast().unwrap());
1111
1112 app.update_block(expire(voting_period));
1114 let err = app
1115 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1116 .unwrap_err();
1117 assert_eq!(ContractError::Expired {}, err.downcast().unwrap());
1118 app.update_block(unexpire(voting_period));
1119
1120 let res = app
1122 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1123 .unwrap();
1124 assert_eq!(
1125 res.custom_attrs(1),
1126 [
1127 ("action", "vote"),
1128 ("sender", VOTER4),
1129 ("proposal_id", proposal_id.to_string().as_str()),
1130 ("status", "Passed"),
1131 ],
1132 );
1133
1134 let res = app
1136 .execute_contract(Addr::unchecked(VOTER5), flex_addr.clone(), &yes_vote, &[])
1137 .unwrap();
1138 assert_eq!(
1140 res.custom_attrs(1),
1141 [
1142 ("action", "vote"),
1143 ("sender", VOTER5),
1144 ("proposal_id", proposal_id.to_string().as_str()),
1145 ("status", "Passed")
1146 ]
1147 );
1148
1149 let voter = OWNER.into();
1152 let vote: VoteResponse = app
1153 .wrap()
1154 .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1155 .unwrap();
1156 assert_eq!(
1157 vote.vote.unwrap(),
1158 VoteInfo {
1159 proposal_id,
1160 voter: OWNER.into(),
1161 vote: Vote::Yes,
1162 weight: 0
1163 }
1164 );
1165
1166 let voter = VOTER2.into();
1168 let vote: VoteResponse = app
1169 .wrap()
1170 .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1171 .unwrap();
1172 assert_eq!(
1173 vote.vote.unwrap(),
1174 VoteInfo {
1175 proposal_id,
1176 voter: VOTER2.into(),
1177 vote: Vote::No,
1178 weight: 2
1179 }
1180 );
1181
1182 let voter = SOMEBODY.into();
1184 let vote: VoteResponse = app
1185 .wrap()
1186 .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1187 .unwrap();
1188 assert!(vote.vote.is_none());
1189
1190 let proposal = pay_somebody_proposal();
1192 let res = app
1193 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1194 .unwrap();
1195
1196 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1198
1199 let no_vote = ExecuteMsg::Vote {
1201 proposal_id,
1202 vote: Vote::No,
1203 };
1204 let _ = app
1205 .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &no_vote, &[])
1206 .unwrap();
1207
1208 let res = app
1210 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &no_vote, &[])
1211 .unwrap();
1212
1213 assert_eq!(
1214 res.custom_attrs(1),
1215 [
1216 ("action", "vote"),
1217 ("sender", VOTER4),
1218 ("proposal_id", proposal_id.to_string().as_str()),
1219 ("status", "Rejected"),
1220 ],
1221 );
1222
1223 let yes_vote = ExecuteMsg::Vote {
1225 proposal_id,
1226 vote: Vote::Yes,
1227 };
1228 let res = app
1229 .execute_contract(Addr::unchecked(VOTER5), flex_addr, &yes_vote, &[])
1230 .unwrap();
1231
1232 assert_eq!(
1233 res.custom_attrs(1),
1234 [
1235 ("action", "vote"),
1236 ("sender", VOTER5),
1237 ("proposal_id", proposal_id.to_string().as_str()),
1238 ("status", "Rejected"),
1239 ],
1240 );
1241 }
1242
1243 #[test]
1244 fn test_execute_works() {
1245 let init_funds = coins(10, "BTC");
1246 let mut app = mock_app(&init_funds);
1247
1248 let threshold = Threshold::ThresholdQuorum {
1249 threshold: Decimal::percent(51),
1250 quorum: Decimal::percent(1),
1251 };
1252 let voting_period = Duration::Time(2000000);
1253 let (flex_addr, _) = setup_test_case(
1254 &mut app,
1255 threshold,
1256 voting_period,
1257 init_funds,
1258 true,
1259 None,
1260 None,
1261 );
1262
1263 let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1265 assert_eq!(contract_bal, coin(10, "BTC"));
1266
1267 let proposal = pay_somebody_proposal();
1269 let res = app
1270 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1271 .unwrap();
1272
1273 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1275
1276 let execution = ExecuteMsg::Execute { proposal_id };
1278 let err = app
1279 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &execution, &[])
1280 .unwrap_err();
1281 assert_eq!(
1282 ContractError::WrongExecuteStatus {},
1283 err.downcast().unwrap()
1284 );
1285
1286 let vote = ExecuteMsg::Vote {
1288 proposal_id,
1289 vote: Vote::Yes,
1290 };
1291 let res = app
1292 .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1293 .unwrap();
1294 assert_eq!(
1295 res.custom_attrs(1),
1296 [
1297 ("action", "vote"),
1298 ("sender", VOTER4),
1299 ("proposal_id", proposal_id.to_string().as_str()),
1300 ("status", "Passed"),
1301 ],
1302 );
1303
1304 let closing = ExecuteMsg::Close { proposal_id };
1306 let err = app
1307 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &closing, &[])
1308 .unwrap_err();
1309 assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1310
1311 let res = app
1313 .execute_contract(
1314 Addr::unchecked(SOMEBODY),
1315 flex_addr.clone(),
1316 &execution,
1317 &[],
1318 )
1319 .unwrap();
1320 assert_eq!(
1321 res.custom_attrs(1),
1322 [
1323 ("action", "execute"),
1324 ("sender", SOMEBODY),
1325 ("proposal_id", proposal_id.to_string().as_str()),
1326 ],
1327 );
1328
1329 let some_bal = app.wrap().query_balance(SOMEBODY, "BTC").unwrap();
1331 assert_eq!(some_bal, coin(1, "BTC"));
1332 let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1333 assert_eq!(contract_bal, coin(9, "BTC"));
1334
1335 let err = app
1337 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &closing, &[])
1338 .unwrap_err();
1339 assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1340
1341 let err = app
1343 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr, &execution, &[])
1344 .unwrap_err();
1345 assert_eq!(
1346 ContractError::WrongExecuteStatus {},
1347 err.downcast().unwrap()
1348 );
1349 }
1350
1351 #[test]
1352 fn execute_with_executor_member() {
1353 let init_funds = coins(10, "BTC");
1354 let mut app = mock_app(&init_funds);
1355
1356 let threshold = Threshold::ThresholdQuorum {
1357 threshold: Decimal::percent(51),
1358 quorum: Decimal::percent(1),
1359 };
1360 let voting_period = Duration::Time(2000000);
1361 let (flex_addr, _) = setup_test_case(
1362 &mut app,
1363 threshold,
1364 voting_period,
1365 init_funds,
1366 true,
1367 Some(crate::state::Executor::Member), None,
1369 );
1370
1371 let proposal = pay_somebody_proposal();
1373 let res = app
1374 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1375 .unwrap();
1376
1377 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1379
1380 let vote = ExecuteMsg::Vote {
1382 proposal_id,
1383 vote: Vote::Yes,
1384 };
1385 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1386 .unwrap();
1387
1388 let execution = ExecuteMsg::Execute { proposal_id };
1389 let err = app
1390 .execute_contract(
1391 Addr::unchecked(Addr::unchecked("anyone")), flex_addr.clone(),
1393 &execution,
1394 &[],
1395 )
1396 .unwrap_err();
1397 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1398
1399 app.execute_contract(
1400 Addr::unchecked(Addr::unchecked(VOTER2)), flex_addr,
1402 &execution,
1403 &[],
1404 )
1405 .unwrap();
1406 }
1407
1408 #[test]
1409 fn execute_with_executor_only() {
1410 let init_funds = coins(10, "BTC");
1411 let mut app = mock_app(&init_funds);
1412
1413 let threshold = Threshold::ThresholdQuorum {
1414 threshold: Decimal::percent(51),
1415 quorum: Decimal::percent(1),
1416 };
1417 let voting_period = Duration::Time(2000000);
1418 let (flex_addr, _) = setup_test_case(
1419 &mut app,
1420 threshold,
1421 voting_period,
1422 init_funds,
1423 true,
1424 Some(crate::state::Executor::Only(Addr::unchecked(VOTER3))), None,
1426 );
1427
1428 let proposal = pay_somebody_proposal();
1430 let res = app
1431 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1432 .unwrap();
1433
1434 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1436
1437 let vote = ExecuteMsg::Vote {
1439 proposal_id,
1440 vote: Vote::Yes,
1441 };
1442 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1443 .unwrap();
1444
1445 let execution = ExecuteMsg::Execute { proposal_id };
1446 let err = app
1447 .execute_contract(
1448 Addr::unchecked(Addr::unchecked("anyone")), flex_addr.clone(),
1450 &execution,
1451 &[],
1452 )
1453 .unwrap_err();
1454 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1455
1456 let err = app
1457 .execute_contract(
1458 Addr::unchecked(Addr::unchecked(VOTER1)), flex_addr.clone(),
1460 &execution,
1461 &[],
1462 )
1463 .unwrap_err();
1464 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1465
1466 app.execute_contract(
1467 Addr::unchecked(Addr::unchecked(VOTER3)), flex_addr,
1469 &execution,
1470 &[],
1471 )
1472 .unwrap();
1473 }
1474
1475 #[test]
1476 fn proposal_pass_on_expiration() {
1477 let init_funds = coins(10, "BTC");
1478 let mut app = mock_app(&init_funds);
1479
1480 let threshold = Threshold::ThresholdQuorum {
1481 threshold: Decimal::percent(51),
1482 quorum: Decimal::percent(1),
1483 };
1484 let voting_period = 2000000;
1485 let (flex_addr, _) = setup_test_case(
1486 &mut app,
1487 threshold,
1488 Duration::Time(voting_period),
1489 init_funds,
1490 true,
1491 None,
1492 None,
1493 );
1494
1495 let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1497 assert_eq!(contract_bal, coin(10, "BTC"));
1498
1499 let proposal = pay_somebody_proposal();
1501 let res = app
1502 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1503 .unwrap();
1504
1505 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1507
1508 let vote = ExecuteMsg::Vote {
1510 proposal_id,
1511 vote: Vote::Yes,
1512 };
1513 let res = app
1514 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &vote, &[])
1515 .unwrap();
1516 assert_eq!(
1517 res.custom_attrs(1),
1518 [
1519 ("action", "vote"),
1520 ("sender", VOTER3),
1521 ("proposal_id", proposal_id.to_string().as_str()),
1522 ("status", "Open"),
1523 ],
1524 );
1525
1526 app.update_block(|block| {
1528 block.time = block.time.plus_seconds(voting_period);
1529 block.height += std::cmp::max(1, voting_period / 5);
1530 });
1531
1532 let prop: ProposalResponse = app
1534 .wrap()
1535 .query_wasm_smart(&flex_addr, &QueryMsg::Proposal { proposal_id })
1536 .unwrap();
1537 assert_eq!(prop.status, Status::Passed);
1538
1539 let err = app
1541 .execute_contract(
1542 Addr::unchecked(SOMEBODY),
1543 flex_addr.clone(),
1544 &ExecuteMsg::Close { proposal_id },
1545 &[],
1546 )
1547 .unwrap_err();
1548 assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1549
1550 let res = app
1552 .execute_contract(
1553 Addr::unchecked(SOMEBODY),
1554 flex_addr,
1555 &ExecuteMsg::Execute { proposal_id },
1556 &[],
1557 )
1558 .unwrap();
1559 assert_eq!(
1560 res.custom_attrs(1),
1561 [
1562 ("action", "execute"),
1563 ("sender", SOMEBODY),
1564 ("proposal_id", proposal_id.to_string().as_str()),
1565 ],
1566 );
1567 }
1568
1569 #[test]
1570 fn test_close_works() {
1571 let init_funds = coins(10, "BTC");
1572 let mut app = mock_app(&init_funds);
1573
1574 let threshold = Threshold::ThresholdQuorum {
1575 threshold: Decimal::percent(51),
1576 quorum: Decimal::percent(1),
1577 };
1578 let voting_period = Duration::Height(2000000);
1579 let (flex_addr, _) = setup_test_case(
1580 &mut app,
1581 threshold,
1582 voting_period,
1583 init_funds,
1584 true,
1585 None,
1586 None,
1587 );
1588
1589 let proposal = pay_somebody_proposal();
1591 let res = app
1592 .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1593 .unwrap();
1594
1595 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1597
1598 let closing = ExecuteMsg::Close { proposal_id };
1600 let err = app
1601 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &closing, &[])
1602 .unwrap_err();
1603 assert_eq!(ContractError::NotExpired {}, err.downcast().unwrap());
1604
1605 app.update_block(expire(voting_period));
1607 let res = app
1608 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &closing, &[])
1609 .unwrap();
1610 assert_eq!(
1611 res.custom_attrs(1),
1612 [
1613 ("action", "close"),
1614 ("sender", SOMEBODY),
1615 ("proposal_id", proposal_id.to_string().as_str()),
1616 ],
1617 );
1618
1619 let closing = ExecuteMsg::Close { proposal_id };
1621 let err = app
1622 .execute_contract(Addr::unchecked(SOMEBODY), flex_addr, &closing, &[])
1623 .unwrap_err();
1624 assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1625 }
1626
1627 #[test]
1629 fn execute_group_changes_from_external() {
1630 let init_funds = coins(10, "BTC");
1631 let mut app = mock_app(&init_funds);
1632
1633 let threshold = Threshold::ThresholdQuorum {
1634 threshold: Decimal::percent(51),
1635 quorum: Decimal::percent(1),
1636 };
1637 let voting_period = Duration::Time(20000);
1638 let (flex_addr, group_addr) = setup_test_case(
1639 &mut app,
1640 threshold,
1641 voting_period,
1642 init_funds,
1643 false,
1644 None,
1645 None,
1646 );
1647
1648 let proposal = pay_somebody_proposal();
1650 let res = app
1651 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[])
1652 .unwrap();
1653 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1655 let prop_status = |app: &App, proposal_id: u64| -> Status {
1656 let query_prop = QueryMsg::Proposal { proposal_id };
1657 let prop: ProposalResponse = app
1658 .wrap()
1659 .query_wasm_smart(&flex_addr, &query_prop)
1660 .unwrap();
1661 prop.status
1662 };
1663
1664 assert_eq!(prop_status(&app, proposal_id), Status::Open);
1666
1667 let threshold: ThresholdResponse = app
1669 .wrap()
1670 .query_wasm_smart(&flex_addr, &QueryMsg::Threshold {})
1671 .unwrap();
1672 let expected_thresh = ThresholdResponse::ThresholdQuorum {
1673 total_weight: 23,
1674 threshold: Decimal::percent(51),
1675 quorum: Decimal::percent(1),
1676 };
1677 assert_eq!(expected_thresh, threshold);
1678
1679 app.update_block(|block| block.height += 2);
1681
1682 let newbie: &str = "newbie";
1687 let update_msg = abstract_cw4_group::msg::ExecuteMsg::UpdateMembers {
1688 remove: vec![VOTER3.into()],
1689 add: vec![member(VOTER2, 21), member(newbie, 2)],
1690 };
1691 app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[])
1692 .unwrap();
1693
1694 let query_voter = QueryMsg::Voter {
1696 address: VOTER3.into(),
1697 };
1698 let power: VoterResponse = app
1699 .wrap()
1700 .query_wasm_smart(&flex_addr, &query_voter)
1701 .unwrap();
1702 assert_eq!(power.weight, None);
1703
1704 assert_eq!(prop_status(&app, proposal_id), Status::Open);
1706
1707 app.update_block(|block| block.height += 3);
1709
1710 let proposal2 = pay_somebody_proposal();
1712 let res = app
1713 .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal2, &[])
1714 .unwrap();
1715 let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1717
1718 let yes_vote = ExecuteMsg::Vote {
1720 proposal_id: proposal_id2,
1721 vote: Vote::Yes,
1722 };
1723 app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1724 .unwrap();
1725 assert_eq!(prop_status(&app, proposal_id2), Status::Passed);
1726
1727 let yes_vote = ExecuteMsg::Vote {
1729 proposal_id,
1730 vote: Vote::Yes,
1731 };
1732 app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1733 .unwrap();
1734 assert_eq!(prop_status(&app, proposal_id), Status::Open);
1735
1736 let err = app
1738 .execute_contract(Addr::unchecked(newbie), flex_addr.clone(), &yes_vote, &[])
1739 .unwrap_err();
1740 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1741
1742 app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1744 .unwrap();
1745
1746 let threshold: ThresholdResponse = app
1748 .wrap()
1749 .query_wasm_smart(&flex_addr, &QueryMsg::Threshold {})
1750 .unwrap();
1751 let expected_thresh = ThresholdResponse::ThresholdQuorum {
1752 total_weight: 41,
1753 threshold: Decimal::percent(51),
1754 quorum: Decimal::percent(1),
1755 };
1756 assert_eq!(expected_thresh, threshold);
1757
1758 }
1760
1761 #[test]
1765 fn execute_group_changes_from_proposal() {
1766 let init_funds = coins(10, "BTC");
1767 let mut app = mock_app(&init_funds);
1768
1769 let required_weight = 4;
1770 let voting_period = Duration::Time(20000);
1771 let (flex_addr, group_addr) =
1772 setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, true);
1773
1774 let update_msg = Cw4GroupContract::new(group_addr)
1776 .update_members(vec![VOTER3.into()], vec![])
1777 .unwrap();
1778 let update_proposal = ExecuteMsg::Propose {
1779 title: "Kick out VOTER3".to_string(),
1780 description: "He's trying to steal our money".to_string(),
1781 msgs: vec![update_msg],
1782 latest: None,
1783 };
1784 let res = app
1785 .execute_contract(
1786 Addr::unchecked(VOTER1),
1787 flex_addr.clone(),
1788 &update_proposal,
1789 &[],
1790 )
1791 .unwrap();
1792 let update_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1794
1795 app.update_block(|b| b.height += 1);
1797
1798 let cash_proposal = pay_somebody_proposal();
1800 let res = app
1801 .execute_contract(
1802 Addr::unchecked(VOTER1),
1803 flex_addr.clone(),
1804 &cash_proposal,
1805 &[],
1806 )
1807 .unwrap();
1808 let cash_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1810 assert_ne!(cash_proposal_id, update_proposal_id);
1811
1812 let prop_status = |app: &App, proposal_id: u64| -> Status {
1814 let query_prop = QueryMsg::Proposal { proposal_id };
1815 let prop: ProposalResponse = app
1816 .wrap()
1817 .query_wasm_smart(&flex_addr, &query_prop)
1818 .unwrap();
1819 prop.status
1820 };
1821 assert_eq!(prop_status(&app, cash_proposal_id), Status::Open);
1822 assert_eq!(prop_status(&app, update_proposal_id), Status::Open);
1823
1824 app.update_block(|b| b.height += 1);
1826
1827 let yes_vote = ExecuteMsg::Vote {
1829 proposal_id: update_proposal_id,
1830 vote: Vote::Yes,
1831 };
1832 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1833 .unwrap();
1834 let execution = ExecuteMsg::Execute {
1835 proposal_id: update_proposal_id,
1836 };
1837 app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &execution, &[])
1838 .unwrap();
1839
1840 assert_eq!(prop_status(&app, update_proposal_id), Status::Executed);
1842 assert_eq!(prop_status(&app, cash_proposal_id), Status::Open);
1843
1844 app.update_block(|b| b.height += 1);
1846
1847 let yes_vote = ExecuteMsg::Vote {
1850 proposal_id: cash_proposal_id,
1851 vote: Vote::Yes,
1852 };
1853 app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1854 .unwrap();
1855 assert_eq!(prop_status(&app, cash_proposal_id), Status::Passed);
1856
1857 let cash_proposal = pay_somebody_proposal();
1859 let err = app
1860 .execute_contract(
1861 Addr::unchecked(VOTER3),
1862 flex_addr.clone(),
1863 &cash_proposal,
1864 &[],
1865 )
1866 .unwrap_err();
1867 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1868
1869 let hook_hack = ExecuteMsg::MemberChangedHook(MemberChangedHookMsg {
1871 diffs: vec![MemberDiff::new(VOTER1, Some(1), None)],
1872 });
1873 let err = app
1874 .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &hook_hack, &[])
1875 .unwrap_err();
1876 assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1877 }
1878
1879 #[test]
1881 fn percentage_handles_group_changes() {
1882 let init_funds = coins(10, "BTC");
1883 let mut app = mock_app(&init_funds);
1884
1885 let threshold = Threshold::ThresholdQuorum {
1887 threshold: Decimal::percent(51),
1888 quorum: Decimal::percent(1),
1889 };
1890 let voting_period = Duration::Time(20000);
1891 let (flex_addr, group_addr) = setup_test_case(
1892 &mut app,
1893 threshold,
1894 voting_period,
1895 init_funds,
1896 false,
1897 None,
1898 None,
1899 );
1900
1901 let proposal = pay_somebody_proposal();
1903 let res = app
1904 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
1905 .unwrap();
1906 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1908 let prop_status = |app: &App| -> Status {
1909 let query_prop = QueryMsg::Proposal { proposal_id };
1910 let prop: ProposalResponse = app
1911 .wrap()
1912 .query_wasm_smart(&flex_addr, &query_prop)
1913 .unwrap();
1914 prop.status
1915 };
1916
1917 assert_eq!(prop_status(&app), Status::Open);
1919
1920 app.update_block(|block| block.height += 2);
1922
1923 let newbie: &str = "newbie";
1925 let update_msg = abstract_cw4_group::msg::ExecuteMsg::UpdateMembers {
1926 remove: vec![VOTER3.into()],
1927 add: vec![member(VOTER2, 9), member(newbie, 29)],
1928 };
1929 app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[])
1930 .unwrap();
1931
1932 app.update_block(|block| block.height += 3);
1934
1935 let yes_vote = ExecuteMsg::Vote {
1938 proposal_id,
1939 vote: Vote::Yes,
1940 };
1941 app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1942 .unwrap();
1943 assert_eq!(prop_status(&app), Status::Open);
1944
1945 let proposal = pay_somebody_proposal();
1947 let res = app
1948 .execute_contract(Addr::unchecked(newbie), flex_addr.clone(), &proposal, &[])
1949 .unwrap();
1950 let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1952
1953 let query_prop = QueryMsg::Proposal {
1955 proposal_id: proposal_id2,
1956 };
1957 let prop: ProposalResponse = app
1958 .wrap()
1959 .query_wasm_smart(&flex_addr, &query_prop)
1960 .unwrap();
1961 assert_eq!(Status::Passed, prop.status);
1962 }
1963
1964 #[test]
1966 fn quorum_handles_group_changes() {
1967 let init_funds = coins(10, "BTC");
1968 let mut app = mock_app(&init_funds);
1969
1970 let voting_period = Duration::Time(20000);
1973 let (flex_addr, group_addr) = setup_test_case(
1974 &mut app,
1975 Threshold::ThresholdQuorum {
1976 threshold: Decimal::percent(51),
1977 quorum: Decimal::percent(33),
1978 },
1979 voting_period,
1980 init_funds,
1981 false,
1982 None,
1983 None,
1984 );
1985
1986 let proposal = pay_somebody_proposal();
1988 let res = app
1989 .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
1990 .unwrap();
1991 let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1993 let prop_status = |app: &App| -> Status {
1994 let query_prop = QueryMsg::Proposal { proposal_id };
1995 let prop: ProposalResponse = app
1996 .wrap()
1997 .query_wasm_smart(&flex_addr, &query_prop)
1998 .unwrap();
1999 prop.status
2000 };
2001
2002 assert_eq!(prop_status(&app), Status::Open);
2004
2005 app.update_block(|block| block.height += 2);
2007
2008 let newbie: &str = "newbie";
2010 let update_msg = abstract_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 &abstract_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 &abstract_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: abstract_cw20::BalanceResponse = app
2233 .wrap()
2234 .query_wasm_smart(
2235 cw20_addr.clone(),
2236 &abstract_cw20::Cw20QueryMsg::Balance {
2237 address: VOTER4.to_string(),
2238 },
2239 )
2240 .unwrap();
2241 assert_eq!(balance.balance, Uint128::zero());
2242
2243 let balance: abstract_cw20::BalanceResponse = app
2244 .wrap()
2245 .query_wasm_smart(
2246 cw20_addr.clone(),
2247 &abstract_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: abstract_cw20::BalanceResponse = app
2264 .wrap()
2265 .query_wasm_smart(
2266 cw20_addr.clone(),
2267 &abstract_cw20::Cw20QueryMsg::Balance {
2268 address: VOTER4.to_string(),
2269 },
2270 )
2271 .unwrap();
2272 assert_eq!(balance.balance, Uint128::new(10));
2273
2274 let balance: abstract_cw20::BalanceResponse = app
2275 .wrap()
2276 .query_wasm_smart(
2277 cw20_addr.clone(),
2278 &abstract_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 &abstract_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: abstract_cw20::BalanceResponse = app
2304 .wrap()
2305 .query_wasm_smart(
2306 cw20_addr.clone(),
2307 &abstract_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: abstract_cw20::BalanceResponse = app
2339 .wrap()
2340 .query_wasm_smart(
2341 cw20_addr,
2342 &abstract_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}