1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! Types related to the pre-propose module. Motivation:
//! <https://github.com/DA0-DA0/dao-contracts/discussions/462>.

use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Empty, StdResult, SubMsg};
use dao_interface::state::ModuleInstantiateInfo;

use crate::reply::pre_propose_module_instantiation_id;

#[cw_serde]
pub enum PreProposeInfo {
    /// Anyone may create a proposal free of charge.
    AnyoneMayPropose {},
    /// The module specified in INFO has exclusive rights to proposal
    /// creation.
    ModuleMayPropose { info: ModuleInstantiateInfo },
}

#[cw_serde]
pub enum ProposalCreationPolicy {
    /// Anyone may create a proposal, free of charge.
    Anyone {},
    /// Only ADDR may create proposals. It is expected that ADDR is a
    /// pre-propose module, though we only require that it is a valid
    /// address.
    Module { addr: Addr },
}

impl ProposalCreationPolicy {
    /// Determines if CREATOR is permitted to create a
    /// proposal. Returns true if so and false otherwise.
    pub fn is_permitted(&self, creator: &Addr) -> bool {
        match self {
            Self::Anyone {} => true,
            Self::Module { addr } => creator == addr,
        }
    }
}

impl PreProposeInfo {
    pub fn into_initial_policy_and_messages(
        self,
        dao: Addr,
    ) -> StdResult<(ProposalCreationPolicy, Vec<SubMsg<Empty>>)> {
        Ok(match self {
            Self::AnyoneMayPropose {} => (ProposalCreationPolicy::Anyone {}, vec![]),
            Self::ModuleMayPropose { info } => (
                // Anyone can propose will be set until instantiation succeeds, then
                // `ModuleMayPropose` will be set. This ensures that we fail open
                // upon instantiation failure.
                ProposalCreationPolicy::Anyone {},
                vec![SubMsg::reply_on_success(
                    info.into_wasm_msg(dao),
                    pre_propose_module_instantiation_id(),
                )],
            ),
        })
    }
}

#[cfg(test)]
mod tests {
    use cosmwasm_std::{to_binary, WasmMsg};

    use super::*;

    #[test]
    fn test_anyone_is_permitted() {
        let policy = ProposalCreationPolicy::Anyone {};

        // I'll actually stand by this as a legit testing strategy
        // when looking at string inputs. If anything is going to
        // screw things up, its weird unicode characters.
        //
        // For example, my langauge server explodes for me if I use
        // the granddaddy of weird unicode characters, the large
        // family: 👩‍👩‍👧‍👦.
        //
        // The family emoji you see is actually a combination of
        // individual person emojis. You can browse the whole
        // collection of combo emojis here:
        // <https://unicode.org/emoji/charts/emoji-zwj-sequences.html>.
        //
        // You may also enjoy this PDF wherein there is a discussion
        // about the feesability of supporting all 7230 possible
        // combos of family emojis:
        // <https://www.unicode.org/L2/L2020/20114-family-emoji-explor.pdf>.
        for c in '😀'..'🤣' {
            assert!(policy.is_permitted(&Addr::unchecked(c.to_string())))
        }
    }

    #[test]
    fn test_module_is_permitted() {
        let policy = ProposalCreationPolicy::Module {
            addr: Addr::unchecked("deposit_module"),
        };
        assert!(!policy.is_permitted(&Addr::unchecked("👩‍👩‍👧‍👦")));
        assert!(policy.is_permitted(&Addr::unchecked("deposit_module")));
    }

    #[test]
    fn test_pre_any_conversion() {
        let info = PreProposeInfo::AnyoneMayPropose {};
        let (policy, messages) = info
            .into_initial_policy_and_messages(Addr::unchecked("😃"))
            .unwrap();
        assert_eq!(policy, ProposalCreationPolicy::Anyone {});
        assert!(messages.is_empty())
    }

    #[test]
    fn test_pre_module_conversion() {
        let info = PreProposeInfo::ModuleMayPropose {
            info: ModuleInstantiateInfo {
                code_id: 42,
                msg: to_binary("foo").unwrap(),
                admin: None,
                label: "pre-propose-9000".to_string(),
            },
        };
        let (policy, messages) = info
            .into_initial_policy_and_messages(Addr::unchecked("🥵"))
            .unwrap();

        // In this case the package is expected to allow anyone to
        // create a proposal (fail-open), and provide some messages
        // that, when handled in a `reply` handler will set the
        // creation policy to a specific module.
        assert_eq!(policy, ProposalCreationPolicy::Anyone {});
        assert_eq!(messages.len(), 1);
        assert_eq!(
            messages[0],
            SubMsg::reply_on_success(
                WasmMsg::Instantiate {
                    admin: None,
                    code_id: 42,
                    msg: to_binary("foo").unwrap(),
                    funds: vec![],
                    label: "pre-propose-9000".to_string()
                },
                crate::reply::pre_propose_module_instantiation_id()
            )
        )
    }
}