mars_core/
council.rs

1use cosmwasm_std::{Addr, CosmosMsg, Uint128};
2use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};
4
5use crate::error::MarsError;
6use crate::helpers::decimal_param_le_one;
7use crate::math::decimal::Decimal;
8
9use self::error::ContractError;
10
11pub const MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 50;
12pub const MAXIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE: u64 = 100;
13
14/// Council global configuration
15#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
16pub struct Config {
17    /// Address provider returns addresses for all protocol contracts
18    pub address_provider_address: Addr,
19    /// Blocks during which a proposal is active since being submitted
20    pub proposal_voting_period: u64,
21    /// Blocks that need to pass since a proposal succeeds in order for it to be available to be
22    /// executed
23    pub proposal_effective_delay: u64,
24    /// Blocks after the effective_delay during which a successful proposal can be activated before it expires
25    pub proposal_expiration_period: u64,
26    /// Number of Mars needed to make a proposal. Will be returned if successful. Will be
27    /// distributed between stakers if rejected.
28    pub proposal_required_deposit: Uint128,
29    /// % of total voting power required to participate in the proposal in order to consider it successfull
30    pub proposal_required_quorum: Decimal,
31    /// % of for votes required in order to consider the proposal successful
32    pub proposal_required_threshold: Decimal,
33}
34
35impl Config {
36    pub fn validate(&self) -> Result<(), ContractError> {
37        decimal_param_le_one(&self.proposal_required_quorum, "proposal_required_quorum")?;
38
39        let minimum_proposal_required_threshold =
40            Decimal::percent(MINIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE);
41        let maximum_proposal_required_threshold =
42            Decimal::percent(MAXIMUM_PROPOSAL_REQUIRED_THRESHOLD_PERCENTAGE);
43
44        if !(self.proposal_required_threshold >= minimum_proposal_required_threshold
45            && self.proposal_required_threshold <= maximum_proposal_required_threshold)
46        {
47            return Err(MarsError::InvalidParam {
48                param_name: "proposal_required_threshold".to_string(),
49                invalid_value: self.proposal_required_threshold.to_string(),
50                predicate: format!(
51                    ">= {} and <= {}",
52                    minimum_proposal_required_threshold, maximum_proposal_required_threshold
53                ),
54            }
55            .into());
56        }
57
58        Ok(())
59    }
60}
61
62/// Global state
63#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
64pub struct GlobalState {
65    /// Number of proposals
66    pub proposal_count: u64,
67}
68
69/// Proposal metadata stored in state
70#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
71pub struct Proposal {
72    pub proposal_id: u64,
73    /// Address submitting the proposal
74    pub submitter_address: Addr,
75    /// Wether the proposal is Active, Passed, Rejected or Executed
76    pub status: ProposalStatus,
77    /// Number of for votes
78    pub for_votes: Uint128,
79    /// Number of against votes
80    pub against_votes: Uint128,
81    /// Block at which voting for the porposal starts
82    pub start_height: u64,
83    /// Block at which voting for the porposal ends
84    pub end_height: u64,
85    /// Title for the proposal
86    pub title: String,
87    /// Description for the proposal
88    pub description: String,
89    /// Link provided for cases where the proposal description is too large or
90    /// some other external resource is intended to be associated with the proposal
91    pub link: Option<String>,
92    /// Set of messages available to get executed if the proposal passes
93    pub messages: Option<Vec<ProposalMessage>>,
94    /// MARS tokens deposited on the proposal submission. Will be returned to
95    /// submitter if proposal passes and sent to xMars stakers otherwise
96    pub deposit_amount: Uint128,
97}
98
99/// Execute call that will be executed by the DAO if the proposal succeeds
100#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
101pub struct ProposalMessage {
102    /// Determines order of execution lower order will be executed first
103    pub execution_order: u64,
104    /// CosmosMsg that will be executed by the council
105    pub msg: CosmosMsg,
106}
107
108/// Proposal Status
109#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
110#[serde(rename_all = "snake_case")]
111pub enum ProposalStatus {
112    /// Proposal is being voted on
113    Active,
114    /// Proposal has been approved but has not been executed yet
115    Passed,
116    /// Proposal was rejected
117    Rejected,
118    /// Proposal has been approved and executed
119    Executed,
120}
121
122/// Single vote made by an address
123#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
124pub struct ProposalVote {
125    /// For or Against the proposal
126    pub option: ProposalVoteOption,
127    /// Voting power
128    pub power: Uint128,
129}
130
131#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
132#[serde(rename_all = "snake_case")]
133pub enum ProposalVoteOption {
134    For,
135    Against,
136}
137
138impl std::fmt::Display for ProposalVoteOption {
139    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
140        let display_str = match self {
141            ProposalVoteOption::For => "for",
142            ProposalVoteOption::Against => "against",
143        };
144        write!(f, "{}", display_str)
145    }
146}
147
148#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
149pub struct ProposalsListResponse {
150    /// Total proposals submitted
151    pub proposal_count: u64,
152    /// List of proposals (paginated by query)
153    pub proposal_list: Vec<Proposal>,
154}
155
156#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
157pub struct ProposalVotesResponse {
158    pub proposal_id: u64,
159    pub votes: Vec<ProposalVoteResponse>,
160}
161
162#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
163pub struct ProposalVoteResponse {
164    pub voter_address: String,
165    pub option: ProposalVoteOption,
166    pub power: Uint128,
167}
168
169pub mod msg {
170    use cosmwasm_std::Uint128;
171    use cw20::Cw20ReceiveMsg;
172    use schemars::JsonSchema;
173    use serde::{Deserialize, Serialize};
174
175    use crate::math::decimal::Decimal;
176
177    use super::{ProposalMessage, ProposalVoteOption};
178
179    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
180    pub struct InstantiateMsg {
181        pub config: CreateOrUpdateConfig,
182    }
183
184    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, Default)]
185    pub struct CreateOrUpdateConfig {
186        pub address_provider_address: Option<String>,
187
188        pub proposal_voting_period: Option<u64>,
189        pub proposal_effective_delay: Option<u64>,
190        pub proposal_expiration_period: Option<u64>,
191        pub proposal_required_deposit: Option<Uint128>,
192        pub proposal_required_quorum: Option<Decimal>,
193        pub proposal_required_threshold: Option<Decimal>,
194    }
195
196    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
197    #[serde(rename_all = "snake_case")]
198    pub enum ExecuteMsg {
199        /// Implementation cw20 receive msg
200        Receive(Cw20ReceiveMsg),
201
202        /// Vote for a proposal
203        CastVote {
204            proposal_id: u64,
205            vote: ProposalVoteOption,
206        },
207
208        /// End proposal after voting period has passed
209        EndProposal { proposal_id: u64 },
210
211        /// Execute a successful proposal
212        ExecuteProposal { proposal_id: u64 },
213
214        /// Update config
215        UpdateConfig { config: CreateOrUpdateConfig },
216    }
217
218    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
219    #[serde(rename_all = "snake_case")]
220    pub enum ReceiveMsg {
221        /// Submit a proposal to be voted
222        /// Requires a Mars deposit equal or greater than the proposal_required_deposit
223        SubmitProposal {
224            title: String,
225            description: String,
226            link: Option<String>,
227            messages: Option<Vec<ProposalMessage>>,
228        },
229    }
230
231    #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
232    #[serde(rename_all = "snake_case")]
233    pub enum QueryMsg {
234        Config {},
235        Proposals {
236            start: Option<u64>,
237            limit: Option<u32>,
238        },
239        Proposal {
240            proposal_id: u64,
241        },
242        ProposalVotes {
243            proposal_id: u64,
244            start_after: Option<String>,
245            limit: Option<u32>,
246        },
247    }
248}
249
250pub mod error {
251    use cosmwasm_std::StdError;
252    use thiserror::Error;
253
254    use crate::error::MarsError;
255
256    #[derive(Error, Debug, PartialEq)]
257    pub enum ContractError {
258        #[error("{0}")]
259        Std(#[from] StdError),
260
261        #[error("{0}")]
262        Mars(#[from] MarsError),
263
264        #[error("Invalid Proposal: {error:?}")]
265        InvalidProposal { error: String },
266
267        #[error("Proposal is not active")]
268        ProposalNotActive {},
269
270        #[error("User has already voted on this proposal")]
271        VoteUserAlreadyVoted {},
272        #[error("User has no voting power at block: {block:?}")]
273        VoteNoVotingPower { block: u64 },
274        #[error("Voting period has ended")]
275        VoteVotingPeriodEnded {},
276
277        #[error("Voting period has not ended")]
278        EndProposalVotingPeriodNotEnded {},
279
280        #[error("Proposal has not passed or has already been executed")]
281        ExecuteProposalNotPassed {},
282        #[error("Proposal must end it's delay period in order to be executed")]
283        ExecuteProposalDelayNotEnded {},
284        #[error("Proposal has expired")]
285        ExecuteProposalExpired {},
286    }
287
288    impl ContractError {
289        pub fn invalid_proposal<S: Into<String>>(error: S) -> ContractError {
290            ContractError::InvalidProposal {
291                error: error.into(),
292            }
293        }
294    }
295}