abstract_cw3_flex_multisig/
contract.rs

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
25// version info for migration info
26const 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    // we ignore earliest
94    latest: Option<Expiration>,
95) -> Result<Response<Empty>, ContractError> {
96    // only members of the multisig can create a proposal
97    let cfg = CONFIG.load(deps.storage)?;
98
99    // Check that the native deposit was paid (as needed).
100    if let Some(deposit) = cfg.proposal_deposit.as_ref() {
101        deposit.check_native_deposit_paid(&info)?;
102    }
103
104    // Only members of the multisig can create a proposal
105    // Non-voting members are special - they are allowed to create a proposal and
106    // therefore "vote", but they aren't allowed to vote otherwise.
107    // Such vote is also special, because despite having 0 weight it still counts when
108    // counting threshold passing
109    let vote_power = cfg
110        .group_addr
111        .is_member(&deps.querier, &info.sender, None)?
112        .ok_or(ContractError::Unauthorized {})?;
113
114    // max expires also used as default
115    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    // Take the cw20 token deposit, if required. We do this before
125    // creating the proposal struct below so that we can avoid a clone
126    // and move the loaded deposit info into it.
127    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    // create a proposal
134    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    // add the first yes vote from voter
152    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    // only members of the multisig can vote
174    let cfg = CONFIG.load(deps.storage)?;
175
176    // ensure proposal exists and can be voted on
177    let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
178    // Allow voting on Passed and Rejected proposals too,
179    if ![Status::Open, Status::Passed, Status::Rejected].contains(&prop.status) {
180        return Err(ContractError::NotOpen {});
181    }
182    // if they are not expired
183    if prop.expires.is_expired(&env.block) {
184        return Err(ContractError::Expired {});
185    }
186
187    // Only voting members of the multisig can vote
188    // Additional check if weight >= 1
189    // use a snapshot of "start of proposal"
190    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    // cast vote if no vote previously cast
196    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    // update vote tally
205    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    // we allow execution even after the proposal "expiration" as long as all vote come in before
224    // that point. If it was approved on time, it can be executed any time.
225    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    // set it to executed
234    prop.status = Status::Executed;
235    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
236
237    // Unconditionally refund here.
238    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    // dispatch all proposed messages
246    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    // anyone can trigger this if the vote passed
260
261    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    // Avoid closing of Passed due to expiration proposals
266    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    // set it to failed
274    prop.status = Status::Rejected;
275    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
276
277    // Refund the deposit if we have been configured to do so.
278    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    // This is now a no-op
298    // But we leave the authorization check as a demo
299    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
364// settings for pagination
365const 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    // uploads code and returns address of group contract
556    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    // this will set up both contracts, instantiating the group with
588    // all voters defined above, and the multisig pointing to it and given threshold criteria.
589    // Returns (multisig address, group address).
590    #[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        // 1. Instantiate group contract with members (and OWNER as admin)
622        let members = vec![
623            member(OWNER, 0),
624            member(VOTER1, 1),
625            member(VOTER2, 2),
626            member(VOTER3, 3),
627            member(VOTER4, 12), // so that he alone can pass a 50 / 52% threshold proposal
628            member(VOTER5, 5),
629        ];
630        let group_addr = instantiate_group(app, members);
631        app.update_block(next_block);
632
633        // 2. Set up Multisig backed by this group
634        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        // 3. (Optional) Set the multisig as the group owner
645        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        // Bonus: set some funds on the multisig contract for future proposals
660        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        // make a simple group
703        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        // Zero required weight fails
709        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        // Total weight less than required weight not allowed
735        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        // All valid
758        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        // Verify contract version set properly
777        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        // Get voters query
787        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        // Only voters can propose
818        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        // Wrong expiration option fails
824        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        // Proposal from voter works
845        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        // Proposal from voter with enough vote power directly passes
859        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        // Get all the voters on the proposal
875        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        // Sum the weights of the Yes votes to get the tally
882        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        // create proposal with 1 vote power
932        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        // another proposal immediately passes
939        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        // expire them both
947        app.update_block(expire(voting_period));
948
949        // add one more open proposal, 2 votes
950        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        // next block, let's query them all... make sure status is properly updated (1 should be rejected in query)
958        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        // check the id and status are properly set
970        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        // ensure the common features are set
979        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        // reverse query can get just proposal_id3
987        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        // create proposal with 0 vote power
1037        let proposal = pay_somebody_proposal();
1038        let res = app
1039            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1040            .unwrap();
1041
1042        // Get the proposal id from the logs
1043        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1044
1045        // Owner with 0 voting power cannot vote
1046        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        // Only voters can vote
1056        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        // But voter1 can
1062        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        // VOTER1 cannot vote again
1076        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        // No/Veto votes have no effect on the tally
1082        // Compute the current tally
1083        let tally = get_tally(&app, flex_addr.as_ref(), proposal_id);
1084        assert_eq!(tally, 1);
1085
1086        // Cast a No vote
1087        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        // Cast a Veto vote
1096        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        // Tally unchanged
1105        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        // Expired proposals cannot be voted
1113        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        // Powerful voter supports it, so it passes
1121        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        // Passed proposals can still be voted (while they are not expired or executed)
1135        let res = app
1136            .execute_contract(Addr::unchecked(VOTER5), flex_addr.clone(), &yes_vote, &[])
1137            .unwrap();
1138        // Verify
1139        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        // query individual votes
1150        // initial (with 0 weight)
1151        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        // nay sayer
1167        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        // non-voter
1183        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        // create proposal with 0 vote power
1191        let proposal = pay_somebody_proposal();
1192        let res = app
1193            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1194            .unwrap();
1195
1196        // Get the proposal id from the logs
1197        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1198
1199        // Cast a No vote
1200        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        // Powerful voter opposes it, so it rejects
1209        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        // Rejected proposals can still be voted (while they are not expired)
1224        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        // ensure we have cash to cover the proposal
1264        let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1265        assert_eq!(contract_bal, coin(10, "BTC"));
1266
1267        // create proposal with 0 vote power
1268        let proposal = pay_somebody_proposal();
1269        let res = app
1270            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1271            .unwrap();
1272
1273        // Get the proposal id from the logs
1274        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1275
1276        // Only Passed can be executed
1277        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        // Vote it, so it passes
1287        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        // In passing: Try to close Passed fails
1305        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        // Execute works. Anybody can execute Passed proposals
1312        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        // verify money was transfered
1330        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        // In passing: Try to close Executed fails
1336        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        // Trying to execute something that was already executed fails
1342        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), // set executor as Member of voting group
1368            None,
1369        );
1370
1371        // create proposal with 0 vote power
1372        let proposal = pay_somebody_proposal();
1373        let res = app
1374            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1375            .unwrap();
1376
1377        // Get the proposal id from the logs
1378        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1379
1380        // Vote it, so it passes
1381        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")), // anyone is not allowed to execute
1392                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)), // member of voting group is allowed to execute
1401            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))), // only VOTER3 can execute proposal
1425            None,
1426        );
1427
1428        // create proposal with 0 vote power
1429        let proposal = pay_somebody_proposal();
1430        let res = app
1431            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1432            .unwrap();
1433
1434        // Get the proposal id from the logs
1435        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1436
1437        // Vote it, so it passes
1438        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")), // anyone is not allowed to execute
1449                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)), // VOTER1 is not allowed to execute
1459                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)), // VOTER3 is allowed to execute
1468            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        // ensure we have cash to cover the proposal
1496        let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1497        assert_eq!(contract_bal, coin(10, "BTC"));
1498
1499        // create proposal with 0 vote power
1500        let proposal = pay_somebody_proposal();
1501        let res = app
1502            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1503            .unwrap();
1504
1505        // Get the proposal id from the logs
1506        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1507
1508        // Vote it, so it passes after voting period is over
1509        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        // Wait until the voting period is over.
1527        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        // Proposal should now be passed.
1533        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        // Closing should NOT be possible
1540        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        // Execution should now be possible.
1551        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        // create proposal with 0 vote power
1590        let proposal = pay_somebody_proposal();
1591        let res = app
1592            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1593            .unwrap();
1594
1595        // Get the proposal id from the logs
1596        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1597
1598        // Non-expired proposals cannot be closed
1599        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        // Expired proposals can be closed
1606        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        // Trying to close it again fails
1620        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    // uses the power from the beginning of the voting period
1628    #[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        // VOTER1 starts a proposal to send some tokens (1/4 votes)
1649        let proposal = pay_somebody_proposal();
1650        let res = app
1651            .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[])
1652            .unwrap();
1653        // Get the proposal id from the logs
1654        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        // 1/4 votes
1665        assert_eq!(prop_status(&app, proposal_id), Status::Open);
1666
1667        // check current threshold (global)
1668        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        // a few blocks later...
1680        app.update_block(|block| block.height += 2);
1681
1682        // admin changes the group
1683        // updates VOTER2 power to 21 -> with snapshot, vote doesn't pass proposal
1684        // adds NEWBIE with 2 power -> with snapshot, invalid vote
1685        // removes VOTER3 -> with snapshot, can vote on proposal
1686        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        // check membership queries properly updated
1695        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        // proposal still open
1705        assert_eq!(prop_status(&app, proposal_id), Status::Open);
1706
1707        // a few blocks later...
1708        app.update_block(|block| block.height += 3);
1709
1710        // make a second proposal
1711        let proposal2 = pay_somebody_proposal();
1712        let res = app
1713            .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal2, &[])
1714            .unwrap();
1715        // Get the proposal id from the logs
1716        let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1717
1718        // VOTER2 can pass this alone with the updated vote (newer height ignores snapshot)
1719        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        // VOTER2 can only vote on first proposal with weight of 2 (not enough to pass)
1728        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        // newbie cannot vote
1737        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        // previously removed VOTER3 can still vote, passing the proposal
1743        app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1744            .unwrap();
1745
1746        // check current threshold (global) is updated
1747        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        // TODO: check proposal threshold not changed
1759    }
1760
1761    // uses the power from the beginning of the voting period
1762    // similar to above - simpler case, but shows that one proposals can
1763    // trigger the action
1764    #[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        // Start a proposal to remove VOTER3 from the set
1775        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        // Get the proposal id from the logs
1793        let update_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1794
1795        // next block...
1796        app.update_block(|b| b.height += 1);
1797
1798        // VOTER1 starts a proposal to send some tokens
1799        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        // Get the proposal id from the logs
1809        let cash_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1810        assert_ne!(cash_proposal_id, update_proposal_id);
1811
1812        // query proposal state
1813        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        // next block...
1825        app.update_block(|b| b.height += 1);
1826
1827        // Pass and execute first proposal
1828        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        // ensure that the update_proposal is executed, but the other unchanged
1841        assert_eq!(prop_status(&app, update_proposal_id), Status::Executed);
1842        assert_eq!(prop_status(&app, cash_proposal_id), Status::Open);
1843
1844        // next block...
1845        app.update_block(|b| b.height += 1);
1846
1847        // VOTER3 can still pass the cash proposal
1848        // voting on it fails
1849        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        // but cannot open a new one
1858        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        // extra: ensure no one else can call the hook
1870        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    // uses the power from the beginning of the voting period
1880    #[test]
1881    fn percentage_handles_group_changes() {
1882        let init_funds = coins(10, "BTC");
1883        let mut app = mock_app(&init_funds);
1884
1885        // 51% required, which is 12 of the initial 24
1886        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        // VOTER3 starts a proposal to send some tokens (3/12 votes)
1902        let proposal = pay_somebody_proposal();
1903        let res = app
1904            .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
1905            .unwrap();
1906        // Get the proposal id from the logs
1907        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        // 3/12 votes
1918        assert_eq!(prop_status(&app), Status::Open);
1919
1920        // a few blocks later...
1921        app.update_block(|block| block.height += 2);
1922
1923        // admin changes the group (3 -> 0, 2 -> 9, 0 -> 29) - total = 56, require 29 to pass
1924        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        // a few blocks later...
1933        app.update_block(|block| block.height += 3);
1934
1935        // VOTER2 votes according to original weights: 3 + 2 = 5 / 12 => Open
1936        // with updated weights, it would be 3 + 9 = 12 / 12 => Passed
1937        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        // new proposal can be passed single-handedly by newbie
1946        let proposal = pay_somebody_proposal();
1947        let res = app
1948            .execute_contract(Addr::unchecked(newbie), flex_addr.clone(), &proposal, &[])
1949            .unwrap();
1950        // Get the proposal id from the logs
1951        let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1952
1953        // check proposal2 status
1954        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    // uses the power from the beginning of the voting period
1965    #[test]
1966    fn quorum_handles_group_changes() {
1967        let init_funds = coins(10, "BTC");
1968        let mut app = mock_app(&init_funds);
1969
1970        // 33% required for quora, which is 8 of the initial 24
1971        // 50% yes required to pass early (12 of the initial 24)
1972        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        // VOTER3 starts a proposal to send some tokens (3 votes)
1987        let proposal = pay_somebody_proposal();
1988        let res = app
1989            .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
1990            .unwrap();
1991        // Get the proposal id from the logs
1992        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        // 3/12 votes - not expired
2003        assert_eq!(prop_status(&app), Status::Open);
2004
2005        // a few blocks later...
2006        app.update_block(|block| block.height += 2);
2007
2008        // admin changes the group (3 -> 0, 2 -> 9, 0 -> 28) - total = 55, require 28 to pass
2009        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        // a few blocks later...
2018        app.update_block(|block| block.height += 3);
2019
2020        // VOTER2 votes yes, according to original weights: 3 yes, 2 no, 5 total (will fail when expired)
2021        // with updated weights, it would be 3 yes, 9 yes, 11 total (will pass when expired)
2022        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        // not expired yet
2029        assert_eq!(prop_status(&app), Status::Open);
2030
2031        // wait until the vote is over, and see it was rejected
2032        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        // 33% required for quora, which is 5 of the initial 15
2042        // 50% yes required to pass early (8 of the initial 15)
2043        let voting_period = Duration::Time(20000);
2044        let (flex_addr, _) = setup_test_case(
2045            &mut app,
2046            // note that 60% yes is not enough to pass without 20% no as well
2047            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        // create proposal
2059        let proposal = pay_somebody_proposal();
2060        let res = app
2061            .execute_contract(Addr::unchecked(VOTER5), flex_addr.clone(), &proposal, &[])
2062            .unwrap();
2063        // Get the proposal id from the logs
2064        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        // reach 60% of yes votes, not enough to pass early (or late)
2077        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        // 9 of 15 is 60% absolute threshold, but less than 12 (80% quorum needed)
2084        assert_eq!(prop_status(&app), Status::Open);
2085
2086        // add 3 weight no vote and we hit quorum and this passes
2087        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        // Instantiate with an invalid cw20 token.
2111        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        // Instantiate with a zero amount.
2139        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        // Make a proposal that will pass.
2227        let proposal = text_proposal();
2228        app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &proposal, &[])
2229            .unwrap();
2230
2231        // Make sure the deposit was transfered.
2232        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        // Make sure the deposit was returned.
2263        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        // Make a proposal that fails.
2298        let proposal = text_proposal();
2299        app.execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
2300            .unwrap();
2301
2302        // Check that the deposit was transfered.
2303        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        // Fail the proposal.
2315        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        // Expire the proposal.
2327        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        // Make sure the deposit was returned despite the proposal failing.
2338        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        // Make a proposal that fails.
2378        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        // Check that the deposit was transfered.
2391        let balance = app
2392            .wrap()
2393            .query_balance(OWNER, "TOKEN".to_string())
2394            .unwrap();
2395        assert_eq!(balance.amount, Uint128::zero());
2396
2397        // Fail the proposal.
2398        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        // Expire the proposal.
2410        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        // Check that the deposit wasn't returned.
2421        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        // Make a proposal that will pass.
2465        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        // Make sure the deposit was transfered.
2478        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        // Make sure the deposit was returned.
2493        let balance = app.wrap().query_balance(VOTER4, "TOKEN").unwrap();
2494        assert_eq!(balance.amount, Uint128::new(10));
2495
2496        // Make a proposal that fails.
2497        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        // Fail the proposal.
2516        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        // Expire the proposal.
2528        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        // Make sure the deposit was returned despite the proposal failing.
2539        let balance = app.wrap().query_balance(OWNER, "TOKEN").unwrap();
2540        assert_eq!(balance.amount, Uint128::new(10));
2541    }
2542}