Multiple Choice Proposal Module - DA0-DA0/dao-contracts GitHub Wiki

Overview

The multiple choice proposal module allows users to create proposals that have multiple custom choices to vote on (vs. single choice, which only allows the options 'Yes', 'No', and 'Abstain.')

This proposal module can be configured with one of two "voting strategies": either "first-past-the-post", or "ranked-choice". As opposed to single choice proposals, both of these voting strategies require a quorum to be met in order for proposals to pass. With first-past-the-post voting, after the quorum is met, the option with the highest amount of voting power is selected. With ranked-choice voting, users may rank their choices in order of preference, and if there is a candidate with the majority of the first choice votes, they are the automatic winner. If not, the candidate with the least first-preference votes is eliminated, and the corresponding second-preference votes are promoted to first-choice. This process is repeated until there is a candidate that wins a majority.

A multiple choice proposal can have a maximum of 10 other choices. Every proposal will also have a "None of the Above" option. This is analogous to "Abstain" in single-choice voting and allows proposals with only unfavorable options to be rejected.

Module Design

A user may create a multiple choice proposal with an ExecuteMsg::Propose message:

    Propose {
        /// The title of the proposal.
        title: String,
        /// A description of the proposal.
        description: String,
        /// The multiple choices.
        choices: MultipleChoiceOptions,
    }

MultipleChoiceOptions is a wrapper type around a vector of MultipleChoiceOptions:

pub struct MultipleChoiceOption {
    pub description: String,
    pub msgs: Option<Vec<CosmosMsg<Empty>>>,
} 

Upon proposal creation, the MultipleChoiceOptions will be validated into CheckedMultipleChoiceOptions:

pub struct CheckedMultipleChoiceOption {
    // This is the index of the option in both the vote_weights and proposal.choices vectors.
    // Workaround due to not being able to use HashMaps in Cosmwasm.
    pub index: u32,
    pub option_type: MultipleChoiceOptionType,
    pub description: String,
    pub msgs: Option<Vec<CosmosMsg<Empty>>>,
    pub vote_count: Uint128,
}

The MultipleChoiceOptions.into_checked method will append a "None of the Above" choice to the options vector. Additionally, it will append an "index" member in each option struct that denotes the given option's position in the options vector. This allows an option index to be used to select a vote in execute_vote:

pub struct MultipleChoiceVote {
    // A vote indicates which option the user has selected.
    pub option_id: u32,
}

as well as for the proposal's vote counts to be incremented by index:

pub struct MultipleChoiceVotes {
    // Vote weights is a vector of integers indicating the vote weight for each option
    // (the index corresponds to the option's index).
    pub vote_weights: Vec<Uint128>,
}

How a proposal passes

The logic to determine the status of a proposal (i.e. Passed or Rejected) in this voting module also differs quite a bit from single choice proposals.

First-past-the-post voting:

With first-past-the-post voting, these are the steps taken to determine whether a proposal has passed:

1. Check if quorum has been met (if enough voting power has been used to vote on the proposal.)
2. Find the option with the most voting power (if there is a tie, the proposal has not passed.)
3. Check that the winning option is not "None of the Above". 
4. If the proposal has expired and there is a discrete winner, the proposal has passed. 
5. If the proposal has not expired, and there is a discrete winner, [check if the winning choice is unbeatable.](https://github.com/DA0-DA0/dao-contracts/blob/452c7747afa7bc47066668426f9d0f52260851c8/contracts/cw-proposal-multiple/src/proposal.rs#L215). This means that the winning choice's voting power > the second-highest choice's voting power + the remaining voting power in the DAO. 

With first-past-the-post voting, these are the steps taken to determine whether a proposal has been rejected:

1. Check if there is a tie. If there is a tie and either the proposal is expired or there is no voting power left, the proposal is rejected. 
2. If there is a single winner and the proposal is expired: the proposal is rejected only if "None" is the winning option. 
3. If there is a single winner and the proposal is not expired: the proposal is rejected only if "None" is the winning option, and none is unbeatable (using the unbeatable logic from above). 
4. Finally, if quorum was unmet and proposal is expired, the proposal is rejected. 

Ranked-choice voting:

This is in the works and this section will be updated once the code is finished.

⚠️ **GitHub.com Fallback** ⚠️