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