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}