dao_proposal_single/
contract.rs

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