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 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 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 if !proposal_creation_policy.is_permitted(&info.sender) {
166 return Err(ContractError::Unauthorized {});
167 }
168
169 let proposer = match (proposer, &proposal_creation_policy) {
173 (None, ProposalCreationPolicy::Anyone {}) => info.sender.clone(),
174 (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 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 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 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 proposal.update_status(&env.block)?;
224 proposal
225 };
226 let id = advance_proposal_id(deps.storage)?;
227
228 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 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 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 veto_config.check_is_vetoer(&info)?;
314
315 match prop.status {
316 Status::Open => {
317 veto_config.check_veto_before_passed_enabled()?;
319 }
320 Status::Passed => {
321 return Err(ContractError::VetoError(VetoError::TimelockExpired {}));
324 }
325 Status::VetoTimelock { expiration } => {
326 if expiration.is_expired(&env.block) {
330 return Err(ContractError::VetoError(VetoError::TimelockExpired {}));
331 }
332 }
333 _ => {
335 return Err(ContractError::VetoError(VetoError::InvalidProposalStatus {
336 status: prop.status.to_string(),
337 }));
338 }
339 }
340
341 prop.status = Status::Vetoed;
343 PROPOSALS.save(deps.storage, proposal_id, &prop)?;
344
345 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 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 if vote.option_id as usize >= prop.choices.len() {
381 return Err(ContractError::InvalidVote {});
382 }
383
384 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 Err(ContractError::AlreadyCast {})
413 } else {
414 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 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 prop.update_status(&env.block)?;
499 let old_status = prop.status;
500 match &prop.status {
501 Status::Passed => {
502 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 if veto_config.vetoer != info.sender {
515 if sender_can_execute {
518 return Err(ContractError::VetoError(VetoError::Timelocked {}));
519 } else {
520 return Err(ContractError::Unauthorized {});
521 }
522 }
523
524 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 {}), 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 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 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 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 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 (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 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 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 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 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 return Err(ContractError::InvalidReplyID {
1037 id: failed_pre_propose_module_hook_id(),
1038 });
1039 }
1040 ProposalCreationPolicy::Module { addr } => {
1041 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}