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 cw2::set_contract_version;
11
12use cw3::{
13    Ballot, Proposal, ProposalListResponse, ProposalResponse, Status, Vote, VoteInfo,
14    VoteListResponse, VoteResponse, VoterDetail, VoterListResponse, VoterResponse, Votes,
15};
16use cw3_fixed_multisig::state::{next_id, BALLOTS, PROPOSALS};
17use cw4::{Cw4Contract, MemberChangedHookMsg, MemberDiff};
18use cw_storage_plus::Bound;
19use cw_utils::{maybe_addr, Expiration, ThresholdResponse};
20
21use crate::error::ContractError;
22use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
23use crate::state::{Config, CONFIG};
24
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 cw2::{query_contract_info, ContractVersion};
493    use cw20::{Cw20Coin, UncheckedDenom};
494    use cw3::{DepositError, UncheckedDepositInfo};
495    use cw4::{Cw4ExecuteMsg, Member};
496    use cw4_group::helpers::Cw4GroupContract;
497    use cw_multi_test::{
498        next_block, App, AppBuilder, BankSudo, Contract, ContractWrapper, Executor, SudoMsg,
499    };
500    use cw_utils::{Duration, Threshold};
501
502    use easy_addr::addr;
503
504    use super::*;
505
506    const OWNER: &str = addr!("admin0001");
507    const VOTER1: &str = addr!("voter0001");
508    const VOTER2: &str = addr!("voter0002");
509    const VOTER3: &str = addr!("voter0003");
510    const VOTER4: &str = addr!("voter0004");
511    const VOTER5: &str = addr!("voter0005");
512    const SOMEBODY: &str = addr!("somebody");
513    const NEWBIE: &str = addr!("newbie");
514
515    fn member<T: Into<String>>(addr: T, weight: u64) -> Member {
516        Member {
517            addr: addr.into(),
518            weight,
519        }
520    }
521
522    pub fn contract_flex() -> Box<dyn Contract<Empty>> {
523        let contract = ContractWrapper::new(
524            crate::contract::execute,
525            crate::contract::instantiate,
526            crate::contract::query,
527        );
528        Box::new(contract)
529    }
530
531    pub fn contract_group() -> Box<dyn Contract<Empty>> {
532        let contract = ContractWrapper::new(
533            cw4_group::contract::execute,
534            cw4_group::contract::instantiate,
535            cw4_group::contract::query,
536        );
537        Box::new(contract)
538    }
539
540    fn contract_cw20() -> Box<dyn Contract<Empty>> {
541        let contract = ContractWrapper::new(
542            cw20_base::contract::execute,
543            cw20_base::contract::instantiate,
544            cw20_base::contract::query,
545        );
546        Box::new(contract)
547    }
548
549    fn mock_app(init_funds: &[Coin]) -> App {
550        AppBuilder::new().build(|router, _, storage| {
551            router
552                .bank
553                .init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec())
554                .unwrap();
555        })
556    }
557
558    // uploads code and returns address of group contract
559    fn instantiate_group(app: &mut App, members: Vec<Member>) -> Addr {
560        let group_id = app.store_code(contract_group());
561        let msg = cw4_group::msg::InstantiateMsg {
562            admin: Some(OWNER.into()),
563            members,
564        };
565        app.instantiate_contract(group_id, Addr::unchecked(OWNER), &msg, &[], "group", None)
566            .unwrap()
567    }
568
569    #[track_caller]
570    fn instantiate_flex(
571        app: &mut App,
572        group: Addr,
573        threshold: Threshold,
574        max_voting_period: Duration,
575        executor: Option<crate::state::Executor>,
576        proposal_deposit: Option<UncheckedDepositInfo>,
577    ) -> Addr {
578        let flex_id = app.store_code(contract_flex());
579        let msg = crate::msg::InstantiateMsg {
580            group_addr: group.to_string(),
581            threshold,
582            max_voting_period,
583            executor,
584            proposal_deposit,
585        };
586        app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "flex", None)
587            .unwrap()
588    }
589
590    // this will set up both contracts, instantiating the group with
591    // all voters defined above, and the multisig pointing to it and given threshold criteria.
592    // Returns (multisig address, group address).
593    #[track_caller]
594    fn setup_test_case_fixed(
595        app: &mut App,
596        weight_needed: u64,
597        max_voting_period: Duration,
598        init_funds: Vec<Coin>,
599        multisig_as_group_admin: bool,
600    ) -> (Addr, Addr) {
601        setup_test_case(
602            app,
603            Threshold::AbsoluteCount {
604                weight: weight_needed,
605            },
606            max_voting_period,
607            init_funds,
608            multisig_as_group_admin,
609            None,
610            None,
611        )
612    }
613
614    #[track_caller]
615    fn setup_test_case(
616        app: &mut App,
617        threshold: Threshold,
618        max_voting_period: Duration,
619        init_funds: Vec<Coin>,
620        multisig_as_group_admin: bool,
621        executor: Option<crate::state::Executor>,
622        proposal_deposit: Option<UncheckedDepositInfo>,
623    ) -> (Addr, Addr) {
624        // 1. Instantiate group contract with members (and OWNER as admin)
625        let members = vec![
626            member(OWNER, 0),
627            member(VOTER1, 1),
628            member(VOTER2, 2),
629            member(VOTER3, 3),
630            member(VOTER4, 12), // so that he alone can pass a 50 / 52% threshold proposal
631            member(VOTER5, 5),
632        ];
633        let group_addr = instantiate_group(app, members);
634        app.update_block(next_block);
635
636        // 2. Set up Multisig backed by this group
637        let flex_addr = instantiate_flex(
638            app,
639            group_addr.clone(),
640            threshold,
641            max_voting_period,
642            executor,
643            proposal_deposit,
644        );
645        app.update_block(next_block);
646
647        // 3. (Optional) Set the multisig as the group owner
648        if multisig_as_group_admin {
649            let update_admin = Cw4ExecuteMsg::UpdateAdmin {
650                admin: Some(flex_addr.to_string()),
651            };
652            app.execute_contract(
653                Addr::unchecked(OWNER),
654                group_addr.clone(),
655                &update_admin,
656                &[],
657            )
658            .unwrap();
659            app.update_block(next_block);
660        }
661
662        // Bonus: set some funds on the multisig contract for future proposals
663        if !init_funds.is_empty() {
664            app.send_tokens(Addr::unchecked(OWNER), flex_addr.clone(), &init_funds)
665                .unwrap();
666        }
667        (flex_addr, group_addr)
668    }
669
670    fn proposal_info() -> (Vec<CosmosMsg<Empty>>, String, String) {
671        let bank_msg = BankMsg::Send {
672            to_address: SOMEBODY.into(),
673            amount: coins(1, "BTC"),
674        };
675        let msgs = vec![bank_msg.into()];
676        let title = "Pay somebody".to_string();
677        let description = "Do I pay her?".to_string();
678        (msgs, title, description)
679    }
680
681    fn pay_somebody_proposal() -> ExecuteMsg {
682        let (msgs, title, description) = proposal_info();
683        ExecuteMsg::Propose {
684            title,
685            description,
686            msgs,
687            latest: None,
688        }
689    }
690
691    fn text_proposal() -> ExecuteMsg {
692        let (_, title, description) = proposal_info();
693        ExecuteMsg::Propose {
694            title,
695            description,
696            msgs: vec![],
697            latest: None,
698        }
699    }
700
701    #[test]
702    fn test_instantiate_works() {
703        let mut app = mock_app(&[]);
704
705        // make a simple group
706        let group_addr = instantiate_group(&mut app, vec![member(OWNER, 1)]);
707        let flex_id = app.store_code(contract_flex());
708
709        let max_voting_period = Duration::Time(1234567);
710
711        // Zero required weight fails
712        let instantiate_msg = InstantiateMsg {
713            group_addr: group_addr.to_string(),
714            threshold: Threshold::ThresholdQuorum {
715                threshold: Decimal::zero(),
716                quorum: Decimal::percent(1),
717            },
718            max_voting_period,
719            executor: None,
720            proposal_deposit: None,
721        };
722        let err = app
723            .instantiate_contract(
724                flex_id,
725                Addr::unchecked(OWNER),
726                &instantiate_msg,
727                &[],
728                "zero required weight",
729                None,
730            )
731            .unwrap_err();
732        assert_eq!(
733            ContractError::Threshold(cw_utils::ThresholdError::InvalidThreshold {}),
734            err.downcast().unwrap()
735        );
736
737        // Total weight less than required weight not allowed
738        let instantiate_msg = InstantiateMsg {
739            group_addr: group_addr.to_string(),
740            threshold: Threshold::AbsoluteCount { weight: 100 },
741            max_voting_period,
742            executor: None,
743            proposal_deposit: None,
744        };
745        let err = app
746            .instantiate_contract(
747                flex_id,
748                Addr::unchecked(OWNER),
749                &instantiate_msg,
750                &[],
751                "high required weight",
752                None,
753            )
754            .unwrap_err();
755        assert_eq!(
756            ContractError::Threshold(cw_utils::ThresholdError::UnreachableWeight {}),
757            err.downcast().unwrap()
758        );
759
760        // All valid
761        let instantiate_msg = InstantiateMsg {
762            group_addr: group_addr.to_string(),
763            threshold: Threshold::AbsoluteCount { weight: 1 },
764            max_voting_period,
765            executor: None,
766            proposal_deposit: None,
767        };
768        let flex_addr = app
769            .instantiate_contract(
770                flex_id,
771                Addr::unchecked(OWNER),
772                &instantiate_msg,
773                &[],
774                "all good",
775                None,
776            )
777            .unwrap();
778
779        // Verify contract version set properly
780        let version = query_contract_info(&app.wrap(), flex_addr.clone()).unwrap();
781        assert_eq!(
782            ContractVersion {
783                contract: CONTRACT_NAME.to_string(),
784                version: CONTRACT_VERSION.to_string(),
785            },
786            version,
787        );
788
789        // Get voters query
790        let voters: VoterListResponse = app
791            .wrap()
792            .query_wasm_smart(
793                &flex_addr,
794                &QueryMsg::ListVoters {
795                    start_after: None,
796                    limit: None,
797                },
798            )
799            .unwrap();
800        assert_eq!(
801            voters.voters,
802            vec![VoterDetail {
803                addr: OWNER.into(),
804                weight: 1
805            }]
806        );
807    }
808
809    #[test]
810    fn test_propose_works() {
811        let init_funds = coins(10, "BTC");
812        let mut app = mock_app(&init_funds);
813
814        let required_weight = 4;
815        let voting_period = Duration::Time(2000000);
816        let (flex_addr, _) =
817            setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, false);
818
819        let proposal = pay_somebody_proposal();
820        // Only voters can propose
821        let err = app
822            .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &proposal, &[])
823            .unwrap_err();
824        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
825
826        // Wrong expiration option fails
827        let msgs = match proposal.clone() {
828            ExecuteMsg::Propose { msgs, .. } => msgs,
829            _ => panic!("Wrong variant"),
830        };
831        let proposal_wrong_exp = ExecuteMsg::Propose {
832            title: "Rewarding somebody".to_string(),
833            description: "Do we reward her?".to_string(),
834            msgs,
835            latest: Some(Expiration::AtHeight(123456)),
836        };
837        let err = app
838            .execute_contract(
839                Addr::unchecked(OWNER),
840                flex_addr.clone(),
841                &proposal_wrong_exp,
842                &[],
843            )
844            .unwrap_err();
845        assert_eq!(ContractError::WrongExpiration {}, err.downcast().unwrap());
846
847        // Proposal from voter works
848        let res = app
849            .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
850            .unwrap();
851        assert_eq!(
852            res.custom_attrs(1),
853            [
854                ("action", "propose"),
855                ("sender", VOTER3),
856                ("proposal_id", "1"),
857                ("status", "Open"),
858            ],
859        );
860
861        // Proposal from voter with enough vote power directly passes
862        let res = app
863            .execute_contract(Addr::unchecked(VOTER4), flex_addr, &proposal, &[])
864            .unwrap();
865        assert_eq!(
866            res.custom_attrs(1),
867            [
868                ("action", "propose"),
869                ("sender", VOTER4),
870                ("proposal_id", "2"),
871                ("status", "Passed"),
872            ],
873        );
874    }
875
876    fn get_tally(app: &App, flex_addr: &str, proposal_id: u64) -> u64 {
877        // Get all the voters on the proposal
878        let voters = QueryMsg::ListVotes {
879            proposal_id,
880            start_after: None,
881            limit: None,
882        };
883        let votes: VoteListResponse = app.wrap().query_wasm_smart(flex_addr, &voters).unwrap();
884        // Sum the weights of the Yes votes to get the tally
885        votes
886            .votes
887            .iter()
888            .filter(|&v| v.vote == Vote::Yes)
889            .map(|v| v.weight)
890            .sum()
891    }
892
893    fn expire(voting_period: Duration) -> impl Fn(&mut BlockInfo) {
894        move |block: &mut BlockInfo| {
895            match voting_period {
896                Duration::Time(duration) => block.time = block.time.plus_seconds(duration + 1),
897                Duration::Height(duration) => block.height += duration + 1,
898            };
899        }
900    }
901
902    fn unexpire(voting_period: Duration) -> impl Fn(&mut BlockInfo) {
903        move |block: &mut BlockInfo| {
904            match voting_period {
905                Duration::Time(duration) => {
906                    block.time =
907                        Timestamp::from_nanos(block.time.nanos() - (duration * 1_000_000_000));
908                }
909                Duration::Height(duration) => block.height -= duration,
910            };
911        }
912    }
913
914    #[test]
915    fn test_proposal_queries() {
916        let init_funds = coins(10, "BTC");
917        let mut app = mock_app(&init_funds);
918
919        let voting_period = Duration::Time(2000000);
920        let threshold = Threshold::ThresholdQuorum {
921            threshold: Decimal::percent(80),
922            quorum: Decimal::percent(20),
923        };
924        let (flex_addr, _) = setup_test_case(
925            &mut app,
926            threshold,
927            voting_period,
928            init_funds,
929            false,
930            None,
931            None,
932        );
933
934        // create proposal with 1 vote power
935        let proposal = pay_somebody_proposal();
936        let res = app
937            .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[])
938            .unwrap();
939        let proposal_id1: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
940
941        // another proposal immediately passes
942        app.update_block(next_block);
943        let proposal = pay_somebody_proposal();
944        let res = app
945            .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &proposal, &[])
946            .unwrap();
947        let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
948
949        // expire them both
950        app.update_block(expire(voting_period));
951
952        // add one more open proposal, 2 votes
953        let proposal = pay_somebody_proposal();
954        let res = app
955            .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &proposal, &[])
956            .unwrap();
957        let proposal_id3: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
958        let proposed_at = app.block_info();
959
960        // next block, let's query them all... make sure status is properly updated (1 should be rejected in query)
961        app.update_block(next_block);
962        let list_query = QueryMsg::ListProposals {
963            start_after: None,
964            limit: None,
965        };
966        let res: ProposalListResponse = app
967            .wrap()
968            .query_wasm_smart(&flex_addr, &list_query)
969            .unwrap();
970        assert_eq!(3, res.proposals.len());
971
972        // check the id and status are properly set
973        let info: Vec<_> = res.proposals.iter().map(|p| (p.id, p.status)).collect();
974        let expected_info = vec![
975            (proposal_id1, Status::Rejected),
976            (proposal_id2, Status::Passed),
977            (proposal_id3, Status::Open),
978        ];
979        assert_eq!(expected_info, info);
980
981        // ensure the common features are set
982        let (expected_msgs, expected_title, expected_description) = proposal_info();
983        for prop in res.proposals {
984            assert_eq!(prop.title, expected_title);
985            assert_eq!(prop.description, expected_description);
986            assert_eq!(prop.msgs, expected_msgs);
987        }
988
989        // reverse query can get just proposal_id3
990        let list_query = QueryMsg::ReverseProposals {
991            start_before: None,
992            limit: Some(1),
993        };
994        let res: ProposalListResponse = app
995            .wrap()
996            .query_wasm_smart(&flex_addr, &list_query)
997            .unwrap();
998        assert_eq!(1, res.proposals.len());
999
1000        let (msgs, title, description) = proposal_info();
1001        let expected = ProposalResponse {
1002            id: proposal_id3,
1003            title,
1004            description,
1005            msgs,
1006            expires: voting_period.after(&proposed_at),
1007            status: Status::Open,
1008            threshold: ThresholdResponse::ThresholdQuorum {
1009                total_weight: 23,
1010                threshold: Decimal::percent(80),
1011                quorum: Decimal::percent(20),
1012            },
1013            proposer: Addr::unchecked(VOTER2),
1014            deposit: None,
1015        };
1016        assert_eq!(&expected, &res.proposals[0]);
1017    }
1018
1019    #[test]
1020    fn test_vote_works() {
1021        let init_funds = coins(10, "BTC");
1022        let mut app = mock_app(&init_funds);
1023
1024        let threshold = Threshold::ThresholdQuorum {
1025            threshold: Decimal::percent(51),
1026            quorum: Decimal::percent(1),
1027        };
1028        let voting_period = Duration::Time(2000000);
1029        let (flex_addr, _) = setup_test_case(
1030            &mut app,
1031            threshold,
1032            voting_period,
1033            init_funds,
1034            false,
1035            None,
1036            None,
1037        );
1038
1039        // create proposal with 0 vote power
1040        let proposal = pay_somebody_proposal();
1041        let res = app
1042            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1043            .unwrap();
1044
1045        // Get the proposal id from the logs
1046        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1047
1048        // Owner with 0 voting power cannot vote
1049        let yes_vote = ExecuteMsg::Vote {
1050            proposal_id,
1051            vote: Vote::Yes,
1052        };
1053        let err = app
1054            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &yes_vote, &[])
1055            .unwrap_err();
1056        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1057
1058        // Only voters can vote
1059        let err = app
1060            .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &yes_vote, &[])
1061            .unwrap_err();
1062        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1063
1064        // But voter1 can
1065        let res = app
1066            .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &yes_vote, &[])
1067            .unwrap();
1068        assert_eq!(
1069            res.custom_attrs(1),
1070            [
1071                ("action", "vote"),
1072                ("sender", VOTER1),
1073                ("proposal_id", proposal_id.to_string().as_str()),
1074                ("status", "Open"),
1075            ],
1076        );
1077
1078        // VOTER1 cannot vote again
1079        let err = app
1080            .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &yes_vote, &[])
1081            .unwrap_err();
1082        assert_eq!(ContractError::AlreadyVoted {}, err.downcast().unwrap());
1083
1084        // No/Veto votes have no effect on the tally
1085        // Compute the current tally
1086        let tally = get_tally(&app, flex_addr.as_ref(), proposal_id);
1087        assert_eq!(tally, 1);
1088
1089        // Cast a No vote
1090        let no_vote = ExecuteMsg::Vote {
1091            proposal_id,
1092            vote: Vote::No,
1093        };
1094        let _ = app
1095            .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &no_vote, &[])
1096            .unwrap();
1097
1098        // Cast a Veto vote
1099        let veto_vote = ExecuteMsg::Vote {
1100            proposal_id,
1101            vote: Vote::Veto,
1102        };
1103        let _ = app
1104            .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &veto_vote, &[])
1105            .unwrap();
1106
1107        // Tally unchanged
1108        assert_eq!(tally, get_tally(&app, flex_addr.as_ref(), proposal_id));
1109
1110        let err = app
1111            .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1112            .unwrap_err();
1113        assert_eq!(ContractError::AlreadyVoted {}, err.downcast().unwrap());
1114
1115        // Expired proposals cannot be voted
1116        app.update_block(expire(voting_period));
1117        let err = app
1118            .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1119            .unwrap_err();
1120        assert_eq!(ContractError::Expired {}, err.downcast().unwrap());
1121        app.update_block(unexpire(voting_period));
1122
1123        // Powerful voter supports it, so it passes
1124        let res = app
1125            .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1126            .unwrap();
1127        assert_eq!(
1128            res.custom_attrs(1),
1129            [
1130                ("action", "vote"),
1131                ("sender", VOTER4),
1132                ("proposal_id", proposal_id.to_string().as_str()),
1133                ("status", "Passed"),
1134            ],
1135        );
1136
1137        // Passed proposals can still be voted (while they are not expired or executed)
1138        let res = app
1139            .execute_contract(Addr::unchecked(VOTER5), flex_addr.clone(), &yes_vote, &[])
1140            .unwrap();
1141        // Verify
1142        assert_eq!(
1143            res.custom_attrs(1),
1144            [
1145                ("action", "vote"),
1146                ("sender", VOTER5),
1147                ("proposal_id", proposal_id.to_string().as_str()),
1148                ("status", "Passed")
1149            ]
1150        );
1151
1152        // query individual votes
1153        // initial (with 0 weight)
1154        let voter = OWNER.into();
1155        let vote: VoteResponse = app
1156            .wrap()
1157            .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1158            .unwrap();
1159        assert_eq!(
1160            vote.vote.unwrap(),
1161            VoteInfo {
1162                proposal_id,
1163                voter: OWNER.into(),
1164                vote: Vote::Yes,
1165                weight: 0
1166            }
1167        );
1168
1169        // nay sayer
1170        let voter = VOTER2.into();
1171        let vote: VoteResponse = app
1172            .wrap()
1173            .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1174            .unwrap();
1175        assert_eq!(
1176            vote.vote.unwrap(),
1177            VoteInfo {
1178                proposal_id,
1179                voter: VOTER2.into(),
1180                vote: Vote::No,
1181                weight: 2
1182            }
1183        );
1184
1185        // non-voter
1186        let voter = SOMEBODY.into();
1187        let vote: VoteResponse = app
1188            .wrap()
1189            .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter })
1190            .unwrap();
1191        assert!(vote.vote.is_none());
1192
1193        // create proposal with 0 vote power
1194        let proposal = pay_somebody_proposal();
1195        let res = app
1196            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1197            .unwrap();
1198
1199        // Get the proposal id from the logs
1200        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1201
1202        // Cast a No vote
1203        let no_vote = ExecuteMsg::Vote {
1204            proposal_id,
1205            vote: Vote::No,
1206        };
1207        let _ = app
1208            .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &no_vote, &[])
1209            .unwrap();
1210
1211        // Powerful voter opposes it, so it rejects
1212        let res = app
1213            .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &no_vote, &[])
1214            .unwrap();
1215
1216        assert_eq!(
1217            res.custom_attrs(1),
1218            [
1219                ("action", "vote"),
1220                ("sender", VOTER4),
1221                ("proposal_id", proposal_id.to_string().as_str()),
1222                ("status", "Rejected"),
1223            ],
1224        );
1225
1226        // Rejected proposals can still be voted (while they are not expired)
1227        let yes_vote = ExecuteMsg::Vote {
1228            proposal_id,
1229            vote: Vote::Yes,
1230        };
1231        let res = app
1232            .execute_contract(Addr::unchecked(VOTER5), flex_addr, &yes_vote, &[])
1233            .unwrap();
1234
1235        assert_eq!(
1236            res.custom_attrs(1),
1237            [
1238                ("action", "vote"),
1239                ("sender", VOTER5),
1240                ("proposal_id", proposal_id.to_string().as_str()),
1241                ("status", "Rejected"),
1242            ],
1243        );
1244    }
1245
1246    #[test]
1247    fn test_execute_works() {
1248        let init_funds = coins(10, "BTC");
1249        let mut app = mock_app(&init_funds);
1250
1251        let threshold = Threshold::ThresholdQuorum {
1252            threshold: Decimal::percent(51),
1253            quorum: Decimal::percent(1),
1254        };
1255        let voting_period = Duration::Time(2000000);
1256        let (flex_addr, _) = setup_test_case(
1257            &mut app,
1258            threshold,
1259            voting_period,
1260            init_funds,
1261            true,
1262            None,
1263            None,
1264        );
1265
1266        // ensure we have cash to cover the proposal
1267        let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1268        assert_eq!(contract_bal, coin(10, "BTC"));
1269
1270        // create proposal with 0 vote power
1271        let proposal = pay_somebody_proposal();
1272        let res = app
1273            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1274            .unwrap();
1275
1276        // Get the proposal id from the logs
1277        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1278
1279        // Only Passed can be executed
1280        let execution = ExecuteMsg::Execute { proposal_id };
1281        let err = app
1282            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &execution, &[])
1283            .unwrap_err();
1284        assert_eq!(
1285            ContractError::WrongExecuteStatus {},
1286            err.downcast().unwrap()
1287        );
1288
1289        // Vote it, so it passes
1290        let vote = ExecuteMsg::Vote {
1291            proposal_id,
1292            vote: Vote::Yes,
1293        };
1294        let res = app
1295            .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1296            .unwrap();
1297        assert_eq!(
1298            res.custom_attrs(1),
1299            [
1300                ("action", "vote"),
1301                ("sender", VOTER4),
1302                ("proposal_id", proposal_id.to_string().as_str()),
1303                ("status", "Passed"),
1304            ],
1305        );
1306
1307        // In passing: Try to close Passed fails
1308        let closing = ExecuteMsg::Close { proposal_id };
1309        let err = app
1310            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &closing, &[])
1311            .unwrap_err();
1312        assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1313
1314        // Execute works. Anybody can execute Passed proposals
1315        let res = app
1316            .execute_contract(
1317                Addr::unchecked(SOMEBODY),
1318                flex_addr.clone(),
1319                &execution,
1320                &[],
1321            )
1322            .unwrap();
1323        assert_eq!(
1324            res.custom_attrs(1),
1325            [
1326                ("action", "execute"),
1327                ("sender", SOMEBODY),
1328                ("proposal_id", proposal_id.to_string().as_str()),
1329            ],
1330        );
1331
1332        // verify money was transfered
1333        let some_bal = app.wrap().query_balance(SOMEBODY, "BTC").unwrap();
1334        assert_eq!(some_bal, coin(1, "BTC"));
1335        let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1336        assert_eq!(contract_bal, coin(9, "BTC"));
1337
1338        // In passing: Try to close Executed fails
1339        let err = app
1340            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &closing, &[])
1341            .unwrap_err();
1342        assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1343
1344        // Trying to execute something that was already executed fails
1345        let err = app
1346            .execute_contract(Addr::unchecked(SOMEBODY), flex_addr, &execution, &[])
1347            .unwrap_err();
1348        assert_eq!(
1349            ContractError::WrongExecuteStatus {},
1350            err.downcast().unwrap()
1351        );
1352    }
1353
1354    #[test]
1355    fn execute_with_executor_member() {
1356        let init_funds = coins(10, "BTC");
1357        let mut app = mock_app(&init_funds);
1358
1359        let threshold = Threshold::ThresholdQuorum {
1360            threshold: Decimal::percent(51),
1361            quorum: Decimal::percent(1),
1362        };
1363        let voting_period = Duration::Time(2000000);
1364        let (flex_addr, _) = setup_test_case(
1365            &mut app,
1366            threshold,
1367            voting_period,
1368            init_funds,
1369            true,
1370            Some(crate::state::Executor::Member), // set executor as Member of voting group
1371            None,
1372        );
1373
1374        // create proposal with 0 vote power
1375        let proposal = pay_somebody_proposal();
1376        let res = app
1377            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1378            .unwrap();
1379
1380        // Get the proposal id from the logs
1381        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1382
1383        // Vote it, so it passes
1384        let vote = ExecuteMsg::Vote {
1385            proposal_id,
1386            vote: Vote::Yes,
1387        };
1388        app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1389            .unwrap();
1390
1391        let execution = ExecuteMsg::Execute { proposal_id };
1392        let err = app
1393            .execute_contract(
1394                Addr::unchecked(Addr::unchecked("anyone")), // anyone is not allowed to execute
1395                flex_addr.clone(),
1396                &execution,
1397                &[],
1398            )
1399            .unwrap_err();
1400        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1401
1402        app.execute_contract(
1403            Addr::unchecked(Addr::unchecked(VOTER2)), // member of voting group is allowed to execute
1404            flex_addr,
1405            &execution,
1406            &[],
1407        )
1408        .unwrap();
1409    }
1410
1411    #[test]
1412    fn execute_with_executor_only() {
1413        let init_funds = coins(10, "BTC");
1414        let mut app = mock_app(&init_funds);
1415
1416        let threshold = Threshold::ThresholdQuorum {
1417            threshold: Decimal::percent(51),
1418            quorum: Decimal::percent(1),
1419        };
1420        let voting_period = Duration::Time(2000000);
1421        let (flex_addr, _) = setup_test_case(
1422            &mut app,
1423            threshold,
1424            voting_period,
1425            init_funds,
1426            true,
1427            Some(crate::state::Executor::Only(Addr::unchecked(VOTER3))), // only VOTER3 can execute proposal
1428            None,
1429        );
1430
1431        // create proposal with 0 vote power
1432        let proposal = pay_somebody_proposal();
1433        let res = app
1434            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1435            .unwrap();
1436
1437        // Get the proposal id from the logs
1438        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1439
1440        // Vote it, so it passes
1441        let vote = ExecuteMsg::Vote {
1442            proposal_id,
1443            vote: Vote::Yes,
1444        };
1445        app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &vote, &[])
1446            .unwrap();
1447
1448        let execution = ExecuteMsg::Execute { proposal_id };
1449        let err = app
1450            .execute_contract(
1451                Addr::unchecked(Addr::unchecked("anyone")), // anyone is not allowed to execute
1452                flex_addr.clone(),
1453                &execution,
1454                &[],
1455            )
1456            .unwrap_err();
1457        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1458
1459        let err = app
1460            .execute_contract(
1461                Addr::unchecked(Addr::unchecked(VOTER1)), // VOTER1 is not allowed to execute
1462                flex_addr.clone(),
1463                &execution,
1464                &[],
1465            )
1466            .unwrap_err();
1467        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1468
1469        app.execute_contract(
1470            Addr::unchecked(Addr::unchecked(VOTER3)), // VOTER3 is allowed to execute
1471            flex_addr,
1472            &execution,
1473            &[],
1474        )
1475        .unwrap();
1476    }
1477
1478    #[test]
1479    fn proposal_pass_on_expiration() {
1480        let init_funds = coins(10, "BTC");
1481        let mut app = mock_app(&init_funds);
1482
1483        let threshold = Threshold::ThresholdQuorum {
1484            threshold: Decimal::percent(51),
1485            quorum: Decimal::percent(1),
1486        };
1487        let voting_period = 2000000;
1488        let (flex_addr, _) = setup_test_case(
1489            &mut app,
1490            threshold,
1491            Duration::Time(voting_period),
1492            init_funds,
1493            true,
1494            None,
1495            None,
1496        );
1497
1498        // ensure we have cash to cover the proposal
1499        let contract_bal = app.wrap().query_balance(&flex_addr, "BTC").unwrap();
1500        assert_eq!(contract_bal, coin(10, "BTC"));
1501
1502        // create proposal with 0 vote power
1503        let proposal = pay_somebody_proposal();
1504        let res = app
1505            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1506            .unwrap();
1507
1508        // Get the proposal id from the logs
1509        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1510
1511        // Vote it, so it passes after voting period is over
1512        let vote = ExecuteMsg::Vote {
1513            proposal_id,
1514            vote: Vote::Yes,
1515        };
1516        let res = app
1517            .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &vote, &[])
1518            .unwrap();
1519        assert_eq!(
1520            res.custom_attrs(1),
1521            [
1522                ("action", "vote"),
1523                ("sender", VOTER3),
1524                ("proposal_id", proposal_id.to_string().as_str()),
1525                ("status", "Open"),
1526            ],
1527        );
1528
1529        // Wait until the voting period is over.
1530        app.update_block(|block| {
1531            block.time = block.time.plus_seconds(voting_period);
1532            block.height += std::cmp::max(1, voting_period / 5);
1533        });
1534
1535        // Proposal should now be passed.
1536        let prop: ProposalResponse = app
1537            .wrap()
1538            .query_wasm_smart(&flex_addr, &QueryMsg::Proposal { proposal_id })
1539            .unwrap();
1540        assert_eq!(prop.status, Status::Passed);
1541
1542        // Closing should NOT be possible
1543        let err = app
1544            .execute_contract(
1545                Addr::unchecked(SOMEBODY),
1546                flex_addr.clone(),
1547                &ExecuteMsg::Close { proposal_id },
1548                &[],
1549            )
1550            .unwrap_err();
1551        assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1552
1553        // Execution should now be possible.
1554        let res = app
1555            .execute_contract(
1556                Addr::unchecked(SOMEBODY),
1557                flex_addr,
1558                &ExecuteMsg::Execute { proposal_id },
1559                &[],
1560            )
1561            .unwrap();
1562        assert_eq!(
1563            res.custom_attrs(1),
1564            [
1565                ("action", "execute"),
1566                ("sender", SOMEBODY),
1567                ("proposal_id", proposal_id.to_string().as_str()),
1568            ],
1569        );
1570    }
1571
1572    #[test]
1573    fn test_close_works() {
1574        let init_funds = coins(10, "BTC");
1575        let mut app = mock_app(&init_funds);
1576
1577        let threshold = Threshold::ThresholdQuorum {
1578            threshold: Decimal::percent(51),
1579            quorum: Decimal::percent(1),
1580        };
1581        let voting_period = Duration::Height(2000000);
1582        let (flex_addr, _) = setup_test_case(
1583            &mut app,
1584            threshold,
1585            voting_period,
1586            init_funds,
1587            true,
1588            None,
1589            None,
1590        );
1591
1592        // create proposal with 0 vote power
1593        let proposal = pay_somebody_proposal();
1594        let res = app
1595            .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[])
1596            .unwrap();
1597
1598        // Get the proposal id from the logs
1599        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1600
1601        // Non-expired proposals cannot be closed
1602        let closing = ExecuteMsg::Close { proposal_id };
1603        let err = app
1604            .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &closing, &[])
1605            .unwrap_err();
1606        assert_eq!(ContractError::NotExpired {}, err.downcast().unwrap());
1607
1608        // Expired proposals can be closed
1609        app.update_block(expire(voting_period));
1610        let res = app
1611            .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &closing, &[])
1612            .unwrap();
1613        assert_eq!(
1614            res.custom_attrs(1),
1615            [
1616                ("action", "close"),
1617                ("sender", SOMEBODY),
1618                ("proposal_id", proposal_id.to_string().as_str()),
1619            ],
1620        );
1621
1622        // Trying to close it again fails
1623        let closing = ExecuteMsg::Close { proposal_id };
1624        let err = app
1625            .execute_contract(Addr::unchecked(SOMEBODY), flex_addr, &closing, &[])
1626            .unwrap_err();
1627        assert_eq!(ContractError::WrongCloseStatus {}, err.downcast().unwrap());
1628    }
1629
1630    // uses the power from the beginning of the voting period
1631    #[test]
1632    fn execute_group_changes_from_external() {
1633        let init_funds = coins(10, "BTC");
1634        let mut app = mock_app(&init_funds);
1635
1636        let threshold = Threshold::ThresholdQuorum {
1637            threshold: Decimal::percent(51),
1638            quorum: Decimal::percent(1),
1639        };
1640        let voting_period = Duration::Time(20000);
1641        let (flex_addr, group_addr) = setup_test_case(
1642            &mut app,
1643            threshold,
1644            voting_period,
1645            init_funds,
1646            false,
1647            None,
1648            None,
1649        );
1650
1651        // VOTER1 starts a proposal to send some tokens (1/4 votes)
1652        let proposal = pay_somebody_proposal();
1653        let res = app
1654            .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[])
1655            .unwrap();
1656        // Get the proposal id from the logs
1657        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1658        let prop_status = |app: &App, proposal_id: u64| -> Status {
1659            let query_prop = QueryMsg::Proposal { proposal_id };
1660            let prop: ProposalResponse = app
1661                .wrap()
1662                .query_wasm_smart(&flex_addr, &query_prop)
1663                .unwrap();
1664            prop.status
1665        };
1666
1667        // 1/4 votes
1668        assert_eq!(prop_status(&app, proposal_id), Status::Open);
1669
1670        // check current threshold (global)
1671        let threshold: ThresholdResponse = app
1672            .wrap()
1673            .query_wasm_smart(&flex_addr, &QueryMsg::Threshold {})
1674            .unwrap();
1675        let expected_thresh = ThresholdResponse::ThresholdQuorum {
1676            total_weight: 23,
1677            threshold: Decimal::percent(51),
1678            quorum: Decimal::percent(1),
1679        };
1680        assert_eq!(expected_thresh, threshold);
1681
1682        // a few blocks later...
1683        app.update_block(|block| block.height += 2);
1684
1685        // admin changes the group
1686        // updates VOTER2 power to 21 -> with snapshot, vote doesn't pass proposal
1687        // adds NEWBIE with 2 power -> with snapshot, invalid vote
1688        // removes VOTER3 -> with snapshot, can vote on proposal
1689        let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
1690            remove: vec![VOTER3.into()],
1691            add: vec![member(VOTER2, 21), member(NEWBIE, 2)],
1692        };
1693        app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[])
1694            .unwrap();
1695
1696        // check membership queries properly updated
1697        let query_voter = QueryMsg::Voter {
1698            address: VOTER3.into(),
1699        };
1700        let power: VoterResponse = app
1701            .wrap()
1702            .query_wasm_smart(&flex_addr, &query_voter)
1703            .unwrap();
1704        assert_eq!(power.weight, None);
1705
1706        // proposal still open
1707        assert_eq!(prop_status(&app, proposal_id), Status::Open);
1708
1709        // a few blocks later...
1710        app.update_block(|block| block.height += 3);
1711
1712        // make a second proposal
1713        let proposal2 = pay_somebody_proposal();
1714        let res = app
1715            .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal2, &[])
1716            .unwrap();
1717        // Get the proposal id from the logs
1718        let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1719
1720        // VOTER2 can pass this alone with the updated vote (newer height ignores snapshot)
1721        let yes_vote = ExecuteMsg::Vote {
1722            proposal_id: proposal_id2,
1723            vote: Vote::Yes,
1724        };
1725        app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1726            .unwrap();
1727        assert_eq!(prop_status(&app, proposal_id2), Status::Passed);
1728
1729        // VOTER2 can only vote on first proposal with weight of 2 (not enough to pass)
1730        let yes_vote = ExecuteMsg::Vote {
1731            proposal_id,
1732            vote: Vote::Yes,
1733        };
1734        app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1735            .unwrap();
1736        assert_eq!(prop_status(&app, proposal_id), Status::Open);
1737
1738        // newbie cannot vote
1739        let err = app
1740            .execute_contract(Addr::unchecked(NEWBIE), flex_addr.clone(), &yes_vote, &[])
1741            .unwrap_err();
1742        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1743
1744        // previously removed VOTER3 can still vote, passing the proposal
1745        app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1746            .unwrap();
1747
1748        // check current threshold (global) is updated
1749        let threshold: ThresholdResponse = app
1750            .wrap()
1751            .query_wasm_smart(&flex_addr, &QueryMsg::Threshold {})
1752            .unwrap();
1753        let expected_thresh = ThresholdResponse::ThresholdQuorum {
1754            total_weight: 41,
1755            threshold: Decimal::percent(51),
1756            quorum: Decimal::percent(1),
1757        };
1758        assert_eq!(expected_thresh, threshold);
1759
1760        // TODO: check proposal threshold not changed
1761    }
1762
1763    // uses the power from the beginning of the voting period
1764    // similar to above - simpler case, but shows that one proposals can
1765    // trigger the action
1766    #[test]
1767    fn execute_group_changes_from_proposal() {
1768        let init_funds = coins(10, "BTC");
1769        let mut app = mock_app(&init_funds);
1770
1771        let required_weight = 4;
1772        let voting_period = Duration::Time(20000);
1773        let (flex_addr, group_addr) =
1774            setup_test_case_fixed(&mut app, required_weight, voting_period, init_funds, true);
1775
1776        // Start a proposal to remove VOTER3 from the set
1777        let update_msg = Cw4GroupContract::new(group_addr)
1778            .update_members(vec![VOTER3.into()], vec![])
1779            .unwrap();
1780        let update_proposal = ExecuteMsg::Propose {
1781            title: "Kick out VOTER3".to_string(),
1782            description: "He's trying to steal our money".to_string(),
1783            msgs: vec![update_msg],
1784            latest: None,
1785        };
1786        let res = app
1787            .execute_contract(
1788                Addr::unchecked(VOTER1),
1789                flex_addr.clone(),
1790                &update_proposal,
1791                &[],
1792            )
1793            .unwrap();
1794        // Get the proposal id from the logs
1795        let update_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1796
1797        // next block...
1798        app.update_block(|b| b.height += 1);
1799
1800        // VOTER1 starts a proposal to send some tokens
1801        let cash_proposal = pay_somebody_proposal();
1802        let res = app
1803            .execute_contract(
1804                Addr::unchecked(VOTER1),
1805                flex_addr.clone(),
1806                &cash_proposal,
1807                &[],
1808            )
1809            .unwrap();
1810        // Get the proposal id from the logs
1811        let cash_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1812        assert_ne!(cash_proposal_id, update_proposal_id);
1813
1814        // query proposal state
1815        let prop_status = |app: &App, proposal_id: u64| -> Status {
1816            let query_prop = QueryMsg::Proposal { proposal_id };
1817            let prop: ProposalResponse = app
1818                .wrap()
1819                .query_wasm_smart(&flex_addr, &query_prop)
1820                .unwrap();
1821            prop.status
1822        };
1823        assert_eq!(prop_status(&app, cash_proposal_id), Status::Open);
1824        assert_eq!(prop_status(&app, update_proposal_id), Status::Open);
1825
1826        // next block...
1827        app.update_block(|b| b.height += 1);
1828
1829        // Pass and execute first proposal
1830        let yes_vote = ExecuteMsg::Vote {
1831            proposal_id: update_proposal_id,
1832            vote: Vote::Yes,
1833        };
1834        app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &yes_vote, &[])
1835            .unwrap();
1836        let execution = ExecuteMsg::Execute {
1837            proposal_id: update_proposal_id,
1838        };
1839        app.execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &execution, &[])
1840            .unwrap();
1841
1842        // ensure that the update_proposal is executed, but the other unchanged
1843        assert_eq!(prop_status(&app, update_proposal_id), Status::Executed);
1844        assert_eq!(prop_status(&app, cash_proposal_id), Status::Open);
1845
1846        // next block...
1847        app.update_block(|b| b.height += 1);
1848
1849        // VOTER3 can still pass the cash proposal
1850        // voting on it fails
1851        let yes_vote = ExecuteMsg::Vote {
1852            proposal_id: cash_proposal_id,
1853            vote: Vote::Yes,
1854        };
1855        app.execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &yes_vote, &[])
1856            .unwrap();
1857        assert_eq!(prop_status(&app, cash_proposal_id), Status::Passed);
1858
1859        // but cannot open a new one
1860        let cash_proposal = pay_somebody_proposal();
1861        let err = app
1862            .execute_contract(
1863                Addr::unchecked(VOTER3),
1864                flex_addr.clone(),
1865                &cash_proposal,
1866                &[],
1867            )
1868            .unwrap_err();
1869        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1870
1871        // extra: ensure no one else can call the hook
1872        let hook_hack = ExecuteMsg::MemberChangedHook(MemberChangedHookMsg {
1873            diffs: vec![MemberDiff::new(VOTER1, Some(1), None)],
1874        });
1875        let err = app
1876            .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &hook_hack, &[])
1877            .unwrap_err();
1878        assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
1879    }
1880
1881    // uses the power from the beginning of the voting period
1882    #[test]
1883    fn percentage_handles_group_changes() {
1884        let init_funds = coins(10, "BTC");
1885        let mut app = mock_app(&init_funds);
1886
1887        // 51% required, which is 12 of the initial 24
1888        let threshold = Threshold::ThresholdQuorum {
1889            threshold: Decimal::percent(51),
1890            quorum: Decimal::percent(1),
1891        };
1892        let voting_period = Duration::Time(20000);
1893        let (flex_addr, group_addr) = setup_test_case(
1894            &mut app,
1895            threshold,
1896            voting_period,
1897            init_funds,
1898            false,
1899            None,
1900            None,
1901        );
1902
1903        // VOTER3 starts a proposal to send some tokens (3/12 votes)
1904        let proposal = pay_somebody_proposal();
1905        let res = app
1906            .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
1907            .unwrap();
1908        // Get the proposal id from the logs
1909        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1910        let prop_status = |app: &App| -> Status {
1911            let query_prop = QueryMsg::Proposal { proposal_id };
1912            let prop: ProposalResponse = app
1913                .wrap()
1914                .query_wasm_smart(&flex_addr, &query_prop)
1915                .unwrap();
1916            prop.status
1917        };
1918
1919        // 3/12 votes
1920        assert_eq!(prop_status(&app), Status::Open);
1921
1922        // a few blocks later...
1923        app.update_block(|block| block.height += 2);
1924
1925        // admin changes the group (3 -> 0, 2 -> 9, 0 -> 29) - total = 56, require 29 to pass
1926        let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
1927            remove: vec![VOTER3.into()],
1928            add: vec![member(VOTER2, 9), member(NEWBIE, 29)],
1929        };
1930        app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[])
1931            .unwrap();
1932
1933        // a few blocks later...
1934        app.update_block(|block| block.height += 3);
1935
1936        // VOTER2 votes according to original weights: 3 + 2 = 5 / 12 => Open
1937        // with updated weights, it would be 3 + 9 = 12 / 12 => Passed
1938        let yes_vote = ExecuteMsg::Vote {
1939            proposal_id,
1940            vote: Vote::Yes,
1941        };
1942        app.execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &yes_vote, &[])
1943            .unwrap();
1944        assert_eq!(prop_status(&app), Status::Open);
1945
1946        // new proposal can be passed single-handedly by newbie
1947        let proposal = pay_somebody_proposal();
1948        let res = app
1949            .execute_contract(Addr::unchecked(NEWBIE), flex_addr.clone(), &proposal, &[])
1950            .unwrap();
1951        // Get the proposal id from the logs
1952        let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1953
1954        // check proposal2 status
1955        let query_prop = QueryMsg::Proposal {
1956            proposal_id: proposal_id2,
1957        };
1958        let prop: ProposalResponse = app
1959            .wrap()
1960            .query_wasm_smart(&flex_addr, &query_prop)
1961            .unwrap();
1962        assert_eq!(Status::Passed, prop.status);
1963    }
1964
1965    // uses the power from the beginning of the voting period
1966    #[test]
1967    fn quorum_handles_group_changes() {
1968        let init_funds = coins(10, "BTC");
1969        let mut app = mock_app(&init_funds);
1970
1971        // 33% required for quora, which is 8 of the initial 24
1972        // 50% yes required to pass early (12 of the initial 24)
1973        let voting_period = Duration::Time(20000);
1974        let (flex_addr, group_addr) = setup_test_case(
1975            &mut app,
1976            Threshold::ThresholdQuorum {
1977                threshold: Decimal::percent(51),
1978                quorum: Decimal::percent(33),
1979            },
1980            voting_period,
1981            init_funds,
1982            false,
1983            None,
1984            None,
1985        );
1986
1987        // VOTER3 starts a proposal to send some tokens (3 votes)
1988        let proposal = pay_somebody_proposal();
1989        let res = app
1990            .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[])
1991            .unwrap();
1992        // Get the proposal id from the logs
1993        let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap();
1994        let prop_status = |app: &App| -> Status {
1995            let query_prop = QueryMsg::Proposal { proposal_id };
1996            let prop: ProposalResponse = app
1997                .wrap()
1998                .query_wasm_smart(&flex_addr, &query_prop)
1999                .unwrap();
2000            prop.status
2001        };
2002
2003        // 3/12 votes - not expired
2004        assert_eq!(prop_status(&app), Status::Open);
2005
2006        // a few blocks later...
2007        app.update_block(|block| block.height += 2);
2008
2009        // admin changes the group (3 -> 0, 2 -> 9, 0 -> 28) - total = 55, require 28 to pass
2010        let update_msg = cw4_group::msg::ExecuteMsg::UpdateMembers {
2011            remove: vec![VOTER3.into()],
2012            add: vec![member(VOTER2, 9), member(NEWBIE, 29)],
2013        };
2014        app.execute_contract(Addr::unchecked(OWNER), group_addr, &update_msg, &[])
2015            .unwrap();
2016
2017        // 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                &cw20_base::msg::InstantiateMsg {
2178                    name: "Token".to_string(),
2179                    symbol: "TOKEN".to_string(),
2180                    decimals: 6,
2181                    initial_balances: vec![
2182                        Cw20Coin {
2183                            address: VOTER4.to_string(),
2184                            amount: Uint128::new(10),
2185                        },
2186                        Cw20Coin {
2187                            address: OWNER.to_string(),
2188                            amount: Uint128::new(10),
2189                        },
2190                    ],
2191                    mint: None,
2192                    marketing: None,
2193                },
2194                &[],
2195                "Token",
2196                None,
2197            )
2198            .unwrap();
2199
2200        let (flex_addr, _) = setup_test_case(
2201            &mut app,
2202            Threshold::AbsoluteCount { weight: 10 },
2203            Duration::Height(10),
2204            vec![],
2205            true,
2206            None,
2207            Some(UncheckedDepositInfo {
2208                amount: Uint128::new(10),
2209                denom: UncheckedDenom::Cw20(cw20_addr.to_string()),
2210                refund_failed_proposals: true,
2211            }),
2212        );
2213
2214        app.execute_contract(
2215            Addr::unchecked(VOTER4),
2216            cw20_addr.clone(),
2217            &cw20::Cw20ExecuteMsg::IncreaseAllowance {
2218                spender: flex_addr.to_string(),
2219                amount: Uint128::new(10),
2220                expires: None,
2221            },
2222            &[],
2223        )
2224        .unwrap();
2225
2226        // 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: cw20::BalanceResponse = app
2233            .wrap()
2234            .query_wasm_smart(
2235                cw20_addr.clone(),
2236                &cw20::Cw20QueryMsg::Balance {
2237                    address: VOTER4.to_string(),
2238                },
2239            )
2240            .unwrap();
2241        assert_eq!(balance.balance, Uint128::zero());
2242
2243        let balance: cw20::BalanceResponse = app
2244            .wrap()
2245            .query_wasm_smart(
2246                cw20_addr.clone(),
2247                &cw20::Cw20QueryMsg::Balance {
2248                    address: flex_addr.to_string(),
2249                },
2250            )
2251            .unwrap();
2252        assert_eq!(balance.balance, Uint128::new(10));
2253
2254        app.execute_contract(
2255            Addr::unchecked(VOTER4),
2256            flex_addr.clone(),
2257            &ExecuteMsg::Execute { proposal_id: 1 },
2258            &[],
2259        )
2260        .unwrap();
2261
2262        // Make sure the deposit was returned.
2263        let balance: cw20::BalanceResponse = app
2264            .wrap()
2265            .query_wasm_smart(
2266                cw20_addr.clone(),
2267                &cw20::Cw20QueryMsg::Balance {
2268                    address: VOTER4.to_string(),
2269                },
2270            )
2271            .unwrap();
2272        assert_eq!(balance.balance, Uint128::new(10));
2273
2274        let balance: cw20::BalanceResponse = app
2275            .wrap()
2276            .query_wasm_smart(
2277                cw20_addr.clone(),
2278                &cw20::Cw20QueryMsg::Balance {
2279                    address: flex_addr.to_string(),
2280                },
2281            )
2282            .unwrap();
2283        assert_eq!(balance.balance, Uint128::zero());
2284
2285        app.execute_contract(
2286            Addr::unchecked(OWNER),
2287            cw20_addr.clone(),
2288            &cw20::Cw20ExecuteMsg::IncreaseAllowance {
2289                spender: flex_addr.to_string(),
2290                amount: Uint128::new(10),
2291                expires: None,
2292            },
2293            &[],
2294        )
2295        .unwrap();
2296
2297        // 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: cw20::BalanceResponse = app
2304            .wrap()
2305            .query_wasm_smart(
2306                cw20_addr.clone(),
2307                &cw20::Cw20QueryMsg::Balance {
2308                    address: flex_addr.to_string(),
2309                },
2310            )
2311            .unwrap();
2312        assert_eq!(balance.balance, Uint128::new(10));
2313
2314        // 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: cw20::BalanceResponse = app
2339            .wrap()
2340            .query_wasm_smart(
2341                cw20_addr,
2342                &cw20::Cw20QueryMsg::Balance {
2343                    address: VOTER4.to_string(),
2344                },
2345            )
2346            .unwrap();
2347        assert_eq!(balance.balance, Uint128::new(10));
2348    }
2349
2350    #[test]
2351    fn proposal_deposit_no_failed_refunds() {
2352        let mut app = App::default();
2353
2354        let (flex_addr, _) = setup_test_case(
2355            &mut app,
2356            Threshold::AbsoluteCount { weight: 10 },
2357            Duration::Height(10),
2358            vec![],
2359            true,
2360            None,
2361            Some(UncheckedDepositInfo {
2362                amount: Uint128::new(10),
2363                denom: UncheckedDenom::Native("TOKEN".to_string()),
2364                refund_failed_proposals: false,
2365            }),
2366        );
2367
2368        app.sudo(SudoMsg::Bank(BankSudo::Mint {
2369            to_address: OWNER.to_string(),
2370            amount: vec![Coin {
2371                amount: Uint128::new(10),
2372                denom: "TOKEN".to_string(),
2373            }],
2374        }))
2375        .unwrap();
2376
2377        // 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}