dao_proposal_multiple/
contract.rs

1#[cfg(not(feature = "library"))]
2use cosmwasm_std::entry_point;
3use cosmwasm_std::{
4    to_json_binary, Addr, Attribute, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply,
5    Response, StdResult, Storage, SubMsg, WasmMsg,
6};
7
8use cw2::set_contract_version;
9use cw_hooks::Hooks;
10use cw_storage_plus::Bound;
11use cw_utils::{parse_reply_instantiate_data, Duration};
12use dao_hooks::proposal::{
13    new_proposal_hooks, proposal_completed_hooks, proposal_status_changed_hooks,
14};
15use dao_hooks::vote::new_vote_hooks;
16use dao_interface::voting::IsActiveResponse;
17use dao_voting::{
18    multiple_choice::{MultipleChoiceVote, MultipleChoiceVotes, VotingStrategy},
19    pre_propose::{PreProposeInfo, ProposalCreationPolicy},
20    proposal::{MultipleChoiceProposeMsg as ProposeMsg, DEFAULT_LIMIT, MAX_PROPOSAL_SIZE},
21    reply::{
22        failed_pre_propose_module_hook_id, mask_proposal_execution_proposal_id, TaggedReplyId,
23    },
24    status::Status,
25    veto::{VetoConfig, VetoError},
26    voting::{get_total_power, get_voting_power, validate_voting_period},
27};
28
29use crate::{msg::MigrateMsg, state::CREATION_POLICY};
30use crate::{
31    msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
32    proposal::{MultipleChoiceProposal, VoteResult},
33    query::{ProposalListResponse, ProposalResponse, VoteInfo, VoteListResponse, VoteResponse},
34    state::{
35        Ballot, Config, BALLOTS, CONFIG, PROPOSALS, PROPOSAL_COUNT, PROPOSAL_HOOKS, VOTE_HOOKS,
36    },
37    ContractError,
38};
39
40pub const CONTRACT_NAME: &str = "crates.io:dao-proposal-multiple";
41pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
42
43#[cfg_attr(not(feature = "library"), entry_point)]
44pub fn instantiate(
45    deps: DepsMut,
46    _env: Env,
47    info: MessageInfo,
48    msg: InstantiateMsg,
49) -> Result<Response, ContractError> {
50    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
51
52    msg.voting_strategy.validate()?;
53
54    let dao = info.sender;
55
56    let (min_voting_period, max_voting_period) =
57        validate_voting_period(msg.min_voting_period, msg.max_voting_period)?;
58
59    let (initial_policy, pre_propose_messages) = msg
60        .pre_propose_info
61        .into_initial_policy_and_messages(dao.clone())?;
62
63    // if veto is configured, validate its fields
64    if let Some(veto_config) = &msg.veto {
65        veto_config.validate(&deps.as_ref(), &max_voting_period)?;
66    };
67
68    let config = Config {
69        voting_strategy: msg.voting_strategy,
70        min_voting_period,
71        max_voting_period,
72        only_members_execute: msg.only_members_execute,
73        allow_revoting: msg.allow_revoting,
74        dao,
75        close_proposal_on_execution_failure: msg.close_proposal_on_execution_failure,
76        veto: msg.veto,
77    };
78
79    // Initialize proposal count to zero so that queries return zero
80    // instead of None.
81    PROPOSAL_COUNT.save(deps.storage, &0)?;
82    CONFIG.save(deps.storage, &config)?;
83    CREATION_POLICY.save(deps.storage, &initial_policy)?;
84
85    Ok(Response::default()
86        .add_submessages(pre_propose_messages)
87        .add_attribute("action", "instantiate")
88        .add_attribute("dao", config.dao))
89}
90
91#[cfg_attr(not(feature = "library"), entry_point)]
92pub fn execute(
93    deps: DepsMut,
94    env: Env,
95    info: MessageInfo,
96    msg: ExecuteMsg,
97) -> Result<Response<Empty>, ContractError> {
98    match msg {
99        ExecuteMsg::Propose(propose_msg) => execute_propose(deps, env, info, propose_msg),
100        ExecuteMsg::Vote {
101            proposal_id,
102            vote,
103            rationale,
104        } => execute_vote(deps, env, info.sender, proposal_id, vote, rationale),
105        ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id),
106        ExecuteMsg::Veto { proposal_id } => execute_veto(deps, env, info, proposal_id),
107        ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id),
108        ExecuteMsg::UpdateConfig {
109            voting_strategy,
110            min_voting_period,
111            max_voting_period,
112            only_members_execute,
113            allow_revoting,
114            dao,
115            close_proposal_on_execution_failure,
116            veto,
117        } => execute_update_config(
118            deps,
119            info,
120            voting_strategy,
121            min_voting_period,
122            max_voting_period,
123            only_members_execute,
124            allow_revoting,
125            dao,
126            close_proposal_on_execution_failure,
127            veto,
128        ),
129        ExecuteMsg::UpdatePreProposeInfo { info: new_info } => {
130            execute_update_proposal_creation_policy(deps, info, new_info)
131        }
132        ExecuteMsg::AddProposalHook { address } => {
133            execute_add_proposal_hook(deps, env, info, address)
134        }
135        ExecuteMsg::RemoveProposalHook { address } => {
136            execute_remove_proposal_hook(deps, env, info, address)
137        }
138        ExecuteMsg::AddVoteHook { address } => execute_add_vote_hook(deps, env, info, address),
139        ExecuteMsg::RemoveVoteHook { address } => {
140            execute_remove_vote_hook(deps, env, info, address)
141        }
142        ExecuteMsg::UpdateRationale {
143            proposal_id,
144            rationale,
145        } => execute_update_rationale(deps, info, proposal_id, rationale),
146    }
147}
148
149pub fn execute_propose(
150    deps: DepsMut,
151    env: Env,
152    info: MessageInfo,
153    ProposeMsg {
154        title,
155        description,
156        choices,
157        proposer,
158        vote,
159    }: ProposeMsg,
160) -> Result<Response<Empty>, ContractError> {
161    let config = CONFIG.load(deps.storage)?;
162    let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?;
163
164    // Check that the sender is permitted to create proposals.
165    if !proposal_creation_policy.is_permitted(&info.sender) {
166        return Err(ContractError::Unauthorized {});
167    }
168
169    // Determine the appropriate proposer. If this is coming from our
170    // pre-propose module, it must be specified. Otherwise, the
171    // proposer should not be specified.
172    let proposer = match (proposer, &proposal_creation_policy) {
173        (None, ProposalCreationPolicy::Anyone {}) => info.sender.clone(),
174        // `is_permitted` above checks that an allowed module is
175        // actually sending the propose message.
176        (Some(proposer), ProposalCreationPolicy::Module { .. }) => {
177            deps.api.addr_validate(&proposer)?
178        }
179        _ => return Err(ContractError::InvalidProposer {}),
180    };
181
182    let voting_module: Addr = deps.querier.query_wasm_smart(
183        config.dao.clone(),
184        &dao_interface::msg::QueryMsg::VotingModule {},
185    )?;
186
187    // Voting modules are not required to implement this
188    // query. Lacking an implementation they are active by default.
189    let active_resp: IsActiveResponse = deps
190        .querier
191        .query_wasm_smart(voting_module, &dao_interface::voting::Query::IsActive {})
192        .unwrap_or(IsActiveResponse { active: true });
193
194    if !active_resp.active {
195        return Err(ContractError::InactiveDao {});
196    }
197
198    // Validate options.
199    let checked_multiple_choice_options = choices.into_checked()?.options;
200
201    let expiration = config.max_voting_period.after(&env.block);
202    let total_power = get_total_power(deps.as_ref(), &config.dao, None)?;
203
204    let proposal = {
205        // Limit mutability to this block.
206        let mut proposal = MultipleChoiceProposal {
207            title,
208            description,
209            proposer: proposer.clone(),
210            start_height: env.block.height,
211            min_voting_period: config.min_voting_period.map(|min| min.after(&env.block)),
212            expiration,
213            voting_strategy: config.voting_strategy,
214            total_power,
215            status: Status::Open,
216            votes: MultipleChoiceVotes::zero(checked_multiple_choice_options.len()),
217            allow_revoting: config.allow_revoting,
218            choices: checked_multiple_choice_options,
219            veto: config.veto,
220        };
221        // Update the proposal's status. Addresses case where proposal
222        // expires on the same block as it is created.
223        proposal.update_status(&env.block)?;
224        proposal
225    };
226    let id = advance_proposal_id(deps.storage)?;
227
228    // Limit the size of proposals.
229    //
230    // The Juno mainnet has a larger limit for data that can be
231    // uploaded as part of an execute message than it does for data
232    // that can be queried as part of a query. This means that without
233    // this check it is possible to create a proposal that can not be
234    // queried.
235    //
236    // The size selected was determined by uploading versions of this
237    // contract to the Juno mainnet until queries worked within a
238    // reasonable margin of error.
239    //
240    // `to_vec` is the method used by cosmwasm to convert a struct
241    // into it's byte representation in storage.
242    let proposal_size = cosmwasm_std::to_json_vec(&proposal)?.len() as u64;
243    if proposal_size > MAX_PROPOSAL_SIZE {
244        return Err(ContractError::ProposalTooLarge {
245            size: proposal_size,
246            max: MAX_PROPOSAL_SIZE,
247        });
248    }
249
250    PROPOSALS.save(deps.storage, id, &proposal)?;
251
252    let hooks = new_proposal_hooks(PROPOSAL_HOOKS, deps.storage, id, proposer.as_str())?;
253
254    let sender = info.sender.clone();
255
256    // Auto cast vote if given.
257    let (vote_hooks, vote_attributes) = if let Some(vote) = vote {
258        let response = execute_vote(
259            deps,
260            env,
261            proposer.clone(),
262            id,
263            vote.vote,
264            vote.rationale.clone(),
265        )?;
266        (
267            response.messages,
268            vec![
269                Attribute {
270                    key: "position".to_string(),
271                    value: vote.vote.to_string(),
272                },
273                Attribute {
274                    key: "rationale".to_string(),
275                    value: vote.rationale.unwrap_or_else(|| "_none".to_string()),
276                },
277            ],
278        )
279    } else {
280        (vec![], vec![])
281    };
282
283    Ok(Response::default()
284        .add_submessages(hooks)
285        .add_submessages(vote_hooks)
286        .add_attribute("action", "propose")
287        .add_attribute("sender", sender)
288        .add_attribute("proposal_id", id.to_string())
289        .add_attributes(vote_attributes)
290        .add_attribute("status", proposal.status.to_string()))
291}
292
293pub fn execute_veto(
294    deps: DepsMut,
295    env: Env,
296    info: MessageInfo,
297    proposal_id: u64,
298) -> Result<Response, ContractError> {
299    let mut prop = PROPOSALS
300        .may_load(deps.storage, proposal_id)?
301        .ok_or(ContractError::NoSuchProposal { id: proposal_id })?;
302
303    // ensure status is up to date
304    prop.update_status(&env.block)?;
305    let old_status = prop.status;
306
307    let veto_config = prop
308        .veto
309        .as_ref()
310        .ok_or(VetoError::NoVetoConfiguration {})?;
311
312    // Check sender is vetoer
313    veto_config.check_is_vetoer(&info)?;
314
315    match prop.status {
316        Status::Open => {
317            // can only veto an open proposal if veto_before_passed is enabled.
318            veto_config.check_veto_before_passed_enabled()?;
319        }
320        Status::Passed => {
321            // if this proposal has veto configured but is in the passed state,
322            // the timelock already expired, so provide a more specific error.
323            return Err(ContractError::VetoError(VetoError::TimelockExpired {}));
324        }
325        Status::VetoTimelock { expiration } => {
326            // vetoer can veto the proposal iff the timelock is active/not
327            // expired. this should never happen since the status updates to
328            // passed after the timelock expires, but let's check anyway.
329            if expiration.is_expired(&env.block) {
330                return Err(ContractError::VetoError(VetoError::TimelockExpired {}));
331            }
332        }
333        // generic status error if the proposal has any other status.
334        _ => {
335            return Err(ContractError::VetoError(VetoError::InvalidProposalStatus {
336                status: prop.status.to_string(),
337            }));
338        }
339    }
340
341    // Update proposal status to vetoed
342    prop.status = Status::Vetoed;
343    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
344
345    // Add proposal status change hooks
346    let proposal_status_changed_hooks = proposal_status_changed_hooks(
347        PROPOSAL_HOOKS,
348        deps.storage,
349        proposal_id,
350        old_status.to_string(),
351        prop.status.to_string(),
352    )?;
353
354    // Add prepropose / deposit module hook which will handle deposit refunds.
355    let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?;
356    let proposal_completed_hooks =
357        proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?;
358
359    Ok(Response::new()
360        .add_attribute("action", "veto")
361        .add_attribute("proposal_id", proposal_id.to_string())
362        .add_submessages(proposal_status_changed_hooks)
363        .add_submessages(proposal_completed_hooks))
364}
365
366pub fn execute_vote(
367    deps: DepsMut,
368    env: Env,
369    sender: Addr,
370    proposal_id: u64,
371    vote: MultipleChoiceVote,
372    rationale: Option<String>,
373) -> Result<Response<Empty>, ContractError> {
374    let config = CONFIG.load(deps.storage)?;
375    let mut prop = PROPOSALS
376        .may_load(deps.storage, proposal_id)?
377        .ok_or(ContractError::NoSuchProposal { id: proposal_id })?;
378
379    // Check that this is a valid vote.
380    if vote.option_id as usize >= prop.choices.len() {
381        return Err(ContractError::InvalidVote {});
382    }
383
384    // Allow voting on proposals until they expire.
385    // Voting on a non-open proposal will never change
386    // their outcome as if an outcome has been determined,
387    // it is because no possible sequence of votes may
388    // cause a different one. This then serves to allow
389    // for better tallies of opinions in the event that a
390    // proposal passes or is rejected early.
391    if prop.expiration.is_expired(&env.block) {
392        return Err(ContractError::Expired { id: proposal_id });
393    }
394
395    let vote_power = get_voting_power(
396        deps.as_ref(),
397        sender.clone(),
398        &config.dao,
399        Some(prop.start_height),
400    )?;
401    if vote_power.is_zero() {
402        return Err(ContractError::NotRegistered {});
403    }
404
405    BALLOTS.update(deps.storage, (proposal_id, &sender), |bal| match bal {
406        Some(current_ballot) => {
407            if prop.allow_revoting {
408                if current_ballot.vote == vote {
409                    // Don't allow casting the same vote more than
410                    // once. This seems liable to be confusing
411                    // behavior.
412                    Err(ContractError::AlreadyCast {})
413                } else {
414                    // Remove the old vote if this is a re-vote.
415                    prop.votes
416                        .remove_vote(current_ballot.vote, current_ballot.power)?;
417                    Ok(Ballot {
418                        power: vote_power,
419                        vote,
420                        rationale: rationale.clone(),
421                    })
422                }
423            } else {
424                Err(ContractError::AlreadyVoted {})
425            }
426        }
427        None => Ok(Ballot {
428            vote,
429            power: vote_power,
430            rationale: rationale.clone(),
431        }),
432    })?;
433
434    let old_status = prop.status;
435
436    prop.votes.add_vote(vote, vote_power)?;
437    prop.update_status(&env.block)?;
438    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
439    let new_status = prop.status;
440    let change_hooks = proposal_status_changed_hooks(
441        PROPOSAL_HOOKS,
442        deps.storage,
443        proposal_id,
444        old_status.to_string(),
445        new_status.to_string(),
446    )?;
447    let vote_hooks = new_vote_hooks(
448        VOTE_HOOKS,
449        deps.storage,
450        proposal_id,
451        sender.to_string(),
452        vote.to_string(),
453    )?;
454    Ok(Response::default()
455        .add_submessages(change_hooks)
456        .add_submessages(vote_hooks)
457        .add_attribute("action", "vote")
458        .add_attribute("sender", sender)
459        .add_attribute("proposal_id", proposal_id.to_string())
460        .add_attribute("position", vote.to_string())
461        .add_attribute(
462            "rationale",
463            rationale.unwrap_or_else(|| "_none".to_string()),
464        )
465        .add_attribute("status", prop.status.to_string()))
466}
467
468pub fn execute_execute(
469    deps: DepsMut,
470    env: Env,
471    info: MessageInfo,
472    proposal_id: u64,
473) -> Result<Response, ContractError> {
474    let mut prop = PROPOSALS
475        .may_load(deps.storage, proposal_id)?
476        .ok_or(ContractError::NoSuchProposal { id: proposal_id })?;
477
478    let config = CONFIG.load(deps.storage)?;
479
480    // determine if this sender can execute
481    let mut sender_can_execute = true;
482    if config.only_members_execute {
483        let power = get_voting_power(
484            deps.as_ref(),
485            info.sender.clone(),
486            &config.dao,
487            Some(prop.start_height),
488        )?;
489
490        sender_can_execute = !power.is_zero();
491    }
492
493    // Check here that the proposal is passed or timelocked.
494    // Allow it to be executed even if it is expired so long
495    // as it passed during its voting period. Allow it to be
496    // executed in timelock state if early_execute is enabled
497    // and the sender is the vetoer.
498    prop.update_status(&env.block)?;
499    let old_status = prop.status;
500    match &prop.status {
501        Status::Passed => {
502            // if passed, verify sender can execute
503            if !sender_can_execute {
504                return Err(ContractError::Unauthorized {});
505            }
506        }
507        Status::VetoTimelock { .. } => {
508            let veto_config = prop
509                .veto
510                .as_ref()
511                .ok_or(VetoError::NoVetoConfiguration {})?;
512
513            // check that the sender is the vetoer
514            if veto_config.vetoer != info.sender {
515                // if the sender can normally execute, but is not the vetoer,
516                // return timelocked error. otherwise return unauthorized.
517                if sender_can_execute {
518                    return Err(ContractError::VetoError(VetoError::Timelocked {}));
519                } else {
520                    return Err(ContractError::Unauthorized {});
521                }
522            }
523
524            // if veto timelocked, only allow execution if early_execute enabled
525            veto_config.check_early_execute_enabled()?;
526        }
527        _ => {
528            return Err(ContractError::NotPassed {});
529        }
530    }
531
532    prop.status = Status::Executed;
533
534    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
535
536    let vote_result = prop.calculate_vote_result()?;
537    match vote_result {
538        VoteResult::Tie => Err(ContractError::Tie {}), // We don't anticipate this case as the proposal would not be in passed state, checked above.
539        VoteResult::SingleWinner(winning_choice) => {
540            let response = if !winning_choice.msgs.is_empty() {
541                let execute_message = WasmMsg::Execute {
542                    contract_addr: config.dao.to_string(),
543                    msg: to_json_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook {
544                        msgs: winning_choice.msgs,
545                    })?,
546                    funds: vec![],
547                };
548                match config.close_proposal_on_execution_failure {
549                    true => {
550                        let masked_proposal_id = mask_proposal_execution_proposal_id(proposal_id);
551                        Response::default().add_submessage(SubMsg::reply_on_error(
552                            execute_message,
553                            masked_proposal_id,
554                        ))
555                    }
556                    false => Response::default().add_message(execute_message),
557                }
558            } else {
559                Response::default()
560            };
561
562            let proposal_status_changed_hooks = proposal_status_changed_hooks(
563                PROPOSAL_HOOKS,
564                deps.storage,
565                proposal_id,
566                old_status.to_string(),
567                prop.status.to_string(),
568            )?;
569
570            // Add prepropose / deposit module hook which will handle deposit refunds.
571            let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?;
572            let proposal_completed_hooks =
573                proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?;
574
575            Ok(response
576                .add_submessages(proposal_status_changed_hooks)
577                .add_submessages(proposal_completed_hooks)
578                .add_attribute("action", "execute")
579                .add_attribute("sender", info.sender)
580                .add_attribute("proposal_id", proposal_id.to_string())
581                .add_attribute("dao", config.dao))
582        }
583    }
584}
585
586pub fn execute_close(
587    deps: DepsMut,
588    env: Env,
589    info: MessageInfo,
590    proposal_id: u64,
591) -> Result<Response<Empty>, ContractError> {
592    let mut prop = PROPOSALS.load(deps.storage, proposal_id)?;
593
594    prop.update_status(&env.block)?;
595    if prop.status != Status::Rejected {
596        return Err(ContractError::WrongCloseStatus {});
597    }
598
599    let old_status = prop.status;
600
601    prop.status = Status::Closed;
602
603    PROPOSALS.save(deps.storage, proposal_id, &prop)?;
604
605    let proposal_status_changed_hooks = proposal_status_changed_hooks(
606        PROPOSAL_HOOKS,
607        deps.storage,
608        proposal_id,
609        old_status.to_string(),
610        prop.status.to_string(),
611    )?;
612
613    // Add prepropose / deposit module hook which will handle deposit refunds.
614    let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?;
615    let proposal_completed_hooks =
616        proposal_completed_hooks(proposal_creation_policy, proposal_id, prop.status)?;
617
618    Ok(Response::default()
619        .add_submessages(proposal_status_changed_hooks)
620        .add_submessages(proposal_completed_hooks)
621        .add_attribute("action", "close")
622        .add_attribute("sender", info.sender)
623        .add_attribute("proposal_id", proposal_id.to_string()))
624}
625
626#[allow(clippy::too_many_arguments)]
627pub fn execute_update_config(
628    deps: DepsMut,
629    info: MessageInfo,
630    voting_strategy: VotingStrategy,
631    min_voting_period: Option<Duration>,
632    max_voting_period: Duration,
633    only_members_execute: bool,
634    allow_revoting: bool,
635    dao: String,
636    close_proposal_on_execution_failure: bool,
637    veto: Option<VetoConfig>,
638) -> Result<Response, ContractError> {
639    let config = CONFIG.load(deps.storage)?;
640
641    // Only the DAO may call this method.
642    if info.sender != config.dao {
643        return Err(ContractError::Unauthorized {});
644    }
645
646    voting_strategy.validate()?;
647
648    let dao = deps.api.addr_validate(&dao)?;
649
650    let (min_voting_period, max_voting_period) =
651        validate_voting_period(min_voting_period, max_voting_period)?;
652
653    // if veto is configured, validate its fields
654    if let Some(veto_config) = &veto {
655        veto_config.validate(&deps.as_ref(), &max_voting_period)?;
656    };
657
658    CONFIG.save(
659        deps.storage,
660        &Config {
661            voting_strategy,
662            min_voting_period,
663            max_voting_period,
664            only_members_execute,
665            allow_revoting,
666            dao,
667            close_proposal_on_execution_failure,
668            veto,
669        },
670    )?;
671
672    Ok(Response::default()
673        .add_attribute("action", "update_config")
674        .add_attribute("sender", info.sender))
675}
676
677pub fn execute_update_proposal_creation_policy(
678    deps: DepsMut,
679    info: MessageInfo,
680    new_info: PreProposeInfo,
681) -> Result<Response, ContractError> {
682    let config = CONFIG.load(deps.storage)?;
683    if config.dao != info.sender {
684        return Err(ContractError::Unauthorized {});
685    }
686
687    let (initial_policy, messages) = new_info.into_initial_policy_and_messages(config.dao)?;
688    CREATION_POLICY.save(deps.storage, &initial_policy)?;
689
690    Ok(Response::default()
691        .add_submessages(messages)
692        .add_attribute("action", "update_proposal_creation_policy")
693        .add_attribute("sender", info.sender)
694        .add_attribute("new_policy", format!("{initial_policy:?}")))
695}
696
697pub fn execute_update_rationale(
698    deps: DepsMut,
699    info: MessageInfo,
700    proposal_id: u64,
701    rationale: Option<String>,
702) -> Result<Response, ContractError> {
703    BALLOTS.update(
704        deps.storage,
705        // info.sender can't be forged so we implicitly access control
706        // with the key.
707        (proposal_id, &info.sender),
708        |ballot| match ballot {
709            Some(ballot) => Ok(Ballot {
710                rationale: rationale.clone(),
711                ..ballot
712            }),
713            None => Err(ContractError::NoSuchVote {
714                id: proposal_id,
715                voter: info.sender.to_string(),
716            }),
717        },
718    )?;
719
720    Ok(Response::default()
721        .add_attribute("action", "update_rationale")
722        .add_attribute("sender", info.sender)
723        .add_attribute("proposal_id", proposal_id.to_string())
724        .add_attribute("rationale", rationale.as_deref().unwrap_or("none")))
725}
726
727pub fn execute_add_proposal_hook(
728    deps: DepsMut,
729    _env: Env,
730    info: MessageInfo,
731    address: String,
732) -> Result<Response, ContractError> {
733    let config = CONFIG.load(deps.storage)?;
734    if config.dao != info.sender {
735        // Only DAO can add hooks
736        return Err(ContractError::Unauthorized {});
737    }
738
739    let validated_address = deps.api.addr_validate(&address)?;
740
741    add_hook(PROPOSAL_HOOKS, deps.storage, validated_address)?;
742
743    Ok(Response::default()
744        .add_attribute("action", "add_proposal_hook")
745        .add_attribute("address", address))
746}
747
748pub fn execute_remove_proposal_hook(
749    deps: DepsMut,
750    _env: Env,
751    info: MessageInfo,
752    address: String,
753) -> Result<Response, ContractError> {
754    let config = CONFIG.load(deps.storage)?;
755    if config.dao != info.sender {
756        // Only DAO can remove hooks
757        return Err(ContractError::Unauthorized {});
758    }
759
760    let validated_address = deps.api.addr_validate(&address)?;
761
762    remove_hook(PROPOSAL_HOOKS, deps.storage, validated_address)?;
763
764    Ok(Response::default()
765        .add_attribute("action", "remove_proposal_hook")
766        .add_attribute("address", address))
767}
768
769pub fn execute_add_vote_hook(
770    deps: DepsMut,
771    _env: Env,
772    info: MessageInfo,
773    address: String,
774) -> Result<Response, ContractError> {
775    let config = CONFIG.load(deps.storage)?;
776    if config.dao != info.sender {
777        // Only DAO can add hooks
778        return Err(ContractError::Unauthorized {});
779    }
780
781    let validated_address = deps.api.addr_validate(&address)?;
782
783    add_hook(VOTE_HOOKS, deps.storage, validated_address)?;
784
785    Ok(Response::default()
786        .add_attribute("action", "add_vote_hook")
787        .add_attribute("address", address))
788}
789
790pub fn execute_remove_vote_hook(
791    deps: DepsMut,
792    _env: Env,
793    info: MessageInfo,
794    address: String,
795) -> Result<Response, ContractError> {
796    let config = CONFIG.load(deps.storage)?;
797    if config.dao != info.sender {
798        // Only DAO can remove hooks
799        return Err(ContractError::Unauthorized {});
800    }
801
802    let validated_address = deps.api.addr_validate(&address)?;
803
804    remove_hook(VOTE_HOOKS, deps.storage, validated_address)?;
805
806    Ok(Response::default()
807        .add_attribute("action", "remove_vote_hook")
808        .add_attribute("address", address))
809}
810
811pub fn add_hook(
812    hooks: Hooks,
813    storage: &mut dyn Storage,
814    validated_address: Addr,
815) -> Result<(), ContractError> {
816    hooks
817        .add_hook(storage, validated_address)
818        .map_err(ContractError::HookError)?;
819    Ok(())
820}
821
822pub fn remove_hook(
823    hooks: Hooks,
824    storage: &mut dyn Storage,
825    validate_address: Addr,
826) -> Result<(), ContractError> {
827    hooks
828        .remove_hook(storage, validate_address)
829        .map_err(ContractError::HookError)?;
830    Ok(())
831}
832
833pub fn next_proposal_id(store: &dyn Storage) -> StdResult<u64> {
834    Ok(PROPOSAL_COUNT.may_load(store)?.unwrap_or_default() + 1)
835}
836
837pub fn advance_proposal_id(store: &mut dyn Storage) -> StdResult<u64> {
838    let id: u64 = next_proposal_id(store)?;
839    PROPOSAL_COUNT.save(store, &id)?;
840    Ok(id)
841}
842
843#[cfg_attr(not(feature = "library"), entry_point)]
844pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
845    match msg {
846        QueryMsg::Config {} => query_config(deps),
847        QueryMsg::Proposal { proposal_id } => query_proposal(deps, env, proposal_id),
848        QueryMsg::ListProposals { start_after, limit } => {
849            query_list_proposals(deps, env, start_after, limit)
850        }
851        QueryMsg::NextProposalId {} => query_next_proposal_id(deps),
852        QueryMsg::ProposalCount {} => query_proposal_count(deps),
853        QueryMsg::GetVote { proposal_id, voter } => query_vote(deps, proposal_id, voter),
854        QueryMsg::ListVotes {
855            proposal_id,
856            start_after,
857            limit,
858        } => query_list_votes(deps, proposal_id, start_after, limit),
859        QueryMsg::Info {} => query_info(deps),
860        QueryMsg::ReverseProposals {
861            start_before,
862            limit,
863        } => query_reverse_proposals(deps, env, start_before, limit),
864        QueryMsg::ProposalCreationPolicy {} => query_creation_policy(deps),
865        QueryMsg::ProposalHooks {} => to_json_binary(&PROPOSAL_HOOKS.query_hooks(deps)?),
866        QueryMsg::VoteHooks {} => to_json_binary(&VOTE_HOOKS.query_hooks(deps)?),
867        QueryMsg::Dao {} => query_dao(deps),
868    }
869}
870
871pub fn query_config(deps: Deps) -> StdResult<Binary> {
872    let config = CONFIG.load(deps.storage)?;
873    to_json_binary(&config)
874}
875
876pub fn query_dao(deps: Deps) -> StdResult<Binary> {
877    let config = CONFIG.load(deps.storage)?;
878    to_json_binary(&config.dao)
879}
880
881pub fn query_proposal(deps: Deps, env: Env, id: u64) -> StdResult<Binary> {
882    let proposal = PROPOSALS.load(deps.storage, id)?;
883    to_json_binary(&proposal.into_response(&env.block, id)?)
884}
885
886pub fn query_creation_policy(deps: Deps) -> StdResult<Binary> {
887    let policy = CREATION_POLICY.load(deps.storage)?;
888    to_json_binary(&policy)
889}
890
891pub fn query_list_proposals(
892    deps: Deps,
893    env: Env,
894    start_after: Option<u64>,
895    limit: Option<u64>,
896) -> StdResult<Binary> {
897    let min = start_after.map(Bound::exclusive);
898    let limit = limit.unwrap_or(DEFAULT_LIMIT);
899    let props: Vec<ProposalResponse> = PROPOSALS
900        .range(deps.storage, min, None, cosmwasm_std::Order::Ascending)
901        .take(limit as usize)
902        .collect::<Result<Vec<(u64, MultipleChoiceProposal)>, _>>()?
903        .into_iter()
904        .map(|(id, proposal)| proposal.into_response(&env.block, id))
905        .collect::<StdResult<Vec<ProposalResponse>>>()?;
906
907    to_json_binary(&ProposalListResponse { proposals: props })
908}
909
910pub fn query_reverse_proposals(
911    deps: Deps,
912    env: Env,
913    start_before: Option<u64>,
914    limit: Option<u64>,
915) -> StdResult<Binary> {
916    let limit = limit.unwrap_or(DEFAULT_LIMIT);
917    let max = start_before.map(Bound::exclusive);
918    let props: Vec<ProposalResponse> = PROPOSALS
919        .range(deps.storage, None, max, cosmwasm_std::Order::Descending)
920        .take(limit as usize)
921        .collect::<Result<Vec<(u64, MultipleChoiceProposal)>, _>>()?
922        .into_iter()
923        .map(|(id, proposal)| proposal.into_response(&env.block, id))
924        .collect::<StdResult<Vec<ProposalResponse>>>()?;
925
926    to_json_binary(&ProposalListResponse { proposals: props })
927}
928
929pub fn query_next_proposal_id(deps: Deps) -> StdResult<Binary> {
930    to_json_binary(&next_proposal_id(deps.storage)?)
931}
932
933pub fn query_proposal_count(deps: Deps) -> StdResult<Binary> {
934    let proposal_count = PROPOSAL_COUNT.load(deps.storage)?;
935    to_json_binary(&proposal_count)
936}
937
938pub fn query_vote(deps: Deps, proposal_id: u64, voter: String) -> StdResult<Binary> {
939    let voter = deps.api.addr_validate(&voter)?;
940    let ballot = BALLOTS.may_load(deps.storage, (proposal_id, &voter))?;
941    let vote = ballot.map(|ballot| VoteInfo {
942        voter,
943        vote: ballot.vote,
944        power: ballot.power,
945        rationale: ballot.rationale,
946    });
947    to_json_binary(&VoteResponse { vote })
948}
949
950pub fn query_list_votes(
951    deps: Deps,
952    proposal_id: u64,
953    start_after: Option<String>,
954    limit: Option<u64>,
955) -> StdResult<Binary> {
956    let limit = limit.unwrap_or(DEFAULT_LIMIT);
957    let start_after = start_after
958        .map(|addr| deps.api.addr_validate(&addr))
959        .transpose()?;
960    let min = start_after.as_ref().map(Bound::<&Addr>::exclusive);
961
962    let votes = BALLOTS
963        .prefix(proposal_id)
964        .range(deps.storage, min, None, Order::Ascending)
965        .take(limit as usize)
966        .map(|item| {
967            let (voter, ballot) = item?;
968            Ok(VoteInfo {
969                voter,
970                vote: ballot.vote,
971                power: ballot.power,
972                rationale: ballot.rationale,
973            })
974        })
975        .collect::<StdResult<Vec<_>>>()?;
976
977    to_json_binary(&VoteListResponse { votes })
978}
979
980pub fn query_info(deps: Deps) -> StdResult<Binary> {
981    let info = cw2::get_contract_version(deps.storage)?;
982    to_json_binary(&dao_interface::voting::InfoResponse { info })
983}
984
985#[cfg_attr(not(feature = "library"), entry_point)]
986pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
987    let repl = TaggedReplyId::new(msg.id)?;
988    match repl {
989        TaggedReplyId::FailedProposalExecution(proposal_id) => {
990            PROPOSALS.update(deps.storage, proposal_id, |prop| match prop {
991                Some(mut prop) => {
992                    prop.status = Status::ExecutionFailed;
993                    Ok(prop)
994                }
995                None => Err(ContractError::NoSuchProposal { id: proposal_id }),
996            })?;
997
998            Ok(Response::new()
999                .add_attribute("proposal execution failed", proposal_id.to_string())
1000                .add_attribute(
1001                    "error",
1002                    msg.result.into_result().err().unwrap_or("None".to_string()),
1003                ))
1004        }
1005        TaggedReplyId::FailedProposalHook(idx) => {
1006            let addr = PROPOSAL_HOOKS.remove_hook_by_index(deps.storage, idx)?;
1007            Ok(Response::new().add_attribute("removed_proposal_hook", format!("{addr}:{idx}")))
1008        }
1009        TaggedReplyId::FailedVoteHook(idx) => {
1010            let addr = VOTE_HOOKS.remove_hook_by_index(deps.storage, idx)?;
1011            Ok(Response::new().add_attribute("removed vote hook", format!("{addr}:{idx}")))
1012        }
1013        TaggedReplyId::PreProposeModuleInstantiation => {
1014            let res = parse_reply_instantiate_data(msg)?;
1015            let module = deps.api.addr_validate(&res.contract_address)?;
1016            CREATION_POLICY.save(
1017                deps.storage,
1018                &ProposalCreationPolicy::Module { addr: module },
1019            )?;
1020
1021            match res.data {
1022                Some(data) => Ok(Response::new()
1023                    .add_attribute("update_pre_propose_module", res.contract_address)
1024                    .set_data(data)),
1025                None => Ok(Response::new()
1026                    .add_attribute("update_pre_propose_module", res.contract_address)),
1027            }
1028        }
1029        TaggedReplyId::FailedPreProposeModuleHook => {
1030            let addr = match CREATION_POLICY.load(deps.storage)? {
1031                ProposalCreationPolicy::Anyone {} => {
1032                    // Something is off if we're getting this
1033                    // reply and we don't have a pre-propose
1034                    // module installed. This should be
1035                    // unreachable.
1036                    return Err(ContractError::InvalidReplyID {
1037                        id: failed_pre_propose_module_hook_id(),
1038                    });
1039                }
1040                ProposalCreationPolicy::Module { addr } => {
1041                    // If we are here, our pre-propose module has
1042                    // errored while receiving a proposal
1043                    // hook. Rest in peace pre-propose module.
1044                    CREATION_POLICY.save(deps.storage, &ProposalCreationPolicy::Anyone {})?;
1045                    addr
1046                }
1047            };
1048            Ok(Response::new().add_attribute("failed_prepropose_hook", format!("{addr}")))
1049        }
1050    }
1051}
1052
1053#[cfg_attr(not(feature = "library"), entry_point)]
1054pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
1055    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
1056    Ok(Response::default())
1057}