dao_voting/pre_propose.rs
1//! Types related to the pre-propose module. Motivation:
2//! <https://github.com/DA0-DA0/dao-contracts/discussions/462>.
3
4use cosmwasm_schema::cw_serde;
5use cosmwasm_std::{Addr, Empty, StdResult, SubMsg};
6use dao_interface::state::ModuleInstantiateInfo;
7use thiserror::Error;
8
9use crate::reply::pre_propose_module_instantiation_id;
10
11#[cw_serde]
12pub enum PreProposeInfo {
13 /// Anyone may create a proposal free of charge.
14 AnyoneMayPropose {},
15 /// The module specified in INFO has exclusive rights to proposal
16 /// creation.
17 ModuleMayPropose { info: ModuleInstantiateInfo },
18}
19
20/// The policy configured in a proposal module that determines whether or not a
21/// pre-propose module is in use. If so, only the module can create new
22/// proposals. Otherwise, there is no restriction on proposal creation.
23#[cw_serde]
24pub enum ProposalCreationPolicy {
25 /// Anyone may create a proposal, free of charge.
26 Anyone {},
27 /// Only ADDR may create proposals. It is expected that ADDR is a
28 /// pre-propose module, though we only require that it is a valid
29 /// address.
30 Module { addr: Addr },
31}
32
33impl ProposalCreationPolicy {
34 /// Determines if CREATOR is permitted to create a
35 /// proposal. Returns true if so and false otherwise.
36 pub fn is_permitted(&self, creator: &Addr) -> bool {
37 match self {
38 Self::Anyone {} => true,
39 Self::Module { addr } => creator == addr,
40 }
41 }
42}
43
44impl PreProposeInfo {
45 pub fn into_initial_policy_and_messages(
46 self,
47 dao: Addr,
48 ) -> StdResult<(ProposalCreationPolicy, Vec<SubMsg<Empty>>)> {
49 Ok(match self {
50 Self::AnyoneMayPropose {} => (ProposalCreationPolicy::Anyone {}, vec![]),
51 Self::ModuleMayPropose { info } => (
52 // Anyone can propose will be set until instantiation succeeds, then
53 // `ModuleMayPropose` will be set. This ensures that we fail open
54 // upon instantiation failure.
55 ProposalCreationPolicy::Anyone {},
56 vec![SubMsg::reply_on_success(
57 info.into_wasm_msg(dao),
58 pre_propose_module_instantiation_id(),
59 )],
60 ),
61 })
62 }
63}
64
65/// The policy configured in a pre-propose module that determines who can submit
66/// proposals. This is the preferred way to restrict proposal creation (as
67/// opposed to the ProposalCreationPolicy above) since pre-propose modules
68/// support other features, such as proposal deposits.
69#[cw_serde]
70pub enum PreProposeSubmissionPolicy {
71 /// Anyone may create proposals, except for those in the denylist.
72 Anyone {
73 /// Addresses that may not create proposals.
74 denylist: Vec<Addr>,
75 },
76 /// Specific people may create proposals.
77 Specific {
78 /// Whether or not DAO members may create proposals.
79 dao_members: bool,
80 /// Addresses that may create proposals.
81 allowlist: Vec<Addr>,
82 /// Addresses that may not create proposals, overriding other settings.
83 denylist: Vec<Addr>,
84 },
85}
86
87#[derive(Error, Debug, PartialEq, Eq)]
88pub enum PreProposeSubmissionPolicyError {
89 #[error("The proposal submission policy doesn't allow anyone to submit proposals")]
90 NoOneAllowed {},
91
92 #[error("Denylist cannot contain addresses in the allowlist")]
93 DenylistAllowlistOverlap {},
94
95 #[error("You are not allowed to submit proposals")]
96 Unauthorized {},
97
98 #[error("The current proposal submission policy (Anyone) only supports a denylist. Change the policy to Specific in order to configure more granular permissions.")]
99 AnyoneInvalidUpdateFields {},
100}
101
102impl PreProposeSubmissionPolicy {
103 /// Validate the policy configuration.
104 pub fn validate(&self) -> Result<(), PreProposeSubmissionPolicyError> {
105 if let PreProposeSubmissionPolicy::Specific {
106 dao_members,
107 allowlist,
108 denylist,
109 } = self
110 {
111 // prevent allowlist and denylist from overlapping
112 if denylist.iter().any(|a| allowlist.iter().any(|b| a == b)) {
113 return Err(PreProposeSubmissionPolicyError::DenylistAllowlistOverlap {});
114 }
115
116 // ensure someone is allowed to submit proposals, be it DAO members
117 // or someone on the allowlist. we can't verify that the denylist
118 // doesn't contain all DAO members, so this is the best we can do to
119 // ensure that someone is allowed to submit.
120 if !dao_members && allowlist.is_empty() {
121 return Err(PreProposeSubmissionPolicyError::NoOneAllowed {});
122 }
123 }
124
125 Ok(())
126 }
127
128 /// Human readable string for use in events.
129 pub fn human_readable(&self) -> String {
130 match self {
131 Self::Anyone { .. } => "anyone".to_string(),
132 Self::Specific { .. } => "specific".to_string(),
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use cosmwasm_std::{to_json_binary, WasmMsg};
140
141 use super::*;
142
143 #[test]
144 fn test_anyone_is_permitted() {
145 let policy = ProposalCreationPolicy::Anyone {};
146
147 // I'll actually stand by this as a legit testing strategy
148 // when looking at string inputs. If anything is going to
149 // screw things up, its weird unicode characters.
150 //
151 // For example, my langauge server explodes for me if I use
152 // the granddaddy of weird unicode characters, the large
153 // family: ๐ฉโ๐ฉโ๐งโ๐ฆ.
154 //
155 // The family emoji you see is actually a combination of
156 // individual person emojis. You can browse the whole
157 // collection of combo emojis here:
158 // <https://unicode.org/emoji/charts/emoji-zwj-sequences.html>.
159 //
160 // You may also enjoy this PDF wherein there is a discussion
161 // about the feesability of supporting all 7230 possible
162 // combos of family emojis:
163 // <https://www.unicode.org/L2/L2020/20114-family-emoji-explor.pdf>.
164 for c in '๐'..'๐คฃ' {
165 assert!(policy.is_permitted(&Addr::unchecked(c.to_string())))
166 }
167 }
168
169 #[test]
170 fn test_module_is_permitted() {
171 let policy = ProposalCreationPolicy::Module {
172 addr: Addr::unchecked("deposit_module"),
173 };
174 assert!(!policy.is_permitted(&Addr::unchecked("๐ฉโ๐ฉโ๐งโ๐ฆ")));
175 assert!(policy.is_permitted(&Addr::unchecked("deposit_module")));
176 }
177
178 #[test]
179 fn test_pre_any_conversion() {
180 let info = PreProposeInfo::AnyoneMayPropose {};
181 let (policy, messages) = info
182 .into_initial_policy_and_messages(Addr::unchecked("๐"))
183 .unwrap();
184 assert_eq!(policy, ProposalCreationPolicy::Anyone {});
185 assert!(messages.is_empty())
186 }
187
188 #[test]
189 fn test_pre_module_conversion() {
190 let info = PreProposeInfo::ModuleMayPropose {
191 info: ModuleInstantiateInfo {
192 code_id: 42,
193 msg: to_json_binary("foo").unwrap(),
194 admin: None,
195 funds: vec![],
196 label: "pre-propose-9000".to_string(),
197 },
198 };
199 let (policy, messages) = info
200 .into_initial_policy_and_messages(Addr::unchecked("๐ฅต"))
201 .unwrap();
202
203 // In this case the package is expected to allow anyone to
204 // create a proposal (fail-open), and provide some messages
205 // that, when handled in a `reply` handler will set the
206 // creation policy to a specific module.
207 assert_eq!(policy, ProposalCreationPolicy::Anyone {});
208 assert_eq!(messages.len(), 1);
209 assert_eq!(
210 messages[0],
211 SubMsg::reply_on_success(
212 WasmMsg::Instantiate {
213 admin: None,
214 code_id: 42,
215 msg: to_json_binary("foo").unwrap(),
216 funds: vec![],
217 label: "pre-propose-9000".to_string()
218 },
219 crate::reply::pre_propose_module_instantiation_id()
220 )
221 )
222 }
223}