diff --git a/ci/integration-tests/src/helpers/helper.rs b/ci/integration-tests/src/helpers/helper.rs index 8b37759d2..e3bb76558 100644 --- a/ci/integration-tests/src/helpers/helper.rs +++ b/ci/integration-tests/src/helpers/helper.rs @@ -247,6 +247,7 @@ pub fn create_proposal( title: "title".to_string(), description: "desc".to_string(), msgs, + vote: None, }, }, key, diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json b/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json index e6f0627f3..3c39d2144 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json +++ b/contracts/pre-propose/dao-pre-propose-approval-single/schema/dao-pre-propose-approval-single.json @@ -1038,6 +1038,16 @@ }, "title": { "type": "string" + }, + "vote": { + "anyOf": [ + { + "$ref": "#/definitions/SingleChoiceAutoVote" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -1047,6 +1057,30 @@ } ] }, + "SingleChoiceAutoVote": { + "type": "object", + "required": [ + "vote" + ], + "properties": { + "rationale": { + "description": "An optional rationale for why this vote was cast. This can be updated, set, or removed later by the address casting the vote.", + "type": [ + "string", + "null" + ] + }, + "vote": { + "description": "The proposer's position on the proposal.", + "allOf": [ + { + "$ref": "#/definitions/Vote" + } + ] + } + }, + "additionalProperties": false + }, "StakingMsg": { "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", "oneOf": [ @@ -1289,6 +1323,31 @@ }, "additionalProperties": false }, + "Vote": { + "oneOf": [ + { + "description": "Marks support for the proposal.", + "type": "string", + "enum": [ + "yes" + ] + }, + { + "description": "Marks opposition to the proposal.", + "type": "string", + "enum": [ + "no" + ] + }, + { + "description": "Marks participation but does not count towards the ratio of support / opposed.", + "type": "string", + "enum": [ + "abstain" + ] + } + ] + }, "VoteOption": { "type": "string", "enum": [ diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs index 215335d83..06714deee 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs @@ -94,11 +94,13 @@ pub fn execute_propose( title, description, msgs, + vote, } => ProposeMsg { title, description, msgs, proposer: Some(info.sender.to_string()), + vote, }, }; diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs index 01b83e361..25bc7ea84 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/msg.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{CosmosMsg, Empty}; use dao_pre_propose_base::msg::{ ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, QueryMsg as QueryBase, }; -use dao_voting::proposal::SingleChoiceProposeMsg as ProposeMsg; +use dao_voting::{proposal::SingleChoiceProposeMsg as ProposeMsg, voting::SingleChoiceAutoVote}; #[cw_serde] pub enum ApproverProposeMessage { @@ -20,6 +20,7 @@ pub enum ProposeMessage { title: String, description: String, msgs: Vec>, + vote: Option, }, } diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs index 07afdf1da..99b8c0369 100644 --- a/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/tests.rs @@ -185,6 +185,7 @@ fn make_pre_proposal(app: &mut App, pre_propose: Addr, proposer: &str, funds: &[ title: "title".to_string(), description: "description".to_string(), msgs: vec![], + vote: None, }, }, funds, @@ -1156,6 +1157,7 @@ fn test_permissions() { title: "I would like to join the DAO".to_string(), description: "though, I am currently not a member.".to_string(), msgs: vec![], + vote: None, }, }, &[], @@ -1304,6 +1306,7 @@ fn test_no_deposit_required_members_submission() { title: "I would like to join the DAO".to_string(), description: "though, I am currently not a member.".to_string(), msgs: vec![], + vote: None, }, }, &[], diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs index 1ec1ec409..fbef73e87 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/tests.rs @@ -309,6 +309,7 @@ fn make_pre_proposal(app: &mut App, pre_propose: Addr, proposer: &str, funds: &[ title: "title".to_string(), description: "description".to_string(), msgs: vec![], + vote: None, }, }, funds, @@ -1151,6 +1152,7 @@ fn test_permissions() { title: "I would like to join the DAO".to_string(), description: "though, I am currently not a member.".to_string(), msgs: vec![], + vote: None, }, }, &[], diff --git a/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json b/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json index e85597f5a..2f1a4675a 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json +++ b/contracts/pre-propose/dao-pre-propose-multiple/schema/dao-pre-propose-multiple.json @@ -929,6 +929,30 @@ } } }, + "MultipleChoiceAutoVote": { + "type": "object", + "required": [ + "vote" + ], + "properties": { + "rationale": { + "description": "An optional rationale for why this vote was cast. This can be updated, set, or removed later by the address casting the vote.", + "type": [ + "string", + "null" + ] + }, + "vote": { + "description": "The proposer's position on the proposal.", + "allOf": [ + { + "$ref": "#/definitions/MultipleChoiceVote" + } + ] + } + }, + "additionalProperties": false + }, "MultipleChoiceOption": { "description": "Unchecked multiple choice option", "type": "object", @@ -969,6 +993,21 @@ }, "additionalProperties": false }, + "MultipleChoiceVote": { + "description": "A multiple choice vote, picking the desired option", + "type": "object", + "required": [ + "option_id" + ], + "properties": { + "option_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "ProposeMessage": { "oneOf": [ { @@ -993,6 +1032,16 @@ }, "title": { "type": "string" + }, + "vote": { + "anyOf": [ + { + "$ref": "#/definitions/MultipleChoiceAutoVote" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs index cfe067683..30e119fbc 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/contract.rs @@ -9,7 +9,10 @@ use dao_pre_propose_base::{ msg::{ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, QueryMsg as QueryBase}, state::PreProposeContract, }; -use dao_voting::multiple_choice::MultipleChoiceOptions; +use dao_voting::{ + multiple_choice::{MultipleChoiceAutoVote, MultipleChoiceOptions}, + proposal::MultipleChoiceProposeMsg as ProposeMsg, +}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-pre-propose-multiple"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -20,6 +23,7 @@ pub enum ProposeMessage { title: String, description: String, choices: MultipleChoiceOptions, + vote: Option, }, } @@ -32,12 +36,7 @@ pub type QueryMsg = QueryBase; /// of the external message. #[cw_serde] enum ProposeMessageInternal { - Propose { - title: String, - description: String, - choices: MultipleChoiceOptions, - proposer: Option, - }, + Propose(ProposeMsg), } type PrePropose = PreProposeContract; @@ -73,14 +72,16 @@ pub fn execute( title, description, choices, + vote, }, } => ExecuteInternal::Propose { - msg: ProposeMessageInternal::Propose { + msg: ProposeMessageInternal::Propose(ProposeMsg { proposer: Some(info.sender.to_string()), title, description, choices, - }, + vote, + }), }, ExecuteMsg::Extension { msg } => ExecuteInternal::Extension { msg }, ExecuteMsg::Withdraw { denom } => ExecuteInternal::Withdraw { denom }, diff --git a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs index 4a8825a5c..56f310ee2 100644 --- a/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-multiple/src/tests.rs @@ -203,6 +203,7 @@ fn make_proposal( }, ], }, + vote: None, }, }, funds, @@ -869,6 +870,7 @@ fn test_permissions() { title: "title".to_string(), }], }, + vote: None, }, }, &[], @@ -975,6 +977,7 @@ fn test_no_deposit_required_members_submission() { title: "title".to_string(), }], }, + vote: None, }, }, &[], diff --git a/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json b/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json index 5d5c772ca..35adbe524 100644 --- a/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json +++ b/contracts/pre-propose/dao-pre-propose-single/schema/dao-pre-propose-single.json @@ -957,6 +957,16 @@ }, "title": { "type": "string" + }, + "vote": { + "anyOf": [ + { + "$ref": "#/definitions/SingleChoiceAutoVote" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -966,6 +976,30 @@ } ] }, + "SingleChoiceAutoVote": { + "type": "object", + "required": [ + "vote" + ], + "properties": { + "rationale": { + "description": "An optional rationale for why this vote was cast. This can be updated, set, or removed later by the address casting the vote.", + "type": [ + "string", + "null" + ] + }, + "vote": { + "description": "The proposer's position on the proposal.", + "allOf": [ + { + "$ref": "#/definitions/Vote" + } + ] + } + }, + "additionalProperties": false + }, "StakingMsg": { "description": "The message types of the staking module.\n\nSee https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto", "oneOf": [ @@ -1208,6 +1242,31 @@ }, "additionalProperties": false }, + "Vote": { + "oneOf": [ + { + "description": "Marks support for the proposal.", + "type": "string", + "enum": [ + "yes" + ] + }, + { + "description": "Marks opposition to the proposal.", + "type": "string", + "enum": [ + "no" + ] + }, + { + "description": "Marks participation but does not count towards the ratio of support / opposed.", + "type": "string", + "enum": [ + "abstain" + ] + } + ] + }, "VoteOption": { "type": "string", "enum": [ diff --git a/contracts/pre-propose/dao-pre-propose-single/src/contract.rs b/contracts/pre-propose/dao-pre-propose-single/src/contract.rs index 66a1e0555..73b283cac 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/contract.rs @@ -11,7 +11,7 @@ use dao_pre_propose_base::{ msg::{ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, QueryMsg as QueryBase}, state::PreProposeContract, }; -use dao_voting::proposal::SingleChoiceProposeMsg as ProposeMsg; +use dao_voting::{proposal::SingleChoiceProposeMsg as ProposeMsg, voting::SingleChoiceAutoVote}; pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-pre-propose-single"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -26,6 +26,7 @@ pub enum ProposeMessage { title: String, description: String, msgs: Vec>, + vote: Option, }, } @@ -74,6 +75,7 @@ pub fn execute( title, description, msgs, + vote, }, } => ExecuteInternal::Propose { msg: ProposeMessageInternal::Propose(ProposeMsg { @@ -82,6 +84,7 @@ pub fn execute( title, description, msgs, + vote, }), }, ExecuteMsg::Extension { msg } => ExecuteInternal::Extension { msg }, diff --git a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs index d766ce5cc..0475d13b6 100644 --- a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs +++ b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs @@ -188,6 +188,7 @@ fn make_proposal( title: "title".to_string(), description: "description".to_string(), msgs: vec![], + vote: None, }, }, funds, @@ -829,6 +830,7 @@ fn test_permissions() { title: "I would like to join the DAO".to_string(), description: "though, I am currently not a member.".to_string(), msgs: vec![], + vote: None, }, }, &[], @@ -917,6 +919,7 @@ fn test_no_deposit_required_members_submission() { title: "I would like to join the DAO".to_string(), description: "though, I am currently not a member.".to_string(), msgs: vec![], + vote: None, }, }, &[], diff --git a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json index 24724d692..f087d6f14 100644 --- a/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json +++ b/contracts/proposal/dao-proposal-multiple/schema/dao-proposal-multiple.json @@ -371,38 +371,7 @@ ], "properties": { "propose": { - "type": "object", - "required": [ - "choices", - "description", - "title" - ], - "properties": { - "choices": { - "description": "The multiple choices.", - "allOf": [ - { - "$ref": "#/definitions/MultipleChoiceOptions" - } - ] - }, - "description": { - "description": "A description of the proposal.", - "type": "string" - }, - "proposer": { - "description": "The address creating the proposal. If no pre-propose module is attached to this module this must always be None as the proposer is the sender of the propose message. If a pre-propose module is attached, this must be Some and will set the proposer of the proposal it creates.", - "type": [ - "string", - "null" - ] - }, - "title": { - "description": "The title of the proposal.", - "type": "string" - } - }, - "additionalProperties": false + "$ref": "#/definitions/MultipleChoiceProposeMsg" } }, "additionalProperties": false @@ -1299,6 +1268,30 @@ }, "additionalProperties": false }, + "MultipleChoiceAutoVote": { + "type": "object", + "required": [ + "vote" + ], + "properties": { + "rationale": { + "description": "An optional rationale for why this vote was cast. This can be updated, set, or removed later by the address casting the vote.", + "type": [ + "string", + "null" + ] + }, + "vote": { + "description": "The proposer's position on the proposal.", + "allOf": [ + { + "$ref": "#/definitions/MultipleChoiceVote" + } + ] + } + }, + "additionalProperties": false + }, "MultipleChoiceOption": { "description": "Unchecked multiple choice option", "type": "object", @@ -1339,6 +1332,52 @@ }, "additionalProperties": false }, + "MultipleChoiceProposeMsg": { + "description": "The contents of a message to create a proposal in the multiple choice proposal module.\n\nWe break this type out of `ExecuteMsg` because we want pre-propose modules that interact with this contract to be able to get type checking on their propose messages.\n\nWe move this type to this package so that pre-propose modules can import it without importing dao-proposal-multiple with the library feature which (as it is not additive) cause the execute exports to not be included in wasm builds.", + "type": "object", + "required": [ + "choices", + "description", + "title" + ], + "properties": { + "choices": { + "description": "The multiple choices.", + "allOf": [ + { + "$ref": "#/definitions/MultipleChoiceOptions" + } + ] + }, + "description": { + "description": "A description of the proposal.", + "type": "string" + }, + "proposer": { + "description": "The address creating the proposal. If no pre-propose module is attached to this module this must always be None as the proposer is the sender of the propose message. If a pre-propose module is attached, this must be Some and will set the proposer of the proposal it creates.", + "type": [ + "string", + "null" + ] + }, + "title": { + "description": "The title of the proposal.", + "type": "string" + }, + "vote": { + "description": "An optional vote cast by the proposer.", + "anyOf": [ + { + "$ref": "#/definitions/MultipleChoiceAutoVote" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, "MultipleChoiceVote": { "description": "A multiple choice vote, picking the desired option", "type": "object", diff --git a/contracts/proposal/dao-proposal-multiple/src/contract.rs b/contracts/proposal/dao-proposal-multiple/src/contract.rs index 09d304964..abe9e73a4 100644 --- a/contracts/proposal/dao-proposal-multiple/src/contract.rs +++ b/contracts/proposal/dao-proposal-multiple/src/contract.rs @@ -1,8 +1,8 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, Response, - StdResult, Storage, SubMsg, WasmMsg, + to_json_binary, Addr, Attribute, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, + Response, StdResult, Storage, SubMsg, WasmMsg, }; use cw2::set_contract_version; @@ -14,17 +14,15 @@ use dao_hooks::proposal::{ }; use dao_hooks::vote::new_vote_hooks; use dao_interface::voting::IsActiveResponse; -use dao_voting::veto::{VetoConfig, VetoError}; use dao_voting::{ - multiple_choice::{ - MultipleChoiceOptions, MultipleChoiceVote, MultipleChoiceVotes, VotingStrategy, - }, + multiple_choice::{MultipleChoiceVote, MultipleChoiceVotes, VotingStrategy}, pre_propose::{PreProposeInfo, ProposalCreationPolicy}, - proposal::{DEFAULT_LIMIT, MAX_PROPOSAL_SIZE}, + proposal::{MultipleChoiceProposeMsg as ProposeMsg, DEFAULT_LIMIT, MAX_PROPOSAL_SIZE}, reply::{ failed_pre_propose_module_hook_id, mask_proposal_execution_proposal_id, TaggedReplyId, }, status::Status, + veto::{VetoConfig, VetoError}, voting::{get_total_power, get_voting_power, validate_voting_period}, }; @@ -98,25 +96,12 @@ pub fn execute( msg: ExecuteMsg, ) -> Result, ContractError> { match msg { - ExecuteMsg::Propose { - title, - description, - choices, - proposer, - } => execute_propose( - deps, - env, - info.sender, - title, - description, - choices, - proposer, - ), + ExecuteMsg::Propose(propose_msg) => execute_propose(deps, env, info, propose_msg), ExecuteMsg::Vote { proposal_id, vote, rationale, - } => execute_vote(deps, env, info, proposal_id, vote, rationale), + } => execute_vote(deps, env, info.sender, proposal_id, vote, rationale), ExecuteMsg::Execute { proposal_id } => execute_execute(deps, env, info, proposal_id), ExecuteMsg::Veto { proposal_id } => execute_veto(deps, env, info, proposal_id), ExecuteMsg::Close { proposal_id } => execute_close(deps, env, info, proposal_id), @@ -164,17 +149,20 @@ pub fn execute( pub fn execute_propose( deps: DepsMut, env: Env, - sender: Addr, - title: String, - description: String, - options: MultipleChoiceOptions, - proposer: Option, + info: MessageInfo, + ProposeMsg { + title, + description, + choices, + proposer, + vote, + }: ProposeMsg, ) -> Result, ContractError> { let config = CONFIG.load(deps.storage)?; let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?; // Check that the sender is permitted to create proposals. - if !proposal_creation_policy.is_permitted(&sender) { + if !proposal_creation_policy.is_permitted(&info.sender) { return Err(ContractError::Unauthorized {}); } @@ -182,7 +170,7 @@ pub fn execute_propose( // pre-propose module, it must be specified. Otherwise, the // proposer should not be specified. let proposer = match (proposer, &proposal_creation_policy) { - (None, ProposalCreationPolicy::Anyone {}) => sender.clone(), + (None, ProposalCreationPolicy::Anyone {}) => info.sender.clone(), // `is_permitted` above checks that an allowed module is // actually sending the propose message. (Some(proposer), ProposalCreationPolicy::Module { .. }) => { @@ -208,7 +196,7 @@ pub fn execute_propose( } // Validate options. - let checked_multiple_choice_options = options.into_checked()?.options; + let checked_multiple_choice_options = choices.into_checked()?.options; let expiration = config.max_voting_period.after(&env.block); let total_power = get_total_power(deps.as_ref(), &config.dao, None)?; @@ -263,11 +251,42 @@ pub fn execute_propose( let hooks = new_proposal_hooks(PROPOSAL_HOOKS, deps.storage, id, proposer.as_str())?; + let sender = info.sender.clone(); + + // Auto cast vote if given. + let (vote_hooks, vote_attributes) = if let Some(vote) = vote { + let response = execute_vote( + deps, + env, + proposer.clone(), + id, + vote.vote, + vote.rationale.clone(), + )?; + ( + response.messages, + vec![ + Attribute { + key: "position".to_string(), + value: vote.vote.to_string(), + }, + Attribute { + key: "rationale".to_string(), + value: vote.rationale.unwrap_or_else(|| "_none".to_string()), + }, + ], + ) + } else { + (vec![], vec![]) + }; + Ok(Response::default() .add_submessages(hooks) + .add_submessages(vote_hooks) .add_attribute("action", "propose") .add_attribute("sender", sender) .add_attribute("proposal_id", id.to_string()) + .add_attributes(vote_attributes) .add_attribute("status", proposal.status.to_string())) } @@ -347,7 +366,7 @@ pub fn execute_veto( pub fn execute_vote( deps: DepsMut, env: Env, - info: MessageInfo, + sender: Addr, proposal_id: u64, vote: MultipleChoiceVote, rationale: Option, @@ -375,7 +394,7 @@ pub fn execute_vote( let vote_power = get_voting_power( deps.as_ref(), - info.sender.clone(), + sender.clone(), &config.dao, Some(prop.start_height), )?; @@ -383,7 +402,7 @@ pub fn execute_vote( return Err(ContractError::NotRegistered {}); } - BALLOTS.update(deps.storage, (proposal_id, &info.sender), |bal| match bal { + BALLOTS.update(deps.storage, (proposal_id, &sender), |bal| match bal { Some(current_ballot) => { if prop.allow_revoting { if current_ballot.vote == vote { @@ -398,7 +417,7 @@ pub fn execute_vote( Ok(Ballot { power: vote_power, vote, - rationale, + rationale: rationale.clone(), }) } } else { @@ -408,7 +427,7 @@ pub fn execute_vote( None => Ok(Ballot { vote, power: vote_power, - rationale, + rationale: rationale.clone(), }), })?; @@ -429,16 +448,20 @@ pub fn execute_vote( VOTE_HOOKS, deps.storage, proposal_id, - info.sender.to_string(), + sender.to_string(), vote.to_string(), )?; Ok(Response::default() .add_submessages(change_hooks) .add_submessages(vote_hooks) .add_attribute("action", "vote") - .add_attribute("sender", info.sender) + .add_attribute("sender", sender) .add_attribute("proposal_id", proposal_id.to_string()) .add_attribute("position", vote.to_string()) + .add_attribute( + "rationale", + rationale.unwrap_or_else(|| "_none".to_string()), + ) .add_attribute("status", prop.status.to_string())) } diff --git a/contracts/proposal/dao-proposal-multiple/src/msg.rs b/contracts/proposal/dao-proposal-multiple/src/msg.rs index 482ffadd7..1ed54db93 100644 --- a/contracts/proposal/dao-proposal-multiple/src/msg.rs +++ b/contracts/proposal/dao-proposal-multiple/src/msg.rs @@ -2,8 +2,9 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cw_utils::Duration; use dao_dao_macros::proposal_module_query; use dao_voting::{ - multiple_choice::{MultipleChoiceOptions, MultipleChoiceVote, VotingStrategy}, + multiple_choice::{MultipleChoiceVote, VotingStrategy}, pre_propose::PreProposeInfo, + proposal::MultipleChoiceProposeMsg, veto::VetoConfig, }; @@ -49,20 +50,7 @@ pub struct InstantiateMsg { #[cw_serde] pub enum ExecuteMsg { /// Creates a proposal in the governance module. - Propose { - /// The title of the proposal. - title: String, - /// A description of the proposal. - description: String, - /// The multiple choices. - choices: MultipleChoiceOptions, - /// The address creating the proposal. If no pre-propose - /// module is attached to this module this must always be None - /// as the proposer is the sender of the propose message. If a - /// pre-propose module is attached, this must be Some and will - /// set the proposer of the proposal it creates. - proposer: Option, - }, + Propose(MultipleChoiceProposeMsg), /// Votes on a proposal. Voting power is determined by the DAO's /// voting power module. Vote { diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/adversarial_tests.rs b/contracts/proposal/dao-proposal-multiple/src/testing/adversarial_tests.rs index d85499917..8f3b593ea 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/adversarial_tests.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/adversarial_tests.rs @@ -53,7 +53,7 @@ fn setup_test(_messages: Vec) -> CommonTest { let mc_options = MultipleChoiceOptions { options }; - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, mc_options); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, mc_options, None); CommonTest { app, @@ -332,7 +332,7 @@ pub fn test_allow_voting_after_proposal_execution_pre_expiration_cw20() { let mc_options = MultipleChoiceOptions { options }; - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, mc_options); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, mc_options, None); // assert initial CREATOR_ADDR address balance is 0 let balance = query_balance_cw20(&app, gov_token.to_string(), CREATOR_ADDR); diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/do_votes.rs b/contracts/proposal/dao-proposal-multiple/src/testing/do_votes.rs index e8b5744bf..7a31d74e5 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/do_votes.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/do_votes.rs @@ -213,6 +213,7 @@ where title: "A simple text proposal".to_string(), description: "This is a simple text proposal".to_string(), choices: mc_options, + vote: None, }, }, &funds, diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/execute.rs b/contracts/proposal/dao-proposal-multiple/src/testing/execute.rs index bc5168e9f..3e136a786 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/execute.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/execute.rs @@ -4,8 +4,10 @@ use cw_multi_test::{App, Executor}; use cw_denom::CheckedDenom; use dao_pre_propose_multiple as cppm; use dao_voting::{ - deposit::CheckedDepositInfo, multiple_choice::MultipleChoiceOptions, + deposit::CheckedDepositInfo, + multiple_choice::{MultipleChoiceAutoVote, MultipleChoiceOptions}, pre_propose::ProposalCreationPolicy, + proposal::MultipleChoiceProposeMsg as ProposeMsg, }; use crate::{ @@ -24,6 +26,7 @@ pub fn make_proposal( proposal_multiple: &Addr, proposer: &str, choices: MultipleChoiceOptions, + vote: Option, ) -> u64 { let proposal_creation_policy = query_creation_policy(app, proposal_multiple); @@ -68,12 +71,13 @@ pub fn make_proposal( .execute_contract( Addr::unchecked(proposer), proposal_multiple.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "title".to_string(), description: "description".to_string(), choices, proposer: None, - }, + vote, + }), &[], ) .unwrap(), @@ -86,6 +90,7 @@ pub fn make_proposal( title: "title".to_string(), description: "description".to_string(), choices, + vote, }, }, &funds, diff --git a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs index 59d4568ae..d7b51beaa 100644 --- a/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-multiple/src/testing/tests.rs @@ -8,6 +8,7 @@ use cw_multi_test::{next_block, App, BankSudo, Contract, ContractWrapper, Execut use cw_utils::Duration; use dao_interface::state::ProposalModule; use dao_interface::state::{Admin, ModuleInstantiateInfo}; +use dao_voting::multiple_choice::MultipleChoiceAutoVote; use dao_voting::veto::{VetoConfig, VetoError}; use dao_voting::{ deposit::{ @@ -20,6 +21,7 @@ use dao_voting::{ MAX_NUM_CHOICES, }, pre_propose::PreProposeInfo, + proposal::MultipleChoiceProposeMsg as ProposeMsg, status::Status, threshold::{ActiveThreshold, PercentageThreshold, Threshold}, }; @@ -155,7 +157,7 @@ fn test_propose() { title: "title".to_string(), }, MultipleChoiceOption { - description: "multiple choice option 1".to_string(), + description: "multiple choice option 2".to_string(), msgs: vec![], title: "title".to_string(), }, @@ -164,7 +166,7 @@ fn test_propose() { let mc_options = MultipleChoiceOptions { options }; // Create a new proposal. - make_proposal(&mut app, &govmod, CREATOR_ADDR, mc_options.clone()); + make_proposal(&mut app, &govmod, CREATOR_ADDR, mc_options.clone(), None); let created: ProposalResponse = query_proposal(&app, &govmod, 1); @@ -237,12 +239,13 @@ fn test_propose_wrong_num_choices() { let err = app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ); assert!(err.is_err()); @@ -263,12 +266,13 @@ fn test_propose_wrong_num_choices() { let err = app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod, - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ); assert!(err.is_err()); @@ -309,6 +313,255 @@ fn test_proposal_count_initialized_to_zero() { assert_eq!(proposal_count, 0); } +#[test] +fn test_propose_auto_vote_winner() { + let mut app = App::default(); + let _govmod_id = app.store_code(proposal_multiple_contract()); + + let max_voting_period = Duration::Height(6); + let quorum = PercentageThreshold::Majority {}; + + let voting_strategy = VotingStrategy::SingleChoice { quorum }; + + let instantiate = InstantiateMsg { + max_voting_period, + only_members_execute: false, + allow_revoting: false, + voting_strategy: voting_strategy.clone(), + min_voting_period: None, + close_proposal_on_execution_failure: true, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }; + + let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + // Check that the config has been configured correctly. + let config: Config = query_proposal_config(&app, &govmod); + let expected = Config { + max_voting_period, + only_members_execute: false, + allow_revoting: false, + dao: core_addr, + voting_strategy: voting_strategy.clone(), + min_voting_period: None, + close_proposal_on_execution_failure: true, + veto: None, + }; + assert_eq!(config, expected); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + + let mc_options = MultipleChoiceOptions { options }; + + // Create a new proposal and auto-vote on the first option. + make_proposal( + &mut app, + &govmod, + CREATOR_ADDR, + mc_options.clone(), + Some(MultipleChoiceAutoVote { + vote: MultipleChoiceVote { option_id: 0 }, + rationale: Some("rationale".to_string()), + }), + ); + + let created: ProposalResponse = query_proposal(&app, &govmod, 1); + + let current_block = app.block_info(); + let checked_options = mc_options.into_checked().unwrap(); + let expected = MultipleChoiceProposal { + title: "title".to_string(), + description: "description".to_string(), + proposer: Addr::unchecked(CREATOR_ADDR), + start_height: current_block.height, + expiration: max_voting_period.after(¤t_block), + choices: checked_options.options, + status: Status::Passed, + voting_strategy, + total_power: Uint128::new(100_000_000), + votes: MultipleChoiceVotes { + vote_weights: vec![Uint128::new(100_000_000), Uint128::zero(), Uint128::zero()], + }, + allow_revoting: false, + min_voting_period: None, + veto: None, + }; + + assert_eq!(created.proposal, expected); + assert_eq!(created.id, 1u64); +} + +#[test] +fn test_propose_auto_vote_reject() { + let mut app = App::default(); + let _govmod_id = app.store_code(proposal_multiple_contract()); + + let max_voting_period = Duration::Height(6); + let quorum = PercentageThreshold::Majority {}; + + let voting_strategy = VotingStrategy::SingleChoice { quorum }; + + let instantiate = InstantiateMsg { + max_voting_period, + only_members_execute: false, + allow_revoting: false, + voting_strategy: voting_strategy.clone(), + min_voting_period: None, + close_proposal_on_execution_failure: true, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }; + + let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + // Check that the config has been configured correctly. + let config: Config = query_proposal_config(&app, &govmod); + let expected = Config { + max_voting_period, + only_members_execute: false, + allow_revoting: false, + dao: core_addr, + voting_strategy: voting_strategy.clone(), + min_voting_period: None, + close_proposal_on_execution_failure: true, + veto: None, + }; + assert_eq!(config, expected); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + + let mc_options = MultipleChoiceOptions { options }; + + // Create a new proposal and auto-vote on the first option. + make_proposal( + &mut app, + &govmod, + CREATOR_ADDR, + mc_options.clone(), + Some(MultipleChoiceAutoVote { + vote: MultipleChoiceVote { option_id: 2 }, + rationale: Some("rationale".to_string()), + }), + ); + + let created: ProposalResponse = query_proposal(&app, &govmod, 1); + + let current_block = app.block_info(); + let checked_options = mc_options.into_checked().unwrap(); + let expected = MultipleChoiceProposal { + title: "title".to_string(), + description: "description".to_string(), + proposer: Addr::unchecked(CREATOR_ADDR), + start_height: current_block.height, + expiration: max_voting_period.after(¤t_block), + choices: checked_options.options, + status: Status::Rejected, + voting_strategy, + total_power: Uint128::new(100_000_000), + votes: MultipleChoiceVotes { + vote_weights: vec![Uint128::zero(), Uint128::zero(), Uint128::new(100_000_000)], + }, + allow_revoting: false, + min_voting_period: None, + veto: None, + }; + + assert_eq!(created.proposal, expected); + assert_eq!(created.id, 1u64); +} + +#[test] +#[should_panic(expected = "Not registered to vote (no voting power) at time of proposal creation")] +fn test_propose_non_member_auto_vote_fail() { + let mut app = App::default(); + let _govmod_id = app.store_code(proposal_multiple_contract()); + + let max_voting_period = Duration::Height(6); + let quorum = PercentageThreshold::Majority {}; + + let voting_strategy = VotingStrategy::SingleChoice { quorum }; + + let instantiate = InstantiateMsg { + max_voting_period, + only_members_execute: false, + allow_revoting: false, + voting_strategy: voting_strategy.clone(), + min_voting_period: None, + close_proposal_on_execution_failure: true, + pre_propose_info: PreProposeInfo::AnyoneMayPropose {}, + veto: None, + }; + + let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let govmod = query_multiple_proposal_module(&app, &core_addr); + + // Check that the config has been configured correctly. + let config: Config = query_proposal_config(&app, &govmod); + let expected = Config { + max_voting_period, + only_members_execute: false, + allow_revoting: false, + dao: core_addr, + voting_strategy: voting_strategy.clone(), + min_voting_period: None, + close_proposal_on_execution_failure: true, + veto: None, + }; + assert_eq!(config, expected); + + let options = vec![ + MultipleChoiceOption { + description: "multiple choice option 1".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + MultipleChoiceOption { + description: "multiple choice option 2".to_string(), + msgs: vec![], + title: "title".to_string(), + }, + ]; + + let mc_options = MultipleChoiceOptions { options }; + + // Should fail if non-member tries to vote on proposal creation. + make_proposal( + &mut app, + &govmod, + "anyone", + mc_options.clone(), + Some(MultipleChoiceAutoVote { + vote: MultipleChoiceVote { option_id: 0 }, + rationale: Some("rationale".to_string()), + }), + ); +} + #[test] fn test_no_early_pass_with_min_duration() { let mut app = App::default(); @@ -368,12 +621,13 @@ fn test_no_early_pass_with_min_duration() { app.execute_contract( Addr::unchecked("whale"), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "This is a simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -482,12 +736,13 @@ fn test_propose_with_messages() { app.execute_contract( Addr::unchecked("whale"), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "This is a simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -649,12 +904,13 @@ fn test_min_duration_same_as_proposal_duration() { app.execute_contract( Addr::unchecked("whale"), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "This is a simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -949,6 +1205,7 @@ fn test_take_proposal_deposit() { title: "title".to_string(), description: "description".to_string(), choices: mc_options.clone(), + vote: None, }, }, &[], @@ -968,7 +1225,7 @@ fn test_take_proposal_deposit() { ) .unwrap(); - make_proposal(&mut app, &govmod, "blue", mc_options); + make_proposal(&mut app, &govmod, "blue", mc_options, None); // Proposal has been executed so deposit has been refunded. let balance = query_balance_cw20(&app, token, "blue".to_string()); @@ -1056,13 +1313,14 @@ fn test_take_native_proposal_deposit() { title: "title".to_string(), description: "description".to_string(), choices: mc_options.clone(), + vote: None, }, }, &[], ) .unwrap_err(); - make_proposal(&mut app, &govmod, "blue", mc_options); + make_proposal(&mut app, &govmod, "blue", mc_options, None); // Proposal has been executed so deposit has been refunded. let balance = query_balance_native(&app, "blue", denom); @@ -1153,6 +1411,7 @@ fn test_native_proposal_deposit() { title: "title".to_string(), description: "description".to_string(), choices: mc_options.clone(), + vote: None, }, }, &[], @@ -1170,7 +1429,7 @@ fn test_native_proposal_deposit() { .unwrap(); // Adding deposit will work - make_proposal(&mut app, &govmod, "blue", mc_options); + make_proposal(&mut app, &govmod, "blue", mc_options, None); // "blue" has been refunded let balance = query_balance_native(&app, "blue", "ujuno"); @@ -1600,6 +1859,7 @@ fn test_cant_propose_zero_power() { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options.clone(), + vote: None, }, }, &[], @@ -1615,6 +1875,7 @@ fn test_cant_propose_zero_power() { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, + vote: None, }, }, &[], @@ -1727,12 +1988,13 @@ fn test_cant_execute_not_member() { app.execute_contract( Addr::unchecked("blue"), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -1818,12 +2080,13 @@ fn test_cant_execute_not_member_when_proposal_created() { app.execute_contract( Addr::unchecked("blue"), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -1927,6 +2190,7 @@ fn test_open_proposal_submission() { }, ], }, + None, ); let created: ProposalResponse = query_proposal(&app, &govmod, 1); @@ -2250,12 +2514,13 @@ fn test_execute_expired_proposal() { app.execute_contract( Addr::unchecked("blue"), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -2547,12 +2812,13 @@ fn test_query_list_proposals() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options.clone(), proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -2820,12 +3086,13 @@ fn test_active_threshold_absolute() { .execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &crate::msg::ExecuteMsg::Propose { + &crate::msg::ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "This is a simple text proposal".to_string(), choices: mc_options.clone(), proposer: None, - }, + vote: None, + }), &[], ) .unwrap_err(); @@ -2845,12 +3112,13 @@ fn test_active_threshold_absolute() { .execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &crate::msg::ExecuteMsg::Propose { + &crate::msg::ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "This is a simple text proposal".to_string(), choices: mc_options.clone(), proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -2868,12 +3136,13 @@ fn test_active_threshold_absolute() { .execute_contract( Addr::unchecked(CREATOR_ADDR), govmod, - &crate::msg::ExecuteMsg::Propose { + &crate::msg::ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "This is a simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap_err(); @@ -2949,12 +3218,13 @@ fn test_active_threshold_percent() { .execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options.clone(), proposer: None, - }, + vote: None, + }), &[], ) .unwrap_err(); @@ -2974,12 +3244,13 @@ fn test_active_threshold_percent() { .execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options.clone(), proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -2997,12 +3268,13 @@ fn test_active_threshold_percent() { .execute_contract( Addr::unchecked(CREATOR_ADDR), govmod, - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap_err(); @@ -3081,12 +3353,13 @@ fn test_active_threshold_none() { .execute_contract( Addr::unchecked(CREATOR_ADDR), govmod, - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options.clone(), proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3106,12 +3379,13 @@ fn test_active_threshold_none() { .execute_contract( Addr::unchecked(CREATOR_ADDR), govmod, - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3168,12 +3442,13 @@ fn test_revoting() { app.execute_contract( Addr::unchecked("a-1"), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3301,12 +3576,13 @@ fn test_allow_revoting_config_changes() { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options.clone(), proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3362,12 +3638,13 @@ fn test_allow_revoting_config_changes() { app.execute_contract( Addr::unchecked("a-2"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A very complex text proposal".to_string(), description: "A very complex text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3454,12 +3731,13 @@ fn test_revoting_same_vote_twice() { app.execute_contract( Addr::unchecked("a-1"), proprosal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3549,12 +3827,13 @@ fn test_invalid_revote_does_not_invalidate_initial_vote() { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3780,12 +4059,13 @@ fn test_close_failed_proposal() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple burn tokens proposal".to_string(), description: "Burning more tokens, than dao treasury have".to_string(), choices: mc_options.clone(), proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3827,7 +4107,7 @@ fn test_close_failed_proposal() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "Disable closing failed proposals".to_string(), description: "We want to re-execute failed proposals".to_string(), choices: MultipleChoiceOptions { @@ -3860,7 +4140,8 @@ fn test_close_failed_proposal() { ], }, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -3892,12 +4173,13 @@ fn test_close_failed_proposal() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple burn tokens proposal".to_string(), description: "Burning more tokens, than dao treasury have".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -4070,6 +4352,7 @@ fn test_no_double_refund_on_execute_fail_and_close() { &govmod, Addr::unchecked(CREATOR_ADDR).as_str(), choices, + None, ); // Vote on proposal @@ -4177,12 +4460,13 @@ pub fn test_not_allow_voting_on_expired_proposal() { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -4272,12 +4556,13 @@ fn test_next_proposal_id() { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -4344,12 +4629,13 @@ fn test_vote_with_rationale() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A proposal".to_string(), description: "A simple proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -4441,12 +4727,13 @@ fn test_revote_with_rationale() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A proposal".to_string(), description: "A simple proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -4597,12 +4884,13 @@ fn test_update_rationale() { app.execute_contract( Addr::unchecked(CREATOR_ADDR), govmod.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A proposal".to_string(), description: "A simple proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -4735,12 +5023,13 @@ fn test_open_proposal_passes_with_zero_timelock_veto_duration() { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -4888,12 +5177,13 @@ fn test_veto_with_no_veto_configuration() { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -4980,12 +5270,13 @@ fn test_veto_open_prop_with_veto_before_passed_disabled() { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -5087,12 +5378,13 @@ fn test_veto_when_veto_timelock_expired() -> anyhow::Result<()> { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -5201,12 +5493,13 @@ fn test_veto_sets_prop_status_to_vetoed() -> anyhow::Result<()> { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -5311,12 +5604,13 @@ fn test_veto_from_catchall_state() { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -5431,12 +5725,13 @@ fn test_veto_timelock_early_execute_happy() -> anyhow::Result<()> { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -5554,12 +5849,13 @@ fn test_veto_timelock_expires_happy() -> anyhow::Result<()> { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); @@ -5666,12 +5962,13 @@ fn test_veto_only_members_execute_proposal() -> anyhow::Result<()> { app.execute_contract( Addr::unchecked("a-1"), proposal_module.clone(), - &ExecuteMsg::Propose { + &ExecuteMsg::Propose(ProposeMsg { title: "A simple text proposal".to_string(), description: "A simple text proposal".to_string(), choices: mc_options, proposer: None, - }, + vote: None, + }), &[], ) .unwrap(); diff --git a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json index 4e6778989..d84fa35d3 100644 --- a/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json +++ b/contracts/proposal/dao-proposal-single/schema/dao-proposal-single.json @@ -1393,6 +1393,30 @@ } ] }, + "SingleChoiceAutoVote": { + "type": "object", + "required": [ + "vote" + ], + "properties": { + "rationale": { + "description": "An optional rationale for why this vote was cast. This can be updated, set, or removed later by the address casting the vote.", + "type": [ + "string", + "null" + ] + }, + "vote": { + "description": "The proposer's position on the proposal.", + "allOf": [ + { + "$ref": "#/definitions/Vote" + } + ] + } + }, + "additionalProperties": false + }, "SingleChoiceProposeMsg": { "description": "The contents of a message to create a proposal in the single choice proposal module.\n\nWe break this type out of `ExecuteMsg` because we want pre-propose modules that interact with this contract to be able to get type checking on their propose messages.\n\nWe move this type to this package so that pre-propose modules can import it without importing dao-proposal-single with the library feature which (as it is not additive) cause the execute exports to not be included in wasm builds.", "type": "object", @@ -1423,6 +1447,17 @@ "title": { "description": "The title of the proposal.", "type": "string" + }, + "vote": { + "description": "An optional vote cast by the proposer.", + "anyOf": [ + { + "$ref": "#/definitions/SingleChoiceAutoVote" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false diff --git a/contracts/proposal/dao-proposal-single/src/contract.rs b/contracts/proposal/dao-proposal-single/src/contract.rs index 711a2f481..d7d9a2972 100644 --- a/contracts/proposal/dao-proposal-single/src/contract.rs +++ b/contracts/proposal/dao-proposal-single/src/contract.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply, + to_json_binary, Addr, Attribute, Binary, Deps, DepsMut, Env, MessageInfo, Order, Reply, Response, StdResult, Storage, SubMsg, WasmMsg, }; use cw2::{get_contract_version, set_contract_version, ContractVersion}; @@ -99,17 +99,12 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::Propose(ProposeMsg { - title, - description, - msgs, - proposer, - }) => execute_propose(deps, env, info.sender, title, description, msgs, proposer), + ExecuteMsg::Propose(propose_msg) => execute_propose(deps, env, info.sender, propose_msg), ExecuteMsg::Vote { proposal_id, vote, rationale, - } => execute_vote(deps, env, info, proposal_id, vote, rationale), + } => execute_vote(deps, env, info.sender, proposal_id, vote, rationale), ExecuteMsg::UpdateRationale { proposal_id, rationale, @@ -158,10 +153,13 @@ pub fn execute_propose( deps: DepsMut, env: Env, sender: Addr, - title: String, - description: String, - msgs: Vec>, - proposer: Option, + ProposeMsg { + title, + description, + msgs, + proposer, + vote, + }: ProposeMsg, ) -> Result { let config = CONFIG.load(deps.storage)?; let proposal_creation_policy = CREATION_POLICY.load(deps.storage)?; @@ -254,11 +252,33 @@ pub fn execute_propose( let hooks = new_proposal_hooks(PROPOSAL_HOOKS, deps.storage, id, proposer.as_str())?; + // Auto cast vote if given. + let (vote_hooks, vote_attributes) = if let Some(vote) = vote { + let response = execute_vote(deps, env, proposer, id, vote.vote, vote.rationale.clone())?; + ( + response.messages, + vec![ + Attribute { + key: "position".to_string(), + value: vote.vote.to_string(), + }, + Attribute { + key: "rationale".to_string(), + value: vote.rationale.unwrap_or_else(|| "_none".to_string()), + }, + ], + ) + } else { + (vec![], vec![]) + }; + Ok(Response::default() .add_submessages(hooks) + .add_submessages(vote_hooks) .add_attribute("action", "propose") .add_attribute("sender", sender) .add_attribute("proposal_id", id.to_string()) + .add_attributes(vote_attributes) .add_attribute("status", proposal.status.to_string())) } @@ -452,7 +472,7 @@ pub fn execute_execute( pub fn execute_vote( deps: DepsMut, env: Env, - info: MessageInfo, + sender: Addr, proposal_id: u64, vote: Vote, rationale: Option, @@ -475,7 +495,7 @@ pub fn execute_vote( let vote_power = get_voting_power( deps.as_ref(), - info.sender.clone(), + sender.clone(), &config.dao, Some(prop.start_height), )?; @@ -483,7 +503,7 @@ pub fn execute_vote( return Err(ContractError::NotRegistered {}); } - BALLOTS.update(deps.storage, (proposal_id, &info.sender), |bal| match bal { + BALLOTS.update(deps.storage, (proposal_id, &sender), |bal| match bal { Some(current_ballot) => { if prop.allow_revoting { if current_ballot.vote == vote { @@ -535,7 +555,7 @@ pub fn execute_vote( VOTE_HOOKS, deps.storage, proposal_id, - info.sender.to_string(), + sender.to_string(), vote.to_string(), )?; @@ -543,10 +563,13 @@ pub fn execute_vote( .add_submessages(change_hooks) .add_submessages(vote_hooks) .add_attribute("action", "vote") - .add_attribute("sender", info.sender) + .add_attribute("sender", sender) .add_attribute("proposal_id", proposal_id.to_string()) .add_attribute("position", vote.to_string()) - .add_attribute("rationale", rationale.as_deref().unwrap_or("_none")) + .add_attribute( + "rationale", + rationale.unwrap_or_else(|| "_none".to_string()), + ) .add_attribute("status", prop.status.to_string())) } diff --git a/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs b/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs index b8883e933..beed03604 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/adversarial_tests.rs @@ -39,7 +39,7 @@ fn setup_test(messages: Vec) -> CommonTest { // Mint some tokens to pay the proposal deposit. mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, messages); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, messages, None); CommonTest { app, @@ -204,7 +204,7 @@ pub fn test_executed_prop_state_remains_after_vote_swing() { let gov_token = query_dao_token(&app, &core_addr); mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); // someone quickly votes, proposal gets executed vote_on_proposal( @@ -320,6 +320,7 @@ pub fn test_passed_prop_state_remains_after_vote_swing() { funds: vec![], } .into()], + None, ); // assert that the initial "threshold" address balance is 0 diff --git a/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs b/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs index aad0cddf8..3646a9ecf 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/do_votes.rs @@ -207,6 +207,7 @@ where title: "A simple text proposal".to_string(), description: "This is a simple text proposal".to_string(), msgs: vec![], + vote: None, }, }, &funds, diff --git a/contracts/proposal/dao-proposal-single/src/testing/execute.rs b/contracts/proposal/dao-proposal-single/src/testing/execute.rs index 657d22874..c8511271f 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/execute.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/execute.rs @@ -4,8 +4,10 @@ use cw_multi_test::{App, BankSudo, Executor}; use cw_denom::CheckedDenom; use dao_pre_propose_single as cppbps; use dao_voting::{ - deposit::CheckedDepositInfo, pre_propose::ProposalCreationPolicy, - proposal::SingleChoiceProposeMsg as ProposeMsg, voting::Vote, + deposit::CheckedDepositInfo, + pre_propose::ProposalCreationPolicy, + proposal::SingleChoiceProposeMsg as ProposeMsg, + voting::{SingleChoiceAutoVote, Vote}, }; use crate::{ @@ -29,6 +31,7 @@ pub(crate) fn make_proposal( proposal_single: &Addr, proposer: &str, msgs: Vec, + vote: Option, ) -> u64 { let proposal_creation_policy = query_creation_policy(app, proposal_single); @@ -78,6 +81,7 @@ pub(crate) fn make_proposal( description: "description".to_string(), msgs: msgs.clone(), proposer: None, + vote, }), &[], ) @@ -91,6 +95,7 @@ pub(crate) fn make_proposal( title: "title".to_string(), description: "description".to_string(), msgs: msgs.clone(), + vote, }, }, &funds, diff --git a/contracts/proposal/dao-proposal-single/src/testing/migration_tests.rs b/contracts/proposal/dao-proposal-single/src/testing/migration_tests.rs index 8dab11460..83196d8cf 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/migration_tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/migration_tests.rs @@ -443,6 +443,7 @@ fn test_v1_v2_full_migration() { funds: vec![], } .into()], + None, ); vote_on_proposal( &mut app, diff --git a/contracts/proposal/dao-proposal-single/src/testing/tests.rs b/contracts/proposal/dao-proposal-single/src/testing/tests.rs index 404dc042d..4246e5a09 100644 --- a/contracts/proposal/dao-proposal-single/src/testing/tests.rs +++ b/contracts/proposal/dao-proposal-single/src/testing/tests.rs @@ -28,7 +28,7 @@ use dao_voting::{ status::Status, threshold::{ActiveThreshold, PercentageThreshold, Threshold}, veto::{VetoConfig, VetoError}, - voting::{Vote, Votes}, + voting::{SingleChoiceAutoVote, Vote, Votes}, }; use crate::{ @@ -88,7 +88,7 @@ fn setup_test(messages: Vec) -> CommonTest { // Mint some tokens to pay the proposal deposit. mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, messages); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, messages, None); CommonTest { app, @@ -157,7 +157,7 @@ fn test_simple_proposal_cw4_voting() { let instantiate = get_default_non_token_dao_proposal_module_instantiate(&mut app); let core_addr = instantiate_with_cw4_groups_governance(&mut app, instantiate, None); let proposal_module = query_single_proposal_module(&app, &core_addr); - let id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); let created = query_proposal(&app, &proposal_module, id); let current_block = app.block_info(); @@ -194,6 +194,104 @@ fn test_simple_proposal_cw4_voting() { assert_eq!(deposit_response.deposit_info, None,); } +#[test] +fn test_simple_proposal_auto_vote_yes() { + let mut app = App::default(); + let instantiate = get_default_non_token_dao_proposal_module_instantiate(&mut app); + let core_addr = instantiate_with_cw4_groups_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![], + Some(SingleChoiceAutoVote { + vote: Vote::Yes, + rationale: Some("rationale".to_string()), + }), + ); + + let created = query_proposal(&app, &proposal_module, id); + let current_block = app.block_info(); + + // These values just come from the default instantiate message + // values. + let expected = SingleChoiceProposal { + title: "title".to_string(), + description: "description".to_string(), + proposer: Addr::unchecked(CREATOR_ADDR), + start_height: current_block.height, + expiration: Duration::Time(604800).after(¤t_block), + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Percent(Decimal::percent(15)), + quorum: PercentageThreshold::Majority {}, + }, + allow_revoting: false, + total_power: Uint128::new(1), + msgs: vec![], + status: Status::Passed, + veto: None, + votes: Votes { + yes: Uint128::new(1), + no: Uint128::zero(), + abstain: Uint128::zero(), + }, + }; + + assert_eq!(created.proposal, expected); + assert_eq!(created.id, 1u64); +} + +#[test] +fn test_simple_proposal_auto_vote_no() { + let mut app = App::default(); + let instantiate = get_default_non_token_dao_proposal_module_instantiate(&mut app); + let core_addr = instantiate_with_cw4_groups_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module(&app, &core_addr); + let id = make_proposal( + &mut app, + &proposal_module, + CREATOR_ADDR, + vec![], + Some(SingleChoiceAutoVote { + vote: Vote::No, + rationale: Some("rationale".to_string()), + }), + ); + + let created = query_proposal(&app, &proposal_module, id); + let current_block = app.block_info(); + + // These values just come from the default instantiate message + // values. + let expected = SingleChoiceProposal { + title: "title".to_string(), + description: "description".to_string(), + proposer: Addr::unchecked(CREATOR_ADDR), + start_height: current_block.height, + expiration: Duration::Time(604800).after(¤t_block), + min_voting_period: None, + threshold: Threshold::ThresholdQuorum { + threshold: PercentageThreshold::Percent(Decimal::percent(15)), + quorum: PercentageThreshold::Majority {}, + }, + allow_revoting: false, + total_power: Uint128::new(1), + msgs: vec![], + status: Status::Rejected, + veto: None, + votes: Votes { + yes: Uint128::zero(), + no: Uint128::new(1), + abstain: Uint128::zero(), + }, + }; + + assert_eq!(created.proposal, expected); + assert_eq!(created.id, 1u64); +} + #[test] fn test_propose_supports_stargate_messages() { // If we can make a proposal with a stargate message, we support @@ -262,7 +360,7 @@ fn test_instantiate_with_non_voting_module_cw20_deposit() { let core_addr = instantiate_with_cw4_groups_governance(&mut app, instantiate, None); let proposal_module = query_single_proposal_module(&app, &core_addr); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); let created = query_proposal(&app, &proposal_module, proposal_id); let current_block = app.block_info(); @@ -337,6 +435,7 @@ fn test_proposal_message_execution() { } .into(), ], + None, ); let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); @@ -432,6 +531,7 @@ fn test_proposal_message_timelock_execution() -> anyhow::Result<()> { } .into(), ], + None, ); let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); @@ -547,6 +647,7 @@ fn test_open_proposal_veto_unauthorized() { } .into(), ], + None, ); // only the vetoer can veto @@ -609,6 +710,7 @@ fn test_open_proposal_veto_with_early_veto_flag_disabled() { } .into(), ], + None, ); let err: ContractError = app @@ -666,6 +768,7 @@ fn test_open_proposal_veto_with_no_timelock() { } .into(), ], + None, ); let err: ContractError = app @@ -731,6 +834,7 @@ fn test_vetoed_proposal_veto() { } .into(), ], + None, ); app.execute_contract( @@ -808,6 +912,7 @@ fn test_open_proposal_veto_early() { } .into(), ], + None, ); app.execute_contract( @@ -874,6 +979,7 @@ fn test_timelocked_proposal_veto_unauthorized() -> anyhow::Result<()> { } .into(), ], + None, ); vote_on_proposal( @@ -974,6 +1080,7 @@ fn test_timelocked_proposal_veto_expired_timelock() -> anyhow::Result<()> { } .into(), ], + None, ); vote_on_proposal( @@ -1059,6 +1166,7 @@ fn test_timelocked_proposal_execute_no_early_exec() -> anyhow::Result<()> { } .into(), ], + None, ); vote_on_proposal( @@ -1142,6 +1250,7 @@ fn test_timelocked_proposal_execute_early() -> anyhow::Result<()> { } .into(), ], + None, ); vote_on_proposal( @@ -1231,6 +1340,7 @@ fn test_timelocked_proposal_execute_active_timelock_unauthorized() -> anyhow::Re } .into(), ], + None, ); vote_on_proposal( @@ -1321,6 +1431,7 @@ fn test_timelocked_proposal_execute_expired_timelock_not_vetoer() -> anyhow::Res } .into(), ], + None, ); vote_on_proposal( @@ -1406,6 +1517,7 @@ fn test_proposal_message_timelock_veto() -> anyhow::Result<()> { } .into(), ], + None, ); let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); @@ -1530,6 +1642,7 @@ fn test_proposal_message_timelock_early_execution() -> anyhow::Result<()> { } .into(), ], + None, ); let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); @@ -1616,6 +1729,7 @@ fn test_proposal_message_timelock_veto_before_passed() { } .into(), ], + None, ); let proposal = query_proposal(&app, &proposal_module, proposal_id); @@ -1688,6 +1802,7 @@ fn test_veto_only_members_execute_proposal() -> anyhow::Result<()> { } .into(), ], + None, ); let cw20_balance = query_balance_cw20(&app, &gov_token, CREATOR_ADDR); let native_balance = query_balance_native(&app, CREATOR_ADDR, "ujuno"); @@ -1801,6 +1916,7 @@ fn test_proposal_cant_close_after_expiry_is_passed() { amount: coins(10, "ujuno"), } .into()], + None, ); vote_on_proposal(&mut app, &proposal_module, "quorum", proposal_id, Vote::Yes); let proposal = query_proposal(&app, &proposal_module, proposal_id); @@ -1849,7 +1965,7 @@ fn test_execute_no_non_passed_execution() { assert!(matches!(err, ContractError::NotPassed {})); mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); vote_on_proposal( &mut app, &proposal_module, @@ -1962,6 +2078,7 @@ fn test_update_config() { funds: vec![], } .into()], + None, ); vote_on_proposal( &mut app, @@ -2061,7 +2178,7 @@ fn test_anyone_may_propose_and_proposal_listing() { for addr in 'm'..'z' { let addr = addr.to_string().repeat(6); - let proposal_id = make_proposal(&mut app, &proposal_module, &addr, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, &addr, vec![], None); vote_on_proposal( &mut app, &proposal_module, @@ -2133,6 +2250,28 @@ fn test_anyone_may_propose_and_proposal_listing() { ) } +#[test] +#[should_panic(expected = "not registered to vote (no voting power) at time of proposal creation")] +fn test_propose_non_member_auto_vote_fails() { + let mut app = App::default(); + let mut instantiate = get_default_token_dao_proposal_module_instantiate(&mut app); + instantiate.pre_propose_info = PreProposeInfo::AnyoneMayPropose {}; + let core_addr = instantiate_with_staked_balances_governance(&mut app, instantiate, None); + let proposal_module = query_single_proposal_module(&app, &core_addr); + + // Should fail if non-member tries to vote on proposal creation. + make_proposal( + &mut app, + &proposal_module, + "anyone", + vec![], + Some(SingleChoiceAutoVote { + vote: Vote::Yes, + rationale: Some("rationale".to_string()), + }), + ); +} + #[test] fn test_proposal_hook_registration() { let CommonTest { @@ -2288,6 +2427,7 @@ fn test_active_threshold_absolute() { description: "description".to_string(), msgs: vec![], proposer: None, + vote: None, }), &[], ) @@ -2307,7 +2447,7 @@ fn test_active_threshold_absolute() { // Proposal creation now works as tokens have been staked to reach // active threshold. - make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); // Unstake some tokens to make it inactive again. let msg = cw20_stake::msg::ExecuteMsg::Unstake { @@ -2326,6 +2466,7 @@ fn test_active_threshold_absolute() { description: "description".to_string(), msgs: vec![], proposer: None, + vote: None, }), &[], ) @@ -2369,6 +2510,7 @@ fn test_active_threshold_percent() { description: "description".to_string(), msgs: vec![], proposer: None, + vote: None, }), &[], ) @@ -2388,7 +2530,7 @@ fn test_active_threshold_percent() { // Proposal creation now works as tokens have been staked to reach // active threshold. - make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); // Unstake some tokens to make it inactive again. let msg = cw20_stake::msg::ExecuteMsg::Unstake { @@ -2408,6 +2550,7 @@ fn test_active_threshold_percent() { description: "description".to_string(), msgs: vec![], proposer: None, + vote: None, }), &[], ) @@ -2448,7 +2591,7 @@ fn test_min_voting_period_no_early_pass() { let proposal_module = query_single_proposal_module(&app, &core_addr); mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); vote_on_proposal( &mut app, &proposal_module, @@ -2490,7 +2633,7 @@ fn test_min_duration_same_as_proposal_duration() { let proposal_module = query_single_proposal_module(&app, &core_addr); mint_cw20s(&mut app, &gov_token, &core_addr, "ekez", 10_000_000); - let proposal_id = make_proposal(&mut app, &proposal_module, "ekez", vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, "ekez", vec![], None); // Whale votes yes. Normally the proposal would just pass and ekez // would be out of luck. @@ -2512,7 +2655,7 @@ fn test_revoting_playthrough() { let proposal_module = query_single_proposal_module(&app, &core_addr); mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); // Vote and change our minds a couple times. vote_on_proposal( @@ -2587,7 +2730,7 @@ fn test_allow_revoting_config_changes() { mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); // This proposal should have revoting enable for its entire // lifetime. - let revoting_proposal = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let revoting_proposal = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); // Update the config of the proposal module to disable revoting. app.execute_contract( @@ -2612,7 +2755,8 @@ fn test_allow_revoting_config_changes() { .unwrap(); mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - let no_revoting_proposal = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let no_revoting_proposal = + make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); vote_on_proposal( &mut app, @@ -2697,7 +2841,7 @@ fn test_three_of_five_multisig() { .unwrap() .address; - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); vote_on_proposal(&mut app, &proposal_module, "one", proposal_id, Vote::Yes); vote_on_proposal(&mut app, &proposal_module, "two", proposal_id, Vote::Yes); @@ -2717,7 +2861,7 @@ fn test_three_of_five_multisig() { assert_eq!(proposal.proposal.status, Status::Executed); // Make another proposal which we'll reject. - let proposal_id = make_proposal(&mut app, &proposal_module, "one", vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, "one", vec![], None); vote_on_proposal(&mut app, &proposal_module, "one", proposal_id, Vote::Yes); vote_on_proposal(&mut app, &proposal_module, "two", proposal_id, Vote::No); @@ -2779,7 +2923,7 @@ fn test_three_of_five_multisig_revoting() { .unwrap() .address; - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); vote_on_proposal(&mut app, &proposal_module, "one", proposal_id, Vote::Yes); vote_on_proposal(&mut app, &proposal_module, "two", proposal_id, Vote::Yes); @@ -3236,7 +3380,7 @@ pub fn test_migrate_updates_version() { // // Make sure we can still make a proposal and vote on it. // mint_cw20s(&mut app, &token_contract, &core_addr, CREATOR_ADDR, 1); -// let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); +// let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); // vote_on_proposal( // &mut app, // &proposal_module, @@ -3299,6 +3443,7 @@ fn test_execution_failed() { amount: coins(10, "ujuno"), } .into()], + None, ); let config = query_proposal_config(&app, &proposal_module); @@ -3421,6 +3566,7 @@ fn test_proposal_too_large() { description: "a".repeat(MAX_PROPOSAL_SIZE as usize), msgs: vec![], proposer: None, + vote: None, }), &[], ) @@ -3472,6 +3618,7 @@ fn test_proposal_creation_permissions() { description: "description".to_string(), msgs: vec![], proposer: None, + vote: None, }), &[], ) @@ -3497,6 +3644,7 @@ fn test_proposal_creation_permissions() { description: "description".to_string(), msgs: vec![], proposer: None, + vote: None, }), &[], ) @@ -3527,6 +3675,7 @@ fn test_proposal_creation_permissions() { description: "description".to_string(), msgs: vec![], proposer: Some("ekez".to_string()), + vote: None, }), &[], ) @@ -3536,7 +3685,7 @@ fn test_proposal_creation_permissions() { assert!(matches!(err, ContractError::InvalidProposer {})); // Works normally. - let proposal_id = make_proposal(&mut app, &proposal_module, "ekez", vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, "ekez", vec![], None); let proposal = query_proposal(&app, &proposal_module, proposal_id); assert_eq!(proposal.proposal.proposer, Addr::unchecked("ekez")); vote_on_proposal( @@ -3692,7 +3841,7 @@ fn test_query_list_votes() { ]), ); let proposal_module = query_single_proposal_module(&app, &core_addr); - let proposal_id = make_proposal(&mut app, &proposal_module, "one", vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, "one", vec![], None); let votes = query_list_votes(&app, &proposal_module, proposal_id, None, None); assert_eq!(votes.votes, vec![]); @@ -3820,6 +3969,7 @@ fn test_update_pre_propose_module() { funds: vec![], } .into()], + None, ); vote_on_proposal( @@ -3861,7 +4011,7 @@ fn test_update_pre_propose_module() { ); // Make a new proposal with this new module installed. - make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); // Check that the deposit was withdrawn. let balance = query_balance_cw20(&app, gov_token.as_str(), CREATOR_ADDR); assert_eq!(balance, Uint128::new(9_999_999)); @@ -3899,6 +4049,7 @@ fn test_update_pre_propose_module() { funds: vec![], } .into()], + None, ); vote_on_proposal( &mut app, @@ -3995,7 +4146,7 @@ fn test_rational_clobbered_on_revote() { let proposal_module = query_single_proposal_module(&app, &core_addr); mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + let proposal_id = make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); let rationale = Some("to_string".to_string()); @@ -4082,7 +4233,7 @@ fn test_proposal_count_goes_up() { assert_eq!(next, 2); mint_cw20s(&mut app, &gov_token, &core_addr, CREATOR_ADDR, 10_000_000); - make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![]); + make_proposal(&mut app, &proposal_module, CREATOR_ADDR, vec![], None); let next = query_next_proposal_id(&app, &proposal_module); assert_eq!(next, 3); diff --git a/contracts/test/dao-proposal-hook-counter/src/tests.rs b/contracts/test/dao-proposal-hook-counter/src/tests.rs index 9ca8effed..611c22dad 100644 --- a/contracts/test/dao-proposal-hook-counter/src/tests.rs +++ b/contracts/test/dao-proposal-hook-counter/src/tests.rs @@ -260,6 +260,7 @@ fn test_counters() { description: "This is a simple text proposal".to_string(), msgs: vec![], proposer: None, + vote: None, }), &[], ) @@ -360,6 +361,7 @@ fn test_counters() { description: "This is a simple text proposal 2nd".to_string(), msgs: vec![], proposer: None, + vote: None, }), &[], ) diff --git a/packages/dao-voting/src/multiple_choice.rs b/packages/dao-voting/src/multiple_choice.rs index 91531a09f..a096281dd 100644 --- a/packages/dao-voting/src/multiple_choice.rs +++ b/packages/dao-voting/src/multiple_choice.rs @@ -170,6 +170,16 @@ impl MultipleChoiceOptions { } } +#[cw_serde] +pub struct MultipleChoiceAutoVote { + /// The proposer's position on the proposal. + pub vote: MultipleChoiceVote, + /// An optional rationale for why this vote was cast. This can + /// be updated, set, or removed later by the address casting + /// the vote. + pub rationale: Option, +} + #[cfg(test)] mod test { use std::vec; diff --git a/packages/dao-voting/src/proposal.rs b/packages/dao-voting/src/proposal.rs index ba685de0d..3f6fe61a8 100644 --- a/packages/dao-voting/src/proposal.rs +++ b/packages/dao-voting/src/proposal.rs @@ -1,6 +1,11 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{CosmosMsg, Empty}; +use crate::{ + multiple_choice::{MultipleChoiceAutoVote, MultipleChoiceOptions}, + voting::SingleChoiceAutoVote, +}; + /// Default limit for proposal pagination. pub const DEFAULT_LIMIT: u64 = 30; pub const MAX_PROPOSAL_SIZE: u64 = 30_000; @@ -31,4 +36,35 @@ pub struct SingleChoiceProposeMsg { /// pre-propose module is attached, this must be Some and will /// set the proposer of the proposal it creates. pub proposer: Option, + /// An optional vote cast by the proposer. + pub vote: Option, +} + +/// The contents of a message to create a proposal in the multiple +/// choice proposal module. +/// +/// We break this type out of `ExecuteMsg` because we want pre-propose +/// modules that interact with this contract to be able to get type +/// checking on their propose messages. +/// +/// We move this type to this package so that pre-propose modules can +/// import it without importing dao-proposal-multiple with the library +/// feature which (as it is not additive) cause the execute exports to +/// not be included in wasm builds. +#[cw_serde] +pub struct MultipleChoiceProposeMsg { + /// The title of the proposal. + pub title: String, + /// A description of the proposal. + pub description: String, + /// The multiple choices. + pub choices: MultipleChoiceOptions, + /// The address creating the proposal. If no pre-propose + /// module is attached to this module this must always be None + /// as the proposer is the sender of the propose message. If a + /// pre-propose module is attached, this must be Some and will + /// set the proposer of the proposal it creates. + pub proposer: Option, + /// An optional vote cast by the proposer. + pub vote: Option, } diff --git a/packages/dao-voting/src/voting.rs b/packages/dao-voting/src/voting.rs index 32f1e1e46..2aabafc2e 100644 --- a/packages/dao-voting/src/voting.rs +++ b/packages/dao-voting/src/voting.rs @@ -29,6 +29,16 @@ pub enum Vote { Abstain, } +#[cw_serde] +pub struct SingleChoiceAutoVote { + /// The proposer's position on the proposal. + pub vote: Vote, + /// An optional rationale for why this vote was cast. This can + /// be updated, set, or removed later by the address casting + /// the vote. + pub rationale: Option, +} + pub enum VoteCmp { Greater, Geq,