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 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 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 if !proposal_creation_policy.is_permitted(&sender) {
169 return Err(ContractError::Unauthorized {});
170 }
171
172 let proposer = match (proposer, &proposal_creation_policy) {
176 (None, ProposalCreationPolicy::Anyone {}) => sender.clone(),
177 (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 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 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 proposal.update_status(&env.block)?;
225 proposal
226 };
227 let id = advance_proposal_id(deps.storage)?;
228
229 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 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 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 veto_config.check_is_vetoer(&info)?;
306
307 match prop.status {
308 Status::Open => {
309 veto_config.check_veto_before_passed_enabled()?;
311 }
312 Status::Passed => {
313 return Err(ContractError::VetoError(VetoError::TimelockExpired {}));
316 }
317 Status::VetoTimelock { expiration } => {
318 if expiration.is_expired(&env.block) {
322 return Err(ContractError::VetoError(VetoError::TimelockExpired {}));
323 }
324 }
325 _ => {
327 return Err(ContractError::VetoError(VetoError::InvalidProposalStatus {
328 status: prop.status.to_string(),
329 }));
330 }
331 }
332
333 prop.status = Status::Vetoed;
335 PROPOSALS.save(deps.storage, proposal_id, &prop)?;
336
337 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 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 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 prop.update_status(&env.block)?;
389 let old_status = prop.status;
390 match &prop.status {
391 Status::Passed => {
392 if !sender_can_execute {
394 return Err(ContractError::Unauthorized {});
395 }
396 }
397 Status::VetoTimelock { .. } => {
398 let veto_config = prop
400 .veto
401 .as_ref()
402 .ok_or(VetoError::NoVetoConfiguration {})?;
403
404 if veto_config.vetoer != info.sender {
406 if sender_can_execute {
409 return Err(ContractError::VetoError(VetoError::Timelocked {}));
410 } else {
411 return Err(ContractError::Unauthorized {});
412 }
413 }
414
415 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 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 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 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 Err(ContractError::AlreadyCast {})
514 } else {
515 prop.votes
517 .remove_vote(current_ballot.vote, current_ballot.power);
518 Ok(Ballot {
519 power: vote_power,
520 vote,
521 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 (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 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 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 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 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 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 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 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 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 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 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 let Some(veto_config) = &veto {
989 veto_config.validate(&deps.as_ref(), &max_voting_period)?;
990 };
991
992 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 let current_proposals = v1::state::PROPOSALS
1015 .range(deps.storage, None, None, Order::Ascending)
1016 .collect::<StdResult<Vec<(u64, v1::proposal::Proposal)>>>()?;
1017
1018 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 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 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 return Err(ContractError::InvalidReplyID {
1127 id: failed_pre_propose_module_hook_id(),
1128 });
1129 }
1130 ProposalCreationPolicy::Module { addr } => {
1131 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}