diff --git a/Cargo.lock b/Cargo.lock index b339f8e7..6b83d661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -824,7 +824,6 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", - "cw-multi-test 0.13.4", "cw-storage-plus 0.13.4", "cw-utils 0.13.4", "cw2 0.13.4", @@ -834,7 +833,12 @@ dependencies = [ "cwd-proposal-hooks", "cwd-proposal-single", "cwd-voting", + "neutron-dao-pre-propose-overrule", "neutron-sdk", + "neutron-subdao-core", + "neutron-subdao-pre-propose-single", + "neutron-subdao-proposal-single", + "neutron-subdao-timelock-single", "schemars", "serde", "thiserror", @@ -986,18 +990,12 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-denom", - "cw-multi-test 0.13.4", "cw-storage-plus 0.13.4", "cw-utils 0.13.4", "cw2 0.13.4", "cw20 0.13.4", - "cw20-base", - "cw4-group", "cwd-interface", "cwd-pre-propose-base", - "cwd-proposal-hooks", - "cwd-subdao-core", - "cwd-subdao-proposal-single", "cwd-voting", "neutron-sdk", "neutron-subdao-pre-propose-single", @@ -1060,6 +1058,9 @@ dependencies = [ "cwd-interface", "cwd-macros", "cwd-pre-propose-base", + "cwd-proposal-single", + "cwd-voting", + "neutron-dao-pre-propose-overrule", "neutron-sdk", "neutron-subdao-core", "neutron-subdao-pre-propose-single", @@ -1384,6 +1385,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "neutron-dao-pre-propose-overrule" +version = "0.1.0" +dependencies = [ + "cwd-pre-propose-base", + "schemars", + "serde", +] + [[package]] name = "neutron-distribution" version = "0.1.0" diff --git a/contracts/dao/cwd-core/src/contract.rs b/contracts/dao/cwd-core/src/contract.rs index d4fc1893..72954cd6 100644 --- a/contracts/dao/cwd-core/src/contract.rs +++ b/contracts/dao/cwd-core/src/contract.rs @@ -330,6 +330,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::ListSubDaos { start_after, limit } => { query_list_sub_daos(deps, start_after, limit) } + QueryMsg::GetSubDao { address } => query_sub_dao(deps, address), QueryMsg::DaoURI {} => query_dao_uri(deps), } } @@ -514,6 +515,18 @@ pub fn query_list_sub_daos( to_binary(&subdaos) } +pub fn query_sub_dao(deps: Deps, address: String) -> StdResult { + let addr = deps.api.addr_validate(&address)?; + let item = SUBDAO_LIST.may_load(deps.storage, &addr)?; + match item { + None => Err(StdError::generic_err("SubDao not found")), + Some(charter) => to_binary(&SubDao { + addr: address, + charter, + }), + } +} + pub fn query_dao_uri(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; to_binary(&config.dao_uri) diff --git a/contracts/dao/cwd-core/src/msg.rs b/contracts/dao/cwd-core/src/msg.rs index 7cbfefe9..c8a34193 100644 --- a/contracts/dao/cwd-core/src/msg.rs +++ b/contracts/dao/cwd-core/src/msg.rs @@ -126,6 +126,8 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + /// Returns the SubDAO for a specific address if it in the list + GetSubDao { address: String }, /// Implements the DAO Star standard: https://daostar.one/EIP DaoURI {}, } diff --git a/contracts/dao/pre-propose/cwd-pre-propose-multiple/examples/schema.rs b/contracts/dao/pre-propose/cwd-pre-propose-multiple/examples/schema.rs index f07f7d81..e9a121e6 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-multiple/examples/schema.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-multiple/examples/schema.rs @@ -1,3 +1,4 @@ +use cosmwasm_std::Empty; use std::env::current_dir; use std::fs::create_dir_all; @@ -14,7 +15,7 @@ fn main() { export_schema(&schema_for!(InstantiateMsg), &out_dir); export_schema(&schema_for!(ExecuteMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(DepositInfoResponse), &out_dir); export_schema_with_title(&schema_for!(Addr), &out_dir, "ProposalModuleResponse"); diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/schema/query_msg.json b/contracts/dao/pre-propose/cwd-pre-propose-multiple/schema/query_msg_for__empty.json similarity index 60% rename from contracts/dao/pre-propose/cwd-pre-propose-single-overrule/schema/query_msg.json rename to contracts/dao/pre-propose/cwd-pre-propose-multiple/schema/query_msg_for__empty.json index a057bb1e..f481ed19 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/schema/query_msg.json +++ b/contracts/dao/pre-propose/cwd-pre-propose-multiple/schema/query_msg_for__empty.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", + "title": "QueryMsg_for_Empty", "oneOf": [ { "description": "Gets the proposal module that this pre propose module is associated with. Returns `Addr`.", @@ -63,6 +63,33 @@ } }, "additionalProperties": false + }, + { + "description": "Extension for queries. The default implementation will do nothing if queried for will return `Binary::default()`.", + "type": "object", + "required": [ + "query_extension" + ], + "properties": { + "query_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" } - ] + } } diff --git a/contracts/dao/pre-propose/cwd-pre-propose-multiple/src/contract.rs b/contracts/dao/pre-propose/cwd-pre-propose-multiple/src/contract.rs index fe4d24ef..32de6066 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-multiple/src/contract.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-multiple/src/contract.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cosmwasm_std::{Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version; use cwd_pre_propose_base::{ @@ -25,7 +25,7 @@ pub enum ProposeMessage { pub type InstantiateMsg = InstantiateBase; pub type ExecuteMsg = ExecuteBase; -pub type QueryMsg = QueryBase; +pub type QueryMsg = QueryBase; /// Internal version of the propose message that includes the /// `proposer` field. The module will fill this in based on the sender @@ -40,7 +40,7 @@ enum ProposeMessageInternal { }, } -type PrePropose = PreProposeContract; +type PrePropose = PreProposeContract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/Cargo.toml b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/Cargo.toml index b21f0ba5..cefd0b7b 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/Cargo.toml +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/Cargo.toml @@ -25,10 +25,16 @@ cwd-voting = { path = "../../../../packages/cwd-voting" } neutron_bindings = { package = "neutron-sdk", version = "0.4.0" } schemars = "0.8.8" thiserror = { version = "1.0.31" } +neutron-subdao-core = { version = "*", path = "../../../../packages/neutron-subdao-core" } +cwd-core = { version = "*", path = "../../../../contracts/dao/cwd-core", features = ["library"] } +neutron-subdao-timelock-single = { version = "*", path = "../../../../packages/neutron-subdao-timelock-single" } +neutron-subdao-proposal-single = { version = "*", path = "../../../../packages/neutron-subdao-proposal-single" } +neutron-subdao-pre-propose-single = { version = "*", path = "../../../../packages/neutron-subdao-pre-propose-single" } +neutron-dao-pre-propose-overrule = { version = "*", path = "../../../../packages/neutron-dao-pre-propose-overrule" } +cwd-proposal-single = { version = "*", path = "../../../../contracts/dao/proposal/cwd-proposal-single", features = ["library"] } [dev-dependencies] cosmwasm-schema = "1.0.0" -cw-multi-test = "0.13.2" cw-utils = "0.13.2" cwd-proposal-single = { path = "../../proposal/cwd-proposal-single" } cwd-core = { path = "../../cwd-core" } diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/examples/schema.rs b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/examples/schema.rs index bdf7a0ba..ad9a7a4e 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/examples/schema.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/examples/schema.rs @@ -4,7 +4,7 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; use cosmwasm_std::Addr; use cwd_pre_propose_base::msg::{DepositInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; -use cwd_pre_propose_overrule::contract::ProposeMessage; +use neutron_dao_pre_propose_overrule::msg::{ProposeMessage, QueryExt}; fn main() { let mut out_dir = current_dir().unwrap(); @@ -14,7 +14,7 @@ fn main() { export_schema(&schema_for!(InstantiateMsg), &out_dir); export_schema(&schema_for!(ExecuteMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(DepositInfoResponse), &out_dir); export_schema_with_title(&schema_for!(Addr), &out_dir, "ProposalModuleResponse"); diff --git a/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/schema/query_msg.json b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/schema/query_msg_for__query_ext.json similarity index 53% rename from contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/schema/query_msg.json rename to contracts/dao/pre-propose/cwd-pre-propose-single-overrule/schema/query_msg_for__query_ext.json index a057bb1e..72150917 100644 --- a/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/schema/query_msg.json +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/schema/query_msg_for__query_ext.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", + "title": "QueryMsg_for_QueryExt", "oneOf": [ { "description": "Gets the proposal module that this pre propose module is associated with. Returns `Addr`.", @@ -63,6 +63,59 @@ } }, "additionalProperties": false + }, + { + "description": "Extension for queries. The default implementation will do nothing if queried for will return `Binary::default()`.", + "type": "object", + "required": [ + "query_extension" + ], + "properties": { + "query_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryExt" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "QueryExt": { + "oneOf": [ + { + "type": "object", + "required": [ + "overrule_proposal_id" + ], + "properties": { + "overrule_proposal_id": { + "type": "object", + "required": [ + "subdao_proposal_id", + "timelock_address" + ], + "properties": { + "subdao_proposal_id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "timelock_address": { + "type": "string" + } + } + } + }, + "additionalProperties": false + } + ] } - ] + } } diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/contract.rs b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/contract.rs index 9e7f4d9a..842710ed 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/contract.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/contract.rs @@ -1,62 +1,39 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult, WasmMsg, + to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + WasmMsg, }; use cw2::set_contract_version; -use neutron_bindings::bindings::msg::NeutronMsg; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - use error::PreProposeOverruleError; use crate::error; use cwd_pre_propose_base::{ error::PreProposeError, - msg::{ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase, QueryMsg as QueryBase}, + msg::{ExecuteMsg as ExecuteBase, InstantiateMsg as InstantiateBase}, state::PreProposeContract, }; +use neutron_dao_pre_propose_overrule::msg::{ + ExecuteMsg, InstantiateMsg, ProposeMessage, QueryExt, QueryMsg, +}; + +use crate::state::PROPOSALS; +use cwd_core::{msg::QueryMsg as MainDaoQueryMsg, query::SubDao}; +use cwd_proposal_single::{ + msg::ExecuteMsg as ProposeMessageInternal, msg::QueryMsg as ProposalSingleQueryMsg, +}; +use cwd_voting::pre_propose::ProposalCreationPolicy; +use neutron_subdao_core::{msg::QueryMsg as SubdaoQueryMsg, types as SubdaoTypes}; +use neutron_subdao_pre_propose_single::msg::{ + QueryExt as SubdaoPreProposeQueryExt, QueryMsg as SubdaoPreProposeQueryMsg, +}; +use neutron_subdao_proposal_single::msg as SubdaoProposalMsg; +use neutron_subdao_timelock_single::{msg as TimelockMsg, types as TimelockTypes}; pub(crate) const CONTRACT_NAME: &str = "crates.io:cwd-pre-propose-single-overrule"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -#[derive(Serialize, JsonSchema, Deserialize, Debug, Clone)] -#[serde(rename_all = "snake_case")] -pub enum ProposeMessage { - ProposeOverrule { - timelock_contract: String, - proposal_id: u64, - }, -} - -#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] -#[serde(rename_all = "snake_case")] -pub enum TimelockExecuteMsg { - OverruleProposal { proposal_id: u64 }, -} - -pub type ExecuteMsg = ExecuteBase; -pub type QueryMsg = QueryBase; - -#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct InstantiateMsg {} - -/// Internal version of the propose message that includes the -/// `proposer` field. The module will fill this in based on the sender -/// of the external message. -#[derive(Serialize, JsonSchema, Deserialize, Debug, Clone)] -#[serde(rename_all = "snake_case")] -pub enum ProposeMessageInternal { - Propose { - title: String, - description: String, - msgs: Vec>, - proposer: Option, - }, -} - -type PrePropose = PreProposeContract; +type PrePropose = PreProposeContract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -65,18 +42,20 @@ pub fn instantiate( info: MessageInfo, _msg: InstantiateMsg, ) -> Result { - // the contract has no info for instantiation so far, so it just calls the init function of base - // deposit is set to zero because it makes no sense for overrule proposals - // for open submission it's tbd + // The contract has no info for instantiation so far, so it just calls the init function of base let resp = PrePropose::default().instantiate( deps.branch(), env, info, InstantiateBase { + // We restrict deposits since overrule proposals are supposed to be created automatically deposit_info: None, + // Actually, the overrule proposal is going to be created by the timelock contract which + // is not the DAO member and has no voting power. open_proposal_submission: true, }, )?; + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; Ok(resp) } @@ -88,12 +67,8 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { - // We don't want to expose the `proposer` field on the propose - // message externally as that is to be set by this module. Here, - // we transform an external message which omits that field into an - // internal message which sets it. type ExecuteInternal = ExecuteBase; - match msg { + let internal_msg = match msg { ExecuteMsg::Propose { msg: ProposeMessage::ProposeOverrule { @@ -103,34 +78,211 @@ pub fn execute( } => { let timelock_contract_addr = deps.api.addr_validate(&timelock_contract)?; + if let Ok(id) = + PROPOSALS.load(deps.storage, (proposal_id, timelock_contract_addr.clone())) + { + return Err(PreProposeOverruleError::AlreadyExists { id }); + } + + let subdao_address = get_subdao_from_timelock(&deps, &timelock_contract_addr)?; + + // We need this check since the timelock contract might be an impostor + // E.g. the timelock contract might be a malicious contract that is not a part of + // the subdao but pretends to be. + if get_timelock_from_subdao(&deps, &subdao_address)? != timelock_contract { + return Err(PreProposeOverruleError::SubdaoMisconfigured {}); + } + + if !is_subdao_legit(&deps, &subdao_address)? { + return Err(PreProposeOverruleError::ForbiddenSubdao {}); + } + + if !is_proposal_timelocked(&deps, &timelock_contract_addr, proposal_id)? { + return Err(PreProposeOverruleError::ProposalWrongState {}); + } + let overrule_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: timelock_contract_addr.to_string(), - msg: to_binary(&TimelockExecuteMsg::OverruleProposal { proposal_id })?, + msg: to_binary(&TimelockMsg::ExecuteMsg::OverruleProposal { proposal_id })?, funds: vec![], }); + let subdao_name = get_subdao_name(&deps, &subdao_address)?; + let prop_name: String = format!( + "Reject the proposal #{} of the '{}' subdao", + proposal_id, subdao_name + ); + let prop_desc: String = format!( + "If this proposal will be accepted, the DAO is going to \ +overrule the proposal #{} of '{}' subdao (address {})", + proposal_id, subdao_name, subdao_address + ); + let internal_msg = ExecuteInternal::Propose { msg: ProposeMessageInternal::Propose { // Fill in proposer based on message sender. proposer: Some(info.sender.to_string()), - title: "Overrule proposal".to_string(), - description: "Reject the decision made by subdao".to_string(), + title: prop_name, + description: prop_desc, msgs: vec![overrule_msg], }, }; - let result = PrePropose::default().execute(deps, env, info, internal_msg); + let next_proposal_id = &get_next_proposal_id(&deps)?; - match result { - Ok(response) => Ok(response), - Err(error) => Err(PreProposeOverruleError::PreProposeBase(error)), - } + PROPOSALS.save( + deps.storage, + (proposal_id, timelock_contract_addr), + next_proposal_id, + )?; + + Ok(internal_msg) } + // The following messages are forwarded to the base contract + ExecuteMsg::ProposalCreatedHook { + proposal_id, + proposer, + } => Ok(ExecuteInternal::ProposalCreatedHook { + proposal_id, + proposer, + }), + ExecuteMsg::ProposalCompletedHook { + proposal_id, + new_status, + } => Ok(ExecuteInternal::ProposalCompletedHook { + proposal_id, + new_status, + }), + // ExecuteMsg::Withdraw and ExecuteMsg::UpdateConfig are unsupported + // ExecuteMsg::Withdraw is unsupported because overrule proposals should have no deposits + // ExecuteMsg::UpdateConfig since the config has only the info about deposits, + // no custom fields are added. _ => Err(PreProposeOverruleError::MessageUnsupported {}), + }; + PrePropose::default() + .execute(deps, env, info, internal_msg?) + .map_err(PreProposeOverruleError::PreProposeBase) +} + +fn get_subdao_from_timelock( + deps: &DepsMut, + timelock_contract: &Addr, +) -> Result { + let timelock_config: TimelockTypes::Config = deps.querier.query_wasm_smart( + timelock_contract.to_string(), + &TimelockMsg::QueryMsg::Config {}, + )?; + Ok(timelock_config.subdao) +} + +fn get_timelock_from_subdao( + deps: &DepsMut, + subdao_core: &Addr, +) -> Result { + let proposal_modules: Vec = deps.querier.query_wasm_smart( + subdao_core, + &SubdaoQueryMsg::ProposalModules { + start_after: None, + // we assume any subdao proposal module has pre-propose module with timelock. + // thus, we need only single module + limit: Some(1), + }, + )?; + + let proposal_module = proposal_modules + .first() + .ok_or(PreProposeOverruleError::SubdaoMisconfigured {})? + .address + .clone(); + + let prop_policy: ProposalCreationPolicy = deps.querier.query_wasm_smart( + proposal_module, + &SubdaoProposalMsg::QueryMsg::ProposalCreationPolicy {}, + )?; + + match prop_policy { + ProposalCreationPolicy::Anyone {} => Err(PreProposeOverruleError::SubdaoMisconfigured {}), + ProposalCreationPolicy::Module { addr } => { + let timelock: Addr = deps.querier.query_wasm_smart( + addr, + &SubdaoPreProposeQueryMsg::QueryExtension { + msg: SubdaoPreProposeQueryExt::TimelockAddress {}, + }, + )?; + Ok(timelock) + } } } +fn is_subdao_legit(deps: &DepsMut, subdao_core: &Addr) -> Result { + let main_dao = get_main_dao_address(deps)?; + + let subdao: StdResult = deps.querier.query_wasm_smart( + main_dao, + &MainDaoQueryMsg::GetSubDao { + address: subdao_core.to_string(), + }, + ); + + match subdao { + Ok(subdao) => Ok(subdao.addr == *subdao_core), + Err(_) => Err(PreProposeOverruleError::ForbiddenSubdao {}), + } +} + +fn get_main_dao_address(deps: &DepsMut) -> Result { + let dao: Addr = deps.querier.query_wasm_smart( + PrePropose::default().proposal_module.load(deps.storage)?, + &ProposalSingleQueryMsg::Dao {}, + )?; + Ok(dao) +} + +fn get_next_proposal_id(deps: &DepsMut) -> Result { + let last_proposal_id: u64 = deps.querier.query_wasm_smart( + PrePropose::default().proposal_module.load(deps.storage)?, + &ProposalSingleQueryMsg::ProposalCount {}, + )?; + Ok(last_proposal_id + 1) +} + +fn is_proposal_timelocked( + deps: &DepsMut, + timelock: &Addr, + proposal_id: u64, +) -> Result { + let proposal: TimelockTypes::SingleChoiceProposal = deps + .querier + .query_wasm_smart(timelock, &TimelockMsg::QueryMsg::Proposal { proposal_id })?; + Ok(proposal.status == TimelockTypes::ProposalStatus::Timelocked) +} + +fn get_subdao_name(deps: &DepsMut, subdao: &Addr) -> Result { + let subdao_config: SubdaoTypes::Config = deps + .querier + .query_wasm_smart(subdao, &SubdaoQueryMsg::Config {})?; + Ok(subdao_config.name) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { - PrePropose::default().query(deps, env, msg) + match msg { + QueryMsg::QueryExtension { + msg: + QueryExt::OverruleProposalId { + timelock_address, + subdao_proposal_id, + }, + } => { + let overrule_proposal_id = PROPOSALS.load( + deps.storage, + ( + subdao_proposal_id, + deps.api.addr_validate(&timelock_address)?, + ), + )?; + to_binary(&overrule_proposal_id) + } + _ => PrePropose::default().query(deps, env, msg), + } } diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/error.rs b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/error.rs index d351fa83..31fd1345 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/error.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/error.rs @@ -10,6 +10,18 @@ pub enum PreProposeOverruleError { #[error(transparent)] PreProposeBase(PreProposeError), - #[error("Base pre propose messages aren't supported.")] + #[error("Base pre propose messages aren't supported")] MessageUnsupported {}, + + #[error("Subdao is misconfigured")] + SubdaoMisconfigured {}, + + #[error("Subdao isn't in the list")] + ForbiddenSubdao {}, + + #[error("Subdao proposal isn't timelocked")] + ProposalWrongState {}, + + #[error("Overrule proposal for this subdao proposal already created with id ({id})")] + AlreadyExists { id: u64 }, } diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/lib.rs b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/lib.rs index 26cc40dc..c8622d46 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/lib.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/lib.rs @@ -1,5 +1,6 @@ pub mod contract; -pub mod error; +mod error; +mod state; #[cfg(test)] pub mod testing; diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/state.rs b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/state.rs new file mode 100644 index 00000000..76c76d49 --- /dev/null +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/state.rs @@ -0,0 +1,4 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Map; + +pub const PROPOSALS: Map<(u64, Addr), u64> = Map::new("overrule_proposals"); diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/testing/mock_querier.rs b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/testing/mock_querier.rs index 235d5938..2557e60d 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/testing/mock_querier.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/testing/mock_querier.rs @@ -1,19 +1,41 @@ +use std::collections::{HashMap, HashSet}; use std::marker::PhantomData; use cosmwasm_std::{ from_binary, from_slice, testing::{MockApi, MockQuerier, MockStorage}, - to_binary, ContractResult, Empty, OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, - SystemResult, WasmQuery, + to_binary, Addr, Binary, ContractResult, Empty, OwnedDeps, Querier, QuerierResult, + QueryRequest, SystemError, SystemResult, WasmQuery, }; -use cwd_pre_propose_base::msg::QueryMsg as PreProposeQuery; +use cwd_core::{msg::QueryMsg as MainDaoQueryMsg, query::SubDao}; +use cwd_proposal_single::msg::QueryMsg as ProposalSingleQueryMsg; -pub const MOCK_CORE_MODULE: &str = "neutron1dao_core_contract"; -pub const MOCK_PROPOSE_MODULE: &str = "neutron1propose_module"; +use neutron_subdao_core::{msg::QueryMsg as SubdaoQueryMsg, types as SubdaoTypes}; +use neutron_subdao_pre_propose_single::msg::{ + QueryExt as SubdaoPreProposeQueryExt, QueryMsg as SubdaoPreProposeQueryMsg, +}; +use neutron_subdao_proposal_single::msg as SubdaoProposalMsg; +use neutron_subdao_timelock_single::types::{ProposalStatus, SingleChoiceProposal}; +use neutron_subdao_timelock_single::{msg as TimelockMsg, types as TimelockTypes}; + +pub const MOCK_DAO_CORE: &str = "neutron1dao_core_contract"; +pub const MOCK_SUBDAO_PROPOSE_MODULE: &str = "neutron1subdao_propose_module"; +pub const MOCK_SUBDAO_PREPROPOSE_MODULE: &str = "neutron1subdao_prepropose_module"; +pub const MOCK_DAO_PROPOSE_MODULE: &str = "neutron1propose_module"; pub const MOCK_TIMELOCK_CONTRACT: &str = "neutron1timelock_contract"; +pub const MOCK_SUBDAO_CORE: &str = "neutron1subdao_core"; + +pub const MOCK_IMPOSTOR_TIMELOCK_CONTRACT: &str = "neutron1timelock_contract_impostor"; + +pub const SUBDAO_NAME: &str = "Based DAO"; +pub const TIMELOCKED_PROPOSAL_ID: u64 = 42; +pub const NON_TIMELOCKED_PROPOSAL_ID: u64 = 24; +pub const PROPOSALS_COUNT: u64 = 61; -pub fn mock_dependencies() -> OwnedDeps { - let custom_querier = WasmMockQuerier::new(MockQuerier::new(&[])); +pub fn mock_dependencies( + contracts: HashMap>, +) -> OwnedDeps { + let custom_querier = WasmMockQuerier::new(MockQuerier::new(&[]), contracts); OwnedDeps { storage: MockStorage::default(), @@ -25,6 +47,7 @@ pub fn mock_dependencies() -> OwnedDeps { pub struct WasmMockQuerier { base: MockQuerier, + contracts: HashMap>, } impl Querier for WasmMockQuerier { @@ -46,19 +69,13 @@ impl WasmMockQuerier { pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { match &request { QueryRequest::Wasm(WasmQuery::Smart { contract_addr, msg }) => { - if contract_addr == MOCK_PROPOSE_MODULE { - let q: PreProposeQuery = from_binary(msg).unwrap(); - let addr = match q { - PreProposeQuery::ProposalModule {} => todo!(), - PreProposeQuery::Dao {} => MOCK_CORE_MODULE, - PreProposeQuery::Config {} => todo!(), - PreProposeQuery::DepositInfo { proposal_id: _ } => todo!(), - }; - return SystemResult::Ok(ContractResult::from(to_binary(addr))); + let mock = self.contracts.get(contract_addr); + match mock { + None => SystemResult::Err(SystemError::NoSuchContract { + addr: contract_addr.to_string(), + }), + Some(m) => m.query(msg), } - SystemResult::Err(SystemError::NoSuchContract { - addr: contract_addr.to_string(), - }) } _ => self.base.handle_query(request), } @@ -66,7 +83,221 @@ impl WasmMockQuerier { } impl WasmMockQuerier { - pub fn new(base: MockQuerier) -> Self { - WasmMockQuerier { base } + pub fn new(base: MockQuerier, contracts: HashMap>) -> Self { + WasmMockQuerier { base, contracts } + } +} + +pub trait ContractQuerier { + fn query(&self, msg: &Binary) -> QuerierResult; +} + +pub struct MockDaoQueries { + sub_dao_set: HashSet, +} + +impl ContractQuerier for MockDaoQueries { + fn query(&self, msg: &Binary) -> QuerierResult { + let q: MainDaoQueryMsg = from_binary(msg).unwrap(); + match q { + MainDaoQueryMsg::GetSubDao { address } => match self.sub_dao_set.contains(&address) { + true => SystemResult::Ok(ContractResult::from(to_binary(&SubDao { + addr: address.clone(), + charter: None, + }))), + false => SystemResult::Err(SystemError::Unknown {}), + }, + _ => SystemResult::Err(SystemError::Unknown {}), + } + } +} + +pub struct MockTimelockQueries { + owner: String, + subdao: String, +} + +impl ContractQuerier for MockTimelockQueries { + fn query(&self, msg: &Binary) -> QuerierResult { + let q: TimelockMsg::QueryMsg = from_binary(msg).unwrap(); + match q { + TimelockMsg::QueryMsg::Config {} => { + SystemResult::Ok(ContractResult::from(to_binary(&TimelockTypes::Config { + owner: Addr::unchecked(self.owner.clone()), + overrule_pre_propose: Addr::unchecked(""), + subdao: Addr::unchecked(self.subdao.clone()), + }))) + } + TimelockMsg::QueryMsg::Proposal { proposal_id } => { + SystemResult::Ok(ContractResult::from(to_binary(&SingleChoiceProposal { + id: proposal_id, + msgs: vec![], + status: match proposal_id { + TIMELOCKED_PROPOSAL_ID => ProposalStatus::Timelocked, + _ => ProposalStatus::Executed, + }, + }))) + } + _ => SystemResult::Err(SystemError::Unknown {}), + } + } +} + +pub struct MockDaoProposalQueries { + dao_core: String, +} + +impl ContractQuerier for MockDaoProposalQueries { + fn query(&self, msg: &Binary) -> QuerierResult { + let q: ProposalSingleQueryMsg = from_binary(msg).unwrap(); + match q { + ProposalSingleQueryMsg::Dao {} => { + SystemResult::Ok(ContractResult::from(to_binary(&self.dao_core))) + } + ProposalSingleQueryMsg::ProposalCount {} => { + SystemResult::Ok(ContractResult::from(to_binary(&PROPOSALS_COUNT))) + } + _ => SystemResult::Err(SystemError::Unknown {}), + } + } +} + +pub struct MockSubdaoProposalQueries { + pre_propose: String, +} + +impl ContractQuerier for MockSubdaoProposalQueries { + fn query(&self, msg: &Binary) -> QuerierResult { + let q: SubdaoProposalMsg::QueryMsg = from_binary(msg).unwrap(); + match q { + SubdaoProposalMsg::QueryMsg::ProposalCreationPolicy {} => { + SystemResult::Ok(ContractResult::from(to_binary( + &cwd_voting::pre_propose::ProposalCreationPolicy::Module { + addr: Addr::unchecked(self.pre_propose.clone()), + }, + ))) + } + _ => SystemResult::Err(SystemError::Unknown {}), + } + } +} + +pub struct MockSubaoPreProposalQueries { + timelock: String, +} + +impl ContractQuerier for MockSubaoPreProposalQueries { + fn query(&self, msg: &Binary) -> QuerierResult { + let q: SubdaoPreProposeQueryMsg = from_binary(msg).unwrap(); + match q { + SubdaoPreProposeQueryMsg::QueryExtension { + msg: SubdaoPreProposeQueryExt::TimelockAddress {}, + } => SystemResult::Ok(ContractResult::from(to_binary(&Addr::unchecked( + self.timelock.clone(), + )))), + _ => SystemResult::Err(SystemError::Unknown {}), + } } } + +pub struct MockSubdaoCoreQueries { + proposal_module: String, + dao_core: String, +} + +impl ContractQuerier for MockSubdaoCoreQueries { + fn query(&self, msg: &Binary) -> QuerierResult { + let q: SubdaoQueryMsg = from_binary(msg).unwrap(); + match q { + SubdaoQueryMsg::ProposalModules { + start_after: _, + limit: _, + } => SystemResult::Ok(ContractResult::from(to_binary(&vec![ + SubdaoTypes::ProposalModule { + address: Addr::unchecked(self.proposal_module.clone()), + prefix: "".to_string(), + status: SubdaoTypes::ProposalModuleStatus::Enabled, + }, + ]))), + SubdaoQueryMsg::Config {} => { + SystemResult::Ok(ContractResult::from(to_binary(&SubdaoTypes::Config { + name: SUBDAO_NAME.to_string(), + description: "".to_string(), + dao_uri: None, + main_dao: Addr::unchecked(self.dao_core.clone()), + security_dao: Addr::unchecked(""), + }))) + } + _ => SystemResult::Err(SystemError::Unknown {}), + } + } +} + +pub fn get_properly_initialized_dao() -> HashMap> { + let mut contracts: HashMap> = HashMap::new(); + contracts.insert( + MOCK_DAO_PROPOSE_MODULE.to_string(), + Box::new(MockDaoProposalQueries { + dao_core: MOCK_DAO_CORE.parse().unwrap(), + }), + ); + contracts.insert( + MOCK_DAO_CORE.to_string(), + Box::new(MockDaoQueries { + sub_dao_set: HashSet::from([MOCK_SUBDAO_CORE.to_string()]), + }), + ); + contracts.insert( + MOCK_TIMELOCK_CONTRACT.to_string(), + Box::new(MockTimelockQueries { + owner: MOCK_DAO_CORE.to_string(), + subdao: MOCK_SUBDAO_CORE.to_string(), + }), + ); + contracts.insert( + MOCK_SUBDAO_CORE.to_string(), + Box::new(MockSubdaoCoreQueries { + proposal_module: MOCK_SUBDAO_PROPOSE_MODULE.to_string(), + dao_core: MOCK_DAO_CORE.to_string(), + }), + ); + contracts.insert( + MOCK_SUBDAO_PROPOSE_MODULE.to_string(), + Box::new(MockSubdaoProposalQueries { + pre_propose: MOCK_SUBDAO_PREPROPOSE_MODULE.to_string(), + }), + ); + contracts.insert( + MOCK_SUBDAO_PREPROPOSE_MODULE.to_string(), + Box::new(MockSubaoPreProposalQueries { + timelock: MOCK_TIMELOCK_CONTRACT.to_string(), + }), + ); + contracts +} + +pub fn get_dao_with_impostor_timelock() -> HashMap> { + let mut contracts: HashMap> = get_properly_initialized_dao(); + // impostor timelock is the same as regular one but the subdao doesn't point to it + contracts.insert( + MOCK_IMPOSTOR_TIMELOCK_CONTRACT.to_string(), + Box::new(MockTimelockQueries { + owner: MOCK_DAO_CORE.to_string(), + subdao: MOCK_SUBDAO_CORE.to_string(), + }), + ); + contracts +} + +pub fn get_dao_with_impostor_subdao() -> HashMap> { + let mut contracts: HashMap> = get_properly_initialized_dao(); + // subdao becomes impostor if it is not in the dao's list, so let's just make it empty + contracts.remove(&MOCK_DAO_CORE.to_string()); + contracts.insert( + MOCK_DAO_CORE.to_string(), + Box::new(MockDaoQueries { + sub_dao_set: HashSet::from([]), + }), + ); + contracts +} diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/testing/tests.rs b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/testing/tests.rs index 619466a0..6fdd5236 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/testing/tests.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-single-overrule/src/testing/tests.rs @@ -1,31 +1,40 @@ use cosmwasm_std::{ from_binary, testing::{mock_env, mock_info}, - to_binary, CosmosMsg, DepsMut, Empty, SubMsg, WasmMsg, + to_binary, Addr, CosmosMsg, DepsMut, Empty, SubMsg, WasmMsg, }; +use std::collections::HashMap; use crate::{ - contract::{ - execute, instantiate, query, ExecuteMsg, InstantiateMsg, ProposeMessage, - ProposeMessageInternal, QueryMsg, TimelockExecuteMsg, - }, - testing::mock_querier::{mock_dependencies, MOCK_PROPOSE_MODULE, MOCK_TIMELOCK_CONTRACT}, + contract::{execute, instantiate, query}, + testing::mock_querier::{mock_dependencies, MOCK_DAO_CORE, MOCK_TIMELOCK_CONTRACT}, +}; +use neutron_dao_pre_propose_overrule::msg::{ + ExecuteMsg, InstantiateMsg, ProposeMessage, QueryExt, QueryMsg, }; use crate::error::PreProposeOverruleError; +use crate::testing::mock_querier::{ + get_dao_with_impostor_subdao, get_dao_with_impostor_timelock, get_properly_initialized_dao, + ContractQuerier, MOCK_DAO_PROPOSE_MODULE, MOCK_IMPOSTOR_TIMELOCK_CONTRACT, MOCK_SUBDAO_CORE, + NON_TIMELOCKED_PROPOSAL_ID, PROPOSALS_COUNT, SUBDAO_NAME, TIMELOCKED_PROPOSAL_ID, +}; use cwd_pre_propose_base::state::Config; +use cwd_proposal_single::msg::ExecuteMsg as ProposeMessageInternal; +use neutron_subdao_timelock_single::msg as TimelockMsg; pub fn init_base_contract(deps: DepsMut) { let msg = InstantiateMsg {}; - let info = mock_info(MOCK_PROPOSE_MODULE, &[]); + let info = mock_info(MOCK_DAO_PROPOSE_MODULE, &[]); instantiate(deps, mock_env(), info, msg).unwrap(); } #[test] fn test_create_overrule_proposal() { - let mut deps = mock_dependencies(); + let contracts: HashMap> = get_properly_initialized_dao(); + let mut deps = mock_dependencies(contracts); init_base_contract(deps.as_mut()); - const PROPOSAL_ID: u64 = 47; + const PROPOSAL_ID: u64 = TIMELOCKED_PROPOSAL_ID; const PROPOSER_ADDR: &str = "whatever"; let msg = ExecuteMsg::Propose { msg: ProposeMessage::ProposeOverrule { @@ -40,16 +49,25 @@ fn test_create_overrule_proposal() { msg, ); assert!(res.is_ok()); + let prop_name: String = format!( + "Reject the proposal #{} of the '{}' subdao", + PROPOSAL_ID, SUBDAO_NAME + ); + let prop_desc: String = format!( + "If this proposal will be accepted, the DAO is going to \ +overrule the proposal #{} of '{}' subdao (address {})", + PROPOSAL_ID, SUBDAO_NAME, MOCK_SUBDAO_CORE + ); assert_eq!( res.unwrap().messages, vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: MOCK_PROPOSE_MODULE.to_string(), + contract_addr: MOCK_DAO_PROPOSE_MODULE.to_string(), msg: to_binary(&ProposeMessageInternal::Propose { - title: "Overrule proposal".to_string(), - description: "Reject the decision made by subdao".to_string(), + title: prop_name, + description: prop_desc, msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: MOCK_TIMELOCK_CONTRACT.to_string(), - msg: to_binary(&TimelockExecuteMsg::OverruleProposal { + msg: to_binary(&TimelockMsg::ExecuteMsg::OverruleProposal { proposal_id: PROPOSAL_ID }) .unwrap(), @@ -64,22 +82,74 @@ fn test_create_overrule_proposal() { } #[test] -fn test_query_config() { - let mut deps = mock_dependencies(); +fn test_base_queries() { + let contracts: HashMap> = get_properly_initialized_dao(); + let mut deps = mock_dependencies(contracts); init_base_contract(deps.as_mut()); - let query_msg = QueryMsg::Config {}; - let res = query(deps.as_ref(), mock_env(), query_msg).unwrap(); - let queried_prop = from_binary(&res).unwrap(); - let expected_prop = Config { + + let res_config = query(deps.as_ref(), mock_env(), QueryMsg::Config {}).unwrap(); + let queried_config = from_binary(&res_config).unwrap(); + let expected_config = Config { deposit_info: None, open_proposal_submission: true, }; - assert_eq!(expected_prop, queried_prop); + assert_eq!(expected_config, queried_config); + + let res_dao = query(deps.as_ref(), mock_env(), QueryMsg::Dao {}).unwrap(); + let queried_dao: Addr = from_binary(&res_dao).unwrap(); + let expected_dao = Addr::unchecked(MOCK_DAO_CORE); + assert_eq!(expected_dao, queried_dao); + + let res_proposal_module = + query(deps.as_ref(), mock_env(), QueryMsg::ProposalModule {}).unwrap(); + let queried_proposal_module: Addr = from_binary(&res_proposal_module).unwrap(); + let expected_proposal_module = Addr::unchecked(MOCK_DAO_PROPOSE_MODULE); + assert_eq!(expected_proposal_module, queried_proposal_module); + + assert_eq!(expected_config, queried_config); +} + +#[test] +fn test_proposal_id_query() { + let contracts: HashMap> = get_properly_initialized_dao(); + let mut deps = mock_dependencies(contracts); + init_base_contract(deps.as_mut()); + const PROPOSAL_ID: u64 = TIMELOCKED_PROPOSAL_ID; + const PROPOSER_ADDR: &str = "whatever"; + let msg = ExecuteMsg::Propose { + msg: ProposeMessage::ProposeOverrule { + timelock_contract: MOCK_TIMELOCK_CONTRACT.to_string(), + proposal_id: PROPOSAL_ID, + }, + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PROPOSER_ADDR, &[]), + msg, + ); + assert!(res.is_ok()); + + let res_id = query( + deps.as_ref(), + mock_env(), + QueryMsg::QueryExtension { + msg: QueryExt::OverruleProposalId { + subdao_proposal_id: PROPOSAL_ID, + timelock_address: MOCK_TIMELOCK_CONTRACT.to_string(), + }, + }, + ) + .unwrap(); + let queried_id: u64 = from_binary(&res_id).unwrap(); + let expected_id = PROPOSALS_COUNT + 1; + assert_eq!(expected_id, queried_id); } #[test] fn test_base_prepropose_methods() { - let mut deps = mock_dependencies(); + let contracts: HashMap> = get_properly_initialized_dao(); + let mut deps = mock_dependencies(contracts); init_base_contract(deps.as_mut()); const PROPOSER_ADDR: &str = "whatever"; let msg = ExecuteMsg::UpdateConfig { @@ -93,8 +163,112 @@ fn test_base_prepropose_methods() { msg, ); assert!(res.is_err()); + assert_eq!(res, Err(PreProposeOverruleError::MessageUnsupported {})) +} + +#[test] +fn test_impostor_subdao() { + // test where timelock contract points to subdao that doesn't points to this timelock + let contracts: HashMap> = get_dao_with_impostor_subdao(); + let mut deps = mock_dependencies(contracts); + init_base_contract(deps.as_mut()); + const PROPOSAL_ID: u64 = TIMELOCKED_PROPOSAL_ID; + const PROPOSER_ADDR: &str = "whatever"; + let msg = ExecuteMsg::Propose { + msg: ProposeMessage::ProposeOverrule { + timelock_contract: MOCK_TIMELOCK_CONTRACT.to_string(), + proposal_id: PROPOSAL_ID, + }, + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PROPOSER_ADDR, &[]), + msg, + ); + assert!(res.is_err()); + assert_eq!(res, Err(PreProposeOverruleError::ForbiddenSubdao {})); +} + +#[test] +fn test_impostor_timelock() { + // test where timelock contract points to subdao that doesn't points to this timelock + let contracts: HashMap> = get_dao_with_impostor_timelock(); + let mut deps = mock_dependencies(contracts); + init_base_contract(deps.as_mut()); + const PROPOSAL_ID: u64 = TIMELOCKED_PROPOSAL_ID; + const PROPOSER_ADDR: &str = "whatever"; + let msg = ExecuteMsg::Propose { + msg: ProposeMessage::ProposeOverrule { + timelock_contract: MOCK_IMPOSTOR_TIMELOCK_CONTRACT.to_string(), + proposal_id: PROPOSAL_ID, + }, + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PROPOSER_ADDR, &[]), + msg, + ); + assert!(res.is_err()); + assert_eq!(res, Err(PreProposeOverruleError::SubdaoMisconfigured {})); +} + +#[test] +fn test_proposal_is_not_timelocked() { + // test where the proposal we're to create overrule for isn't timelocked already/yet + let contracts: HashMap> = get_properly_initialized_dao(); + let mut deps = mock_dependencies(contracts); + init_base_contract(deps.as_mut()); + const PROPOSAL_ID: u64 = NON_TIMELOCKED_PROPOSAL_ID; + const PROPOSER_ADDR: &str = "whatever"; + let msg = ExecuteMsg::Propose { + msg: ProposeMessage::ProposeOverrule { + timelock_contract: MOCK_TIMELOCK_CONTRACT.to_string(), + proposal_id: PROPOSAL_ID, + }, + }; + let res = execute( + deps.as_mut(), + mock_env(), + mock_info(PROPOSER_ADDR, &[]), + msg, + ); + assert!(res.is_err()); + assert_eq!(res, Err(PreProposeOverruleError::ProposalWrongState {})); +} + +#[test] +fn test_double_creation() { + let contracts: HashMap> = get_properly_initialized_dao(); + let mut deps = mock_dependencies(contracts); + init_base_contract(deps.as_mut()); + const PROPOSAL_ID: u64 = TIMELOCKED_PROPOSAL_ID; + const PROPOSER_ADDR: &str = "whatever"; + let msg = ExecuteMsg::Propose { + msg: ProposeMessage::ProposeOverrule { + timelock_contract: MOCK_TIMELOCK_CONTRACT.to_string(), + proposal_id: PROPOSAL_ID, + }, + }; + let res_ok = execute( + deps.as_mut(), + mock_env(), + mock_info(PROPOSER_ADDR, &[]), + msg.clone(), + ); + assert!(res_ok.is_ok()); + let res_not_ok = execute( + deps.as_mut(), + mock_env(), + mock_info(PROPOSER_ADDR, &[]), + msg, + ); + assert!(res_not_ok.is_err()); assert_eq!( - res.err().unwrap(), - PreProposeOverruleError::MessageUnsupported {} - ) + res_not_ok, + Err(PreProposeOverruleError::AlreadyExists { + id: PROPOSALS_COUNT + 1 + }) + ); } diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single/examples/schema.rs b/contracts/dao/pre-propose/cwd-pre-propose-single/examples/schema.rs index c269e8ca..a9526b1a 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single/examples/schema.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-single/examples/schema.rs @@ -1,3 +1,4 @@ +use cosmwasm_std::Empty; use std::env::current_dir; use std::fs::create_dir_all; @@ -14,7 +15,7 @@ fn main() { export_schema(&schema_for!(InstantiateMsg), &out_dir); export_schema(&schema_for!(ExecuteMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(DepositInfoResponse), &out_dir); export_schema_with_title(&schema_for!(Addr), &out_dir, "ProposalModuleResponse"); diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single/schema/query_msg.json b/contracts/dao/pre-propose/cwd-pre-propose-single/schema/query_msg_for__empty.json similarity index 60% rename from contracts/dao/pre-propose/cwd-pre-propose-single/schema/query_msg.json rename to contracts/dao/pre-propose/cwd-pre-propose-single/schema/query_msg_for__empty.json index a057bb1e..f481ed19 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single/schema/query_msg.json +++ b/contracts/dao/pre-propose/cwd-pre-propose-single/schema/query_msg_for__empty.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", + "title": "QueryMsg_for_Empty", "oneOf": [ { "description": "Gets the proposal module that this pre propose module is associated with. Returns `Addr`.", @@ -63,6 +63,33 @@ } }, "additionalProperties": false + }, + { + "description": "Extension for queries. The default implementation will do nothing if queried for will return `Binary::default()`.", + "type": "object", + "required": [ + "query_extension" + ], + "properties": { + "query_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/Empty" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Empty": { + "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)", + "type": "object" } - ] + } } diff --git a/contracts/dao/pre-propose/cwd-pre-propose-single/src/contract.rs b/contracts/dao/pre-propose/cwd-pre-propose-single/src/contract.rs index c791bdde..b0495232 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-single/src/contract.rs +++ b/contracts/dao/pre-propose/cwd-pre-propose-single/src/contract.rs @@ -1,6 +1,8 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; -use cosmwasm_std::{Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cosmwasm_std::{ + Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, +}; use cw2::set_contract_version; use neutron_bindings::bindings::msg::NeutronMsg; @@ -27,7 +29,7 @@ pub enum ProposeMessage { pub type InstantiateMsg = InstantiateBase; pub type ExecuteMsg = ExecuteBase; -pub type QueryMsg = QueryBase; +pub type QueryMsg = QueryBase; /// Internal version of the propose message that includes the /// `proposer` field. The module will fill this in based on the sender @@ -43,7 +45,7 @@ enum ProposeMessageInternal { }, } -type PrePropose = PreProposeContract; +type PrePropose = PreProposeContract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( diff --git a/contracts/dao/proposal/cwd-proposal-single/src/testing/tests.rs b/contracts/dao/proposal/cwd-proposal-single/src/testing/tests.rs index 4090a6f5..c2ede878 100644 --- a/contracts/dao/proposal/cwd-proposal-single/src/testing/tests.rs +++ b/contracts/dao/proposal/cwd-proposal-single/src/testing/tests.rs @@ -2,13 +2,15 @@ use cosmwasm_std::{ coins, testing::{mock_dependencies, mock_env}, to_binary, Addr, Attribute, BankMsg, ContractInfoResponse, CosmosMsg, Decimal, Empty, Reply, - StdError, SubMsgResult, Uint128, WasmMsg, WasmQuery, + StdError, StdResult, SubMsgResult, Uint128, WasmMsg, WasmQuery, }; use cosmwasm_std::{Api, Storage}; use cw2::ContractVersion; use cw20::Cw20Coin; use cw_multi_test::{custom_app, BasicApp, Executor, Router}; use cw_utils::Duration; +use cwd_core::msg::{ExecuteMsg as DaoExecuteMsg, QueryMsg as DaoQueryMsg}; +use cwd_core::query::SubDao; use cwd_hooks::{HookError, HooksResponse}; use cwd_interface::voting::InfoResponse; use cwd_voting::{ @@ -1358,6 +1360,65 @@ fn test_pre_propose_admin_is_dao() { assert_eq!(info.admin, Some(core_addr.into_string())); } +#[test] +fn test_subdao_queries() { + let mut app = custom_app::(no_init); + let instantiate = get_proposal_module_instantiate(&mut app); + let core_addr = instantiate_with_native_bonded_balances_governance(&mut app, instantiate, None); + + let subdao_addr = Addr::unchecked("subdao"); + let res: StdResult = app.wrap().query_wasm_smart( + core_addr.clone(), + &DaoQueryMsg::GetSubDao { + address: subdao_addr.to_string(), + }, + ); + assert!(res.is_err()); + let res: Vec = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &DaoQueryMsg::ListSubDaos { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(res.len(), 0); + + let res = app.execute_contract( + core_addr.clone(), + core_addr.clone(), + &DaoExecuteMsg::UpdateSubDaos { + to_add: vec![SubDao { + addr: subdao_addr.to_string(), + charter: None, + }], + to_remove: vec![], + }, + &[], + ); + assert!(res.is_ok()); + let res: StdResult = app.wrap().query_wasm_smart( + core_addr.clone(), + &DaoQueryMsg::GetSubDao { + address: subdao_addr.to_string(), + }, + ); + assert!(res.is_ok()); + let res: Vec = app + .wrap() + .query_wasm_smart( + core_addr, + &DaoQueryMsg::ListSubDaos { + start_after: None, + limit: None, + }, + ) + .unwrap(); + assert_eq!(res.len(), 1); +} + // TODO: test pre-propose module that fails on new proposal hook (ugh). // - What happens if you have proposals that can not be executed but diff --git a/contracts/subdaos/cwd-subdao-core/src/contract.rs b/contracts/subdaos/cwd-subdao-core/src/contract.rs index bfcc319e..ba5d2386 100644 --- a/contracts/subdaos/cwd-subdao-core/src/contract.rs +++ b/contracts/subdaos/cwd-subdao-core/src/contract.rs @@ -17,6 +17,7 @@ use neutron_subdao_core::msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateM use neutron_subdao_core::types::{ Config, DumpStateResponse, GetItemResponse, ProposalModule, ProposalModuleStatus, SubDao, }; +use neutron_subdao_pre_propose_single::msg::QueryExt as PreProposeQueryExt; use neutron_subdao_pre_propose_single::msg::QueryMsg as PreProposeQueryMsg; use neutron_subdao_proposal_single::msg::QueryMsg as ProposeQueryMsg; @@ -630,9 +631,12 @@ pub(crate) fn execution_access_check(deps: Deps, sender: Addr) -> Result<(), Con &ProposeQueryMsg::ProposalCreationPolicy {}, )?; if let ProposalCreationPolicy::Module { addr } = policy { - let timelock_contract: Addr = deps - .querier - .query_wasm_smart(&addr, &PreProposeQueryMsg::TimelockAddress {})?; + let timelock_contract: Addr = deps.querier.query_wasm_smart( + &addr, + &PreProposeQueryMsg::QueryExtension { + msg: PreProposeQueryExt::TimelockAddress {}, + }, + )?; if sender == timelock_contract { return Ok(()); } diff --git a/contracts/subdaos/cwd-subdao-timelock-single/Cargo.toml b/contracts/subdaos/cwd-subdao-timelock-single/Cargo.toml index d43a887e..3cd7fad0 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/Cargo.toml +++ b/contracts/subdaos/cwd-subdao-timelock-single/Cargo.toml @@ -21,7 +21,6 @@ cosmwasm-std = { version = "1.0.0" } cosmwasm-storage = { version = "1.0.0" } cw-storage-plus = "0.13" cw2 = "0.13" -cw-utils = "0.13" cw-controllers = "0.13" schemars = "0.8" serde = { version = "1.0.147", default-features = false, features = ["derive"] } @@ -32,8 +31,12 @@ neutron-subdao-pre-propose-single = { path = "../../../packages/neutron-subdao-p neutron-subdao-timelock-single = { path = "../../../packages/neutron-subdao-timelock-single" } cwd-pre-propose-base = { path = "../../../packages/cwd-pre-propose-base" } neutron-subdao-core = { path = "../../../packages/neutron-subdao-core" } +neutron-dao-pre-propose-overrule = { path = "../../../packages/neutron-dao-pre-propose-overrule" } +cwd-proposal-single = { path = "../../../contracts/dao/proposal/cwd-proposal-single", features = ["library"]} +cwd-voting = { path = "../../../packages/cwd-voting" } [dev-dependencies] cosmwasm-schema = { version = "1.0.0" } cw-multi-test = "0.13" anyhow = "1.0.57" +cw-utils = "0.13" diff --git a/contracts/subdaos/cwd-subdao-timelock-single/schema/config_response.json b/contracts/subdaos/cwd-subdao-timelock-single/schema/config_response.json index 42f06433..4e71b722 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/schema/config_response.json +++ b/contracts/subdaos/cwd-subdao-timelock-single/schema/config_response.json @@ -3,21 +3,19 @@ "title": "ConfigResponse", "type": "object", "required": [ + "overrule_pre_propose", "owner", - "subdao", - "timelock_duration" + "subdao" ], "properties": { + "overrule_pre_propose": { + "$ref": "#/definitions/Addr" + }, "owner": { "$ref": "#/definitions/Addr" }, "subdao": { "$ref": "#/definitions/Addr" - }, - "timelock_duration": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 } }, "definitions": { diff --git a/contracts/subdaos/cwd-subdao-timelock-single/schema/execute_msg.json b/contracts/subdaos/cwd-subdao-timelock-single/schema/execute_msg.json index 8267fdbd..5d4201c2 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/schema/execute_msg.json +++ b/contracts/subdaos/cwd-subdao-timelock-single/schema/execute_msg.json @@ -84,19 +84,17 @@ "update_config": { "type": "object", "properties": { - "owner": { + "overrule_pre_propose": { "type": [ "string", "null" ] }, - "timelock_duration": { + "owner": { "type": [ - "integer", + "string", "null" - ], - "format": "uint64", - "minimum": 0.0 + ] } } } diff --git a/contracts/subdaos/cwd-subdao-timelock-single/schema/instantiate_msg.json b/contracts/subdaos/cwd-subdao-timelock-single/schema/instantiate_msg.json index 1f434048..cb81f9a9 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/schema/instantiate_msg.json +++ b/contracts/subdaos/cwd-subdao-timelock-single/schema/instantiate_msg.json @@ -3,13 +3,11 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "timelock_duration" + "overrule_pre_propose" ], "properties": { - "timelock_duration": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "overrule_pre_propose": { + "type": "string" } } } diff --git a/contracts/subdaos/cwd-subdao-timelock-single/src/contract.rs b/contracts/subdaos/cwd-subdao-timelock-single/src/contract.rs index 343fcdcd..030d7c04 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/src/contract.rs +++ b/contracts/subdaos/cwd-subdao-timelock-single/src/contract.rs @@ -2,17 +2,25 @@ use cosmwasm_std::entry_point; use cosmwasm_std::{ to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response, - StdResult, SubMsg, + StdResult, SubMsg, WasmMsg, }; use cw2::set_contract_version; use cw_storage_plus::Bound; -use cwd_pre_propose_base::msg::QueryMsg as PreProposeQueryBase; +use cwd_proposal_single::{ + msg::QueryMsg as MainDaoProposalModuleQueryMsg, + query::ProposalResponse as MainDaoProposalResponse, +}; +use cwd_voting::status::Status; use neutron_bindings::bindings::msg::NeutronMsg; +use neutron_dao_pre_propose_overrule::msg::{ + ExecuteMsg as OverruleExecuteMsg, ProposeMessage as OverruleProposeMessage, + QueryExt as OverruleQueryExt, QueryMsg as OverruleQueryMsg, +}; use neutron_subdao_core::msg::QueryMsg as SubdaoQuery; use neutron_subdao_pre_propose_single::msg::QueryMsg as PreProposeQuery; -use neutron_subdao_timelock_single::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use neutron_subdao_timelock_single::types::{ - Config, ProposalListResponse, ProposalStatus, SingleChoiceProposal, +use neutron_subdao_timelock_single::{ + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + types::{Config, ProposalListResponse, ProposalStatus, SingleChoiceProposal}, }; use crate::error::ContractError; @@ -32,16 +40,22 @@ pub fn instantiate( let subdao_core: Addr = deps.querier.query_wasm_smart( info.sender, // sender is meant to be the pre-propose module - &PreProposeQuery::QueryBase(PreProposeQueryBase::Dao {}), + &PreProposeQuery::Dao {}, )?; let main_dao: Addr = deps .querier .query_wasm_smart(subdao_core.clone(), &SubdaoQuery::MainDao {})?; + // We don't validate overrule pre propose address more than just as address. + // We could also query the DAO address of this module and check if it matches the main DAO set + // in the config. But we don't do that because it would require for the subdao to know much more + // about the main DAO than it should IMO. It also makes testing harder. + let overrule_pre_propose = deps.api.addr_validate(&msg.overrule_pre_propose)?; + let config = Config { owner: main_dao, - timelock_duration: msg.timelock_duration, + overrule_pre_propose, subdao: subdao_core, }; @@ -50,7 +64,10 @@ pub fn instantiate( Ok(Response::new() .add_attribute("action", "instantiate") .add_attribute("owner", config.owner) - .add_attribute("timelock_duration", config.timelock_duration.to_string())) + .add_attribute( + "overrule_pre_propose", + config.overrule_pre_propose.to_string(), + )) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -72,8 +89,8 @@ pub fn execute( } ExecuteMsg::UpdateConfig { owner, - timelock_duration, - } => execute_update_config(deps, info, owner, timelock_duration), + overrule_pre_propose, + } => execute_update_config(deps, info, owner, overrule_pre_propose), } } @@ -93,13 +110,26 @@ pub fn execute_timelock_proposal( let proposal = SingleChoiceProposal { id: proposal_id, msgs, - timelock_ts: env.block.time, status: ProposalStatus::Timelocked, }; PROPOSALS.save(deps.storage, proposal_id, &proposal)?; + let create_overrule_proposal = WasmMsg::Execute { + contract_addr: config.overrule_pre_propose.to_string(), + msg: to_binary(&OverruleExecuteMsg::Propose { + msg: OverruleProposeMessage::ProposeOverrule { + timelock_contract: env.contract.address.to_string(), + proposal_id, + }, + })?, + funds: vec![], + }; + + // NOTE: we don't handle an error that might appear during overrule proposal creation. + // Thus, we expect for proposal to get execution error status on proposal module level. Ok(Response::default() + .add_message(create_overrule_proposal) .add_attribute("action", "timelock_proposal") .add_attribute("sender", info.sender) .add_attribute("proposal_id", proposal_id.to_string()) @@ -123,8 +153,7 @@ pub fn execute_execute_proposal( }); } - // Check if timelock has passed - if env.block.time.seconds() < (config.timelock_duration + proposal.timelock_ts.seconds()) { + if !is_overrule_proposal_rejected(&deps, &env, &config.overrule_pre_propose, proposal.id)? { return Err(ContractError::TimeLocked {}); } @@ -182,7 +211,7 @@ pub fn execute_update_config( deps: DepsMut, info: MessageInfo, new_owner: Option, - new_timelock_duration: Option, + new_overrule_pre_propose: Option, ) -> Result, ContractError> { let mut config: Config = CONFIG.load(deps.storage)?; if info.sender != config.owner { @@ -197,15 +226,18 @@ pub fn execute_update_config( config.owner = owner; } - if let Some(timelock_duration) = new_timelock_duration { - config.timelock_duration = timelock_duration; + if let Some(overrule_pre_propose) = new_overrule_pre_propose { + config.overrule_pre_propose = deps.api.addr_validate(&overrule_pre_propose)?; } CONFIG.save(deps.storage, &config)?; Ok(Response::new() .add_attribute("action", "update_config") .add_attribute("owner", config.owner) - .add_attribute("timelock_duration", config.timelock_duration.to_string())) + .add_attribute( + "overrule_pre_propose", + config.overrule_pre_propose.to_string(), + )) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -249,6 +281,33 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result Result { + let overrule_proposal_id: u64 = deps.querier.query_wasm_smart( + overrule_pre_propose, + &OverruleQueryMsg::QueryExtension { + msg: OverruleQueryExt::OverruleProposalId { + timelock_address: env.contract.address.to_string(), + subdao_proposal_id, + }, + }, + )?; + let propose: Addr = deps + .querier + .query_wasm_smart(overrule_pre_propose, &OverruleQueryMsg::ProposalModule {})?; + let overrule_proposal: MainDaoProposalResponse = deps.querier.query_wasm_smart( + propose, + &MainDaoProposalModuleQueryMsg::Proposal { + proposal_id: overrule_proposal_id, + }, + )?; + Ok(overrule_proposal.proposal.status == Status::Rejected) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { let proposal_id = msg.id; diff --git a/contracts/subdaos/cwd-subdao-timelock-single/src/error.rs b/contracts/subdaos/cwd-subdao-timelock-single/src/error.rs index 404644cc..7d508820 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/src/error.rs +++ b/contracts/subdaos/cwd-subdao-timelock-single/src/error.rs @@ -20,4 +20,7 @@ pub enum ContractError { #[error("no such proposal ({id})")] NoSuchProposal { id: u64 }, + + #[error("Can not create overrule proposal for main DAO")] + CantCreateOverrule {}, } diff --git a/contracts/subdaos/cwd-subdao-timelock-single/src/testing/mock_querier.rs b/contracts/subdaos/cwd-subdao-timelock-single/src/testing/mock_querier.rs index ac5b2d53..a54dd8e3 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/src/testing/mock_querier.rs +++ b/contracts/subdaos/cwd-subdao-timelock-single/src/testing/mock_querier.rs @@ -1,23 +1,44 @@ +use std::cell::RefCell; use std::marker::PhantomData; +use std::rc::Rc; use cosmwasm_std::{ from_binary, from_slice, testing::{MockApi, MockQuerier, MockStorage}, - to_binary, ContractResult, Empty, OwnedDeps, Querier, QuerierResult, QueryRequest, SystemError, - SystemResult, WasmQuery, + to_binary, Addr, ContractResult, Empty, OwnedDeps, Querier, QuerierResult, QueryRequest, + SystemError, SystemResult, Uint128, WasmQuery, +}; +use cw_utils::Duration; +use cwd_proposal_single::{ + msg::{QueryMsg as ProposeQuery, QueryMsg}, + proposal::SingleChoiceProposal as MainDaoSingleChoiceProposal, + query::ProposalResponse as MainDaoProposalResponse, + state::Config as OverrulProposalConfig, +}; +use cwd_voting::status::Status; +use cwd_voting::threshold::Threshold; +use cwd_voting::voting::Votes; +use neutron_dao_pre_propose_overrule::msg::{ + QueryExt as PreProposeOverruleQueryExt, QueryMsg as PreProposeOverruleQuery, +}; +use neutron_subdao_pre_propose_single::msg::{ + QueryExt as PreProposeQueryExt, QueryMsg as PreProposeQuery, }; -use cwd_pre_propose_base::msg::QueryMsg as PreProposeQueryBase; -use neutron_subdao_pre_propose_single::msg::QueryMsg as PreProposeQuery; pub const MOCK_SUBDAO_CORE_ADDR: &str = "neutron1subdao_core_contract"; pub const MOCK_TIMELOCK_INITIALIZER: &str = "neutron1timelock_initializer"; pub const MOCK_MAIN_DAO_ADDR: &str = "neutron1main_dao_core_contract"; +pub const MOCK_OVERRULE_PROPOSAL: &str = "neutron1main_dao_overrule_proposal"; +pub const MOCK_OVERRULE_PREPROPOSAL: &str = "neutron1main_dao_overrule_preproposal"; -pub fn mock_dependencies() -> OwnedDeps { - let custom_querier = WasmMockQuerier::new(MockQuerier::new(&[])); +pub fn mock_dependencies( + x: Rc>, +) -> OwnedDeps { + let custom_storage = MockStorage::default(); + let custom_querier = WasmMockQuerier::new(MockQuerier::new(&[]), x); OwnedDeps { - storage: MockStorage::default(), + storage: custom_storage, api: MockApi::default(), querier: custom_querier, custom_query_type: PhantomData, @@ -26,6 +47,7 @@ pub fn mock_dependencies() -> OwnedDeps { pub struct WasmMockQuerier { base: MockQuerier, + overrule_proposal_status: Rc>, } impl Querier for WasmMockQuerier { @@ -50,17 +72,15 @@ impl WasmMockQuerier { if contract_addr == MOCK_TIMELOCK_INITIALIZER { let q: PreProposeQuery = from_binary(msg).unwrap(); let addr = match q { - PreProposeQuery::QueryBase(PreProposeQueryBase::ProposalModule {}) => { + PreProposeQuery::ProposalModule {} => { todo!() } - PreProposeQuery::QueryBase(PreProposeQueryBase::Dao {}) => { - MOCK_SUBDAO_CORE_ADDR - } - PreProposeQuery::QueryBase(PreProposeQueryBase::Config {}) => todo!(), - PreProposeQuery::QueryBase(PreProposeQueryBase::DepositInfo { - proposal_id: _, - }) => todo!(), - PreProposeQuery::TimelockAddress {} => todo!(), + PreProposeQuery::Dao {} => MOCK_SUBDAO_CORE_ADDR, + PreProposeQuery::Config {} => todo!(), + PreProposeQuery::DepositInfo { proposal_id: _ } => todo!(), + PreProposeQuery::QueryExtension { + msg: PreProposeQueryExt::TimelockAddress {}, + } => todo!(), }; return SystemResult::Ok(ContractResult::from(to_binary(addr))); } @@ -68,6 +88,72 @@ impl WasmMockQuerier { let addr = { MOCK_MAIN_DAO_ADDR }; return SystemResult::Ok(ContractResult::from(to_binary(addr))); } + if contract_addr == MOCK_OVERRULE_PREPROPOSAL { + let q: PreProposeOverruleQuery = from_binary(msg).unwrap(); + let reply = match q { + PreProposeOverruleQuery::ProposalModule {} => { + to_binary(&MOCK_OVERRULE_PROPOSAL.to_string()) + } + PreProposeOverruleQuery::Dao {} => { + to_binary(&MOCK_MAIN_DAO_ADDR.to_string()) + } + PreProposeOverruleQuery::Config {} => todo!(), + PreProposeOverruleQuery::DepositInfo { proposal_id: _ } => todo!(), + PreProposeOverruleQuery::QueryExtension { + msg: PreProposeOverruleQueryExt::OverruleProposalId { .. }, + } => to_binary(&1), + }; + return SystemResult::Ok(ContractResult::from(reply)); + } + if contract_addr == MOCK_OVERRULE_PROPOSAL { + let q: ProposeQuery = from_binary(msg).unwrap(); + let reply = match q { + QueryMsg::Config {} => to_binary(&OverrulProposalConfig { + threshold: Threshold::AbsoluteCount { + threshold: Default::default(), + }, + max_voting_period: Duration::Time(10), + min_voting_period: None, + allow_revoting: false, + dao: Addr::unchecked(MOCK_MAIN_DAO_ADDR), + close_proposal_on_execution_failure: false, + }), + QueryMsg::Proposal { .. } => to_binary(&MainDaoProposalResponse { + id: 1, + proposal: MainDaoSingleChoiceProposal { + title: "".to_string(), + description: "".to_string(), + proposer: Addr::unchecked(""), + start_height: 0, + min_voting_period: None, + expiration: Default::default(), + threshold: Threshold::AbsoluteCount { + threshold: Uint128::new(1), + }, + total_power: Default::default(), + msgs: vec![], + // status: Status::Rejected, + status: *(*self.overrule_proposal_status).borrow(), + votes: Votes { + yes: Default::default(), + no: Default::default(), + abstain: Default::default(), + }, + allow_revoting: false, + }, + }), + QueryMsg::ListProposals { .. } => todo!(), + QueryMsg::ReverseProposals { .. } => todo!(), + QueryMsg::ProposalCount { .. } => todo!(), + QueryMsg::GetVote { .. } => todo!(), + QueryMsg::ListVotes { .. } => todo!(), + QueryMsg::ProposalCreationPolicy { .. } => todo!(), + QueryMsg::ProposalHooks { .. } => todo!(), + QueryMsg::VoteHooks { .. } => todo!(), + _ => todo!(), + }; + return SystemResult::Ok(ContractResult::from(reply)); + } SystemResult::Err(SystemError::NoSuchContract { addr: contract_addr.to_string(), }) @@ -78,7 +164,10 @@ impl WasmMockQuerier { } impl WasmMockQuerier { - pub fn new(base: MockQuerier) -> Self { - WasmMockQuerier { base } + pub fn new(base: MockQuerier, x: Rc>) -> WasmMockQuerier { + WasmMockQuerier { + base, + overrule_proposal_status: x, + } } } diff --git a/contracts/subdaos/cwd-subdao-timelock-single/src/testing/tests.rs b/contracts/subdaos/cwd-subdao-timelock-single/src/testing/tests.rs index d941accf..84b7090c 100644 --- a/contracts/subdaos/cwd-subdao-timelock-single/src/testing/tests.rs +++ b/contracts/subdaos/cwd-subdao-timelock-single/src/testing/tests.rs @@ -1,30 +1,39 @@ +use cosmwasm_std::testing::MOCK_CONTRACT_ADDR; use cosmwasm_std::{ from_binary, testing::{mock_env, mock_info}, - Addr, Attribute, Reply, SubMsg, SubMsgResult, + to_binary, Addr, Attribute, CosmosMsg, Reply, SubMsg, SubMsgResult, WasmMsg, }; +use cwd_voting::status::Status; use neutron_bindings::bindings::msg::NeutronMsg; -use neutron_subdao_timelock_single::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; -use neutron_subdao_timelock_single::types::{ - Config, ProposalListResponse, ProposalStatus, SingleChoiceProposal, +use neutron_subdao_timelock_single::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + types::{Config, ProposalListResponse, ProposalStatus, SingleChoiceProposal}, }; -use crate::testing::mock_querier::MOCK_MAIN_DAO_ADDR; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::testing::mock_querier::{MOCK_MAIN_DAO_ADDR, MOCK_OVERRULE_PREPROPOSAL}; use crate::{ contract::{execute, instantiate, query, reply}, state::{CONFIG, DEFAULT_LIMIT, PROPOSALS}, testing::mock_querier::MOCK_TIMELOCK_INITIALIZER, }; +use neutron_dao_pre_propose_overrule::msg::{ + ExecuteMsg as OverruleExecuteMsg, ProposeMessage as OverruleProposeMessage, +}; use super::mock_querier::{mock_dependencies, MOCK_SUBDAO_CORE_ADDR}; #[test] fn test_instantiate_test() { - let mut deps = mock_dependencies(); + let overrule_proposal_status = Rc::new(RefCell::new(Status::Open)); + let mut deps = mock_dependencies(Rc::clone(&overrule_proposal_status)); let env = mock_env(); let info = mock_info("neutron1unknownsender", &[]); let msg = InstantiateMsg { - timelock_duration: 10, + overrule_pre_propose: MOCK_OVERRULE_PREPROPOSAL.to_string(), }; let res = instantiate(deps.as_mut(), env.clone(), info, msg); assert_eq!( @@ -35,39 +44,39 @@ fn test_instantiate_test() { let info = mock_info(MOCK_TIMELOCK_INITIALIZER, &[]); let msg = InstantiateMsg { - timelock_duration: 10, + overrule_pre_propose: MOCK_OVERRULE_PREPROPOSAL.to_string(), }; let res = instantiate(deps.as_mut(), env.clone(), info.clone(), msg.clone()); let res_ok = res.unwrap(); let expected_attributes = vec![ Attribute::new("action", "instantiate"), Attribute::new("owner", MOCK_MAIN_DAO_ADDR), - Attribute::new("timelock_duration", "10"), + Attribute::new("overrule_pre_propose", MOCK_OVERRULE_PREPROPOSAL), ]; assert_eq!(expected_attributes, res_ok.attributes); let config = CONFIG.load(&deps.storage).unwrap(); let expected_config = Config { owner: Addr::unchecked(MOCK_MAIN_DAO_ADDR), - timelock_duration: msg.timelock_duration, + overrule_pre_propose: Addr::unchecked(msg.overrule_pre_propose), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; assert_eq!(expected_config, config); let msg = InstantiateMsg { - timelock_duration: 10, + overrule_pre_propose: MOCK_OVERRULE_PREPROPOSAL.to_string(), }; let res = instantiate(deps.as_mut(), env, info, msg.clone()); let res_ok = res.unwrap(); let expected_attributes = vec![ Attribute::new("action", "instantiate"), Attribute::new("owner", MOCK_MAIN_DAO_ADDR), - Attribute::new("timelock_duration", "10"), + Attribute::new("overrule_pre_propose", MOCK_OVERRULE_PREPROPOSAL), ]; assert_eq!(expected_attributes, res_ok.attributes); let config = CONFIG.load(&deps.storage).unwrap(); let expected_config = Config { owner: Addr::unchecked(MOCK_MAIN_DAO_ADDR), - timelock_duration: msg.timelock_duration, + overrule_pre_propose: Addr::unchecked(msg.overrule_pre_propose), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; assert_eq!(expected_config, config); @@ -75,7 +84,8 @@ fn test_instantiate_test() { #[test] fn test_execute_timelock_proposal() { - let mut deps = mock_dependencies(); + let overrule_proposal_status = Rc::new(RefCell::new(Status::Open)); + let mut deps = mock_dependencies(Rc::clone(&overrule_proposal_status)); let env = mock_env(); let info = mock_info("neutron1unknownsender", &[]); @@ -92,7 +102,7 @@ fn test_execute_timelock_proposal() { let config = Config { owner: Addr::unchecked("owner"), - timelock_duration: 10, + overrule_pre_propose: Addr::unchecked(MOCK_OVERRULE_PREPROPOSAL), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -100,7 +110,7 @@ fn test_execute_timelock_proposal() { assert_eq!("Unauthorized", res.unwrap_err().to_string()); let info = mock_info(MOCK_SUBDAO_CORE_ADDR, &[]); - let res_ok = execute(deps.as_mut(), env.clone(), info, msg).unwrap(); + let res_ok = execute(deps.as_mut(), env, info, msg).unwrap(); let expected_attributes = vec![ Attribute::new("action", "timelock_proposal"), Attribute::new("sender", MOCK_SUBDAO_CORE_ADDR), @@ -108,11 +118,25 @@ fn test_execute_timelock_proposal() { Attribute::new("status", "timelocked"), ]; assert_eq!(expected_attributes, res_ok.attributes); - assert_eq!(0, res_ok.messages.len()); + assert_eq!(1, res_ok.messages.len()); + + assert_eq!( + res_ok.messages, + vec![SubMsg::new(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: MOCK_OVERRULE_PREPROPOSAL.to_string(), + msg: to_binary(&OverruleExecuteMsg::Propose { + msg: OverruleProposeMessage::ProposeOverrule { + timelock_contract: MOCK_CONTRACT_ADDR.to_string(), + proposal_id: 10, + }, + }) + .unwrap(), + funds: vec![], + }))] + ); let expected_proposal = SingleChoiceProposal { id: 10, - timelock_ts: env.block.time, msgs: vec![NeutronMsg::remove_interchain_query(1).into()], status: ProposalStatus::Timelocked, }; @@ -122,8 +146,9 @@ fn test_execute_timelock_proposal() { #[test] fn test_execute_proposal() { - let mut deps = mock_dependencies(); - let mut env = mock_env(); + let overrule_proposal_status = Rc::new(RefCell::new(Status::Open)); + let mut deps = mock_dependencies(Rc::clone(&overrule_proposal_status)); + let env = mock_env(); let info = mock_info("neutron1unknownsender", &[]); let msg = ExecuteMsg::ExecuteProposal { proposal_id: 10 }; @@ -136,7 +161,7 @@ fn test_execute_proposal() { let config = Config { owner: Addr::unchecked("owner"), - timelock_duration: 10, + overrule_pre_propose: Addr::unchecked(MOCK_OVERRULE_PREPROPOSAL), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -154,7 +179,6 @@ fn test_execute_proposal() { for s in wrong_prop_statuses { let proposal = SingleChoiceProposal { id: 10, - timelock_ts: env.block.time, msgs: vec![NeutronMsg::remove_interchain_query(1).into()], status: s, }; @@ -170,7 +194,6 @@ fn test_execute_proposal() { let proposal = SingleChoiceProposal { id: 10, - timelock_ts: env.block.time, msgs: vec![NeutronMsg::remove_interchain_query(1).into()], status: ProposalStatus::Timelocked, }; @@ -179,8 +202,10 @@ fn test_execute_proposal() { .unwrap(); let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); assert_eq!("Proposal is timelocked", res.unwrap_err().to_string()); - - env.block.time = env.block.time.plus_seconds(11); + { + let mut data_mut_ref = overrule_proposal_status.borrow_mut(); + *data_mut_ref = Status::Rejected; + } let res = execute(deps.as_mut(), env, info, msg).unwrap(); let expected_attributes = vec![ Attribute::new("action", "execute_proposal"), @@ -202,7 +227,8 @@ fn test_execute_proposal() { #[test] fn test_overrule_proposal() { - let mut deps = mock_dependencies(); + let overrule_proposal_status = Rc::new(RefCell::new(Status::Open)); + let mut deps = mock_dependencies(Rc::clone(&overrule_proposal_status)); let env = mock_env(); let info = mock_info("neutron1unknownsender", &[]); @@ -216,7 +242,7 @@ fn test_overrule_proposal() { let config = Config { owner: Addr::unchecked("owner"), - timelock_duration: 10, + overrule_pre_propose: Addr::unchecked(MOCK_OVERRULE_PREPROPOSAL), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -233,7 +259,6 @@ fn test_overrule_proposal() { for s in wrong_prop_statuses { let proposal = SingleChoiceProposal { id: 10, - timelock_ts: env.block.time, msgs: vec![NeutronMsg::remove_interchain_query(1).into()], status: s, }; @@ -249,7 +274,6 @@ fn test_overrule_proposal() { let proposal = SingleChoiceProposal { id: 10, - timelock_ts: env.block.time, msgs: vec![NeutronMsg::remove_interchain_query(1).into()], status: ProposalStatus::Timelocked, }; @@ -270,13 +294,14 @@ fn test_overrule_proposal() { #[test] fn execute_update_config() { - let mut deps = mock_dependencies(); + let overrule_proposal_status = Rc::new(RefCell::new(Status::Open)); + let mut deps = mock_dependencies(Rc::clone(&overrule_proposal_status)); let env = mock_env(); let info = mock_info("neutron1unknownsender", &[]); let msg = ExecuteMsg::UpdateConfig { owner: None, - timelock_duration: Some(20), + overrule_pre_propose: Some("neutron1someotheroverrule".to_string()), }; let res = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); @@ -287,7 +312,7 @@ fn execute_update_config() { let config = Config { owner: Addr::unchecked("owner"), - timelock_duration: 10, + overrule_pre_propose: Addr::unchecked(MOCK_OVERRULE_PREPROPOSAL), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -297,7 +322,7 @@ fn execute_update_config() { let info = mock_info("owner", &[]); let config = Config { owner: Addr::unchecked("none"), - timelock_duration: 10, + overrule_pre_propose: Addr::unchecked(MOCK_OVERRULE_PREPROPOSAL), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -306,7 +331,7 @@ fn execute_update_config() { let config = Config { owner: Addr::unchecked("owner"), - timelock_duration: 10, + overrule_pre_propose: Addr::unchecked(MOCK_OVERRULE_PREPROPOSAL), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -315,22 +340,23 @@ fn execute_update_config() { let expected_attributes = vec![ Attribute::new("action", "update_config"), Attribute::new("owner", "owner"), - Attribute::new("timelock_duration", "20"), + Attribute::new("overrule_pre_propose", "neutron1someotheroverrule"), ]; assert_eq!(expected_attributes, res_ok.attributes); let updated_config = CONFIG.load(deps.as_mut().storage).unwrap(); + let some_other_prepropose = "neutron1someotheroverrule"; assert_eq!( updated_config, Config { owner: Addr::unchecked("owner"), - timelock_duration: 20, + overrule_pre_propose: Addr::unchecked(some_other_prepropose), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR) } ); let msg = ExecuteMsg::UpdateConfig { owner: Some("neutron1newowner".to_string()), - timelock_duration: None, + overrule_pre_propose: None, }; let res_ok = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()).unwrap(); @@ -338,7 +364,7 @@ fn execute_update_config() { let expected_attributes = vec![ Attribute::new("action", "update_config"), Attribute::new("owner", "neutron1newowner"), - Attribute::new("timelock_duration", "20"), + Attribute::new("overrule_pre_propose", some_other_prepropose), ]; assert_eq!(expected_attributes, res_ok.attributes); let updated_config = CONFIG.load(deps.as_mut().storage).unwrap(); @@ -346,7 +372,7 @@ fn execute_update_config() { updated_config, Config { owner: Addr::unchecked("neutron1newowner"), - timelock_duration: 20, + overrule_pre_propose: Addr::unchecked(some_other_prepropose), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR) } ); @@ -358,10 +384,11 @@ fn execute_update_config() { #[test] fn test_query_config() { - let mut deps = mock_dependencies(); + let overrule_proposal_status = Rc::new(RefCell::new(Status::Open)); + let mut deps = mock_dependencies(Rc::clone(&overrule_proposal_status)); let config = Config { owner: Addr::unchecked("owner"), - timelock_duration: 20, + overrule_pre_propose: Addr::unchecked(MOCK_OVERRULE_PREPROPOSAL), subdao: Addr::unchecked(MOCK_SUBDAO_CORE_ADDR), }; CONFIG.save(deps.as_mut().storage, &config).unwrap(); @@ -373,11 +400,11 @@ fn test_query_config() { #[test] fn test_query_proposals() { - let mut deps = mock_dependencies(); + let overrule_proposal_status = Rc::new(RefCell::new(Status::Open)); + let mut deps = mock_dependencies(Rc::clone(&overrule_proposal_status)); for i in 1..=100 { let prop = SingleChoiceProposal { id: i, - timelock_ts: mock_env().block.time, msgs: vec![NeutronMsg::remove_interchain_query(i).into()], status: ProposalStatus::Timelocked, }; @@ -389,7 +416,6 @@ fn test_query_proposals() { let queried_prop: SingleChoiceProposal = from_binary(&res).unwrap(); let expected_prop = SingleChoiceProposal { id: i, - timelock_ts: mock_env().block.time, msgs: vec![NeutronMsg::remove_interchain_query(i).into()], status: ProposalStatus::Timelocked, }; @@ -405,7 +431,6 @@ fn test_query_proposals() { for (p, i) in queried_props.proposals.iter().zip(1..) { let expected_prop = SingleChoiceProposal { id: i, - timelock_ts: mock_env().block.time, msgs: vec![NeutronMsg::remove_interchain_query(i).into()], status: ProposalStatus::Timelocked, }; @@ -422,7 +447,6 @@ fn test_query_proposals() { for (p, i) in queried_props.proposals.iter().zip(1..) { let expected_prop = SingleChoiceProposal { id: i, - timelock_ts: mock_env().block.time, msgs: vec![NeutronMsg::remove_interchain_query(i).into()], status: ProposalStatus::Timelocked, }; @@ -439,7 +463,6 @@ fn test_query_proposals() { for (p, i) in queried_props.proposals.iter().zip(1..) { let expected_prop = SingleChoiceProposal { id: i, - timelock_ts: mock_env().block.time, msgs: vec![NeutronMsg::remove_interchain_query(i).into()], status: ProposalStatus::Timelocked, }; @@ -456,7 +479,6 @@ fn test_query_proposals() { for (p, i) in queried_props.proposals.iter().zip(51..) { let expected_prop = SingleChoiceProposal { id: i, - timelock_ts: mock_env().block.time, msgs: vec![NeutronMsg::remove_interchain_query(i).into()], status: ProposalStatus::Timelocked, }; @@ -473,7 +495,6 @@ fn test_query_proposals() { for (p, i) in queried_props.proposals.iter().zip(91..) { let expected_prop = SingleChoiceProposal { id: i, - timelock_ts: mock_env().block.time, msgs: vec![NeutronMsg::remove_interchain_query(i).into()], status: ProposalStatus::Timelocked, }; @@ -484,7 +505,8 @@ fn test_query_proposals() { #[test] fn test_reply() { - let mut deps = mock_dependencies(); + let overrule_proposal_status = Rc::new(RefCell::new(Status::Open)); + let mut deps = mock_dependencies(Rc::clone(&overrule_proposal_status)); let msg = Reply { id: 10, result: SubMsgResult::Err("error".to_string()), @@ -494,7 +516,6 @@ fn test_reply() { let prop = SingleChoiceProposal { id: 10, - timelock_ts: mock_env().block.time, msgs: vec![NeutronMsg::remove_interchain_query(1).into()], status: ProposalStatus::Timelocked, }; diff --git a/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/Cargo.toml b/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/Cargo.toml index 48fd6027..ccf98b94 100644 --- a/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/Cargo.toml +++ b/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/Cargo.toml @@ -32,14 +32,8 @@ cwd-interface = { path = "../../../../packages/cwd-interface" } [dev-dependencies] cosmwasm-schema = "1.0.0" -cw-multi-test = "0.13.2" cw-utils = "0.13.2" -cw4-group = "0.13.2" cw20 = "0.13.2" -cw20-base = "0.13.2" -cwd-subdao-proposal-single = { path = "../../proposal/cwd-subdao-proposal-single" } -cwd-subdao-core = { path = "../../cwd-subdao-core" } cwd-voting = { path = "../../../../packages/cwd-voting" } cw-denom = { path = "../../../../packages/cw-denom" } cwd-interface = { path = "../../../../packages/cwd-interface" } -cwd-proposal-hooks = { path = "../../../../packages/cwd-proposal-hooks" } diff --git a/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/examples/schema.rs b/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/examples/schema.rs index 8923218c..1fb23f13 100644 --- a/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/examples/schema.rs +++ b/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/examples/schema.rs @@ -4,6 +4,7 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; use cosmwasm_std::Addr; use cwd_pre_propose_base::msg::{DepositInfoResponse, ExecuteMsg, InstantiateMsg, QueryMsg}; +use neutron_subdao_pre_propose_single::msg::QueryExt; use neutron_subdao_pre_propose_single::types::ProposeMessage; fn main() { @@ -14,7 +15,7 @@ fn main() { export_schema(&schema_for!(InstantiateMsg), &out_dir); export_schema(&schema_for!(ExecuteMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(DepositInfoResponse), &out_dir); export_schema_with_title(&schema_for!(Addr), &out_dir, "ProposalModuleResponse"); diff --git a/contracts/dao/pre-propose/cwd-pre-propose-multiple/schema/query_msg.json b/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/schema/query_msg_for__query_ext.json similarity index 63% rename from contracts/dao/pre-propose/cwd-pre-propose-multiple/schema/query_msg.json rename to contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/schema/query_msg_for__query_ext.json index a057bb1e..e6f4a6ea 100644 --- a/contracts/dao/pre-propose/cwd-pre-propose-multiple/schema/query_msg.json +++ b/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/schema/query_msg_for__query_ext.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "QueryMsg", + "title": "QueryMsg_for_QueryExt", "oneOf": [ { "description": "Gets the proposal module that this pre propose module is associated with. Returns `Addr`.", @@ -63,6 +63,45 @@ } }, "additionalProperties": false + }, + { + "description": "Extension for queries. The default implementation will do nothing if queried for will return `Binary::default()`.", + "type": "object", + "required": [ + "query_extension" + ], + "properties": { + "query_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryExt" + } + } + } + }, + "additionalProperties": false + } + ], + "definitions": { + "QueryExt": { + "oneOf": [ + { + "type": "object", + "required": [ + "timelock_address" + ], + "properties": { + "timelock_address": { + "type": "object" + } + }, + "additionalProperties": false + } + ] } - ] + } } diff --git a/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/src/contract.rs b/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/src/contract.rs index 8207c42c..c1f7ad4a 100644 --- a/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/src/contract.rs +++ b/contracts/subdaos/pre-propose/cwd-subdao-pre-propose-single/src/contract.rs @@ -17,7 +17,7 @@ use cwd_pre_propose_base::{ state::PreProposeContract, }; use neutron_subdao_pre_propose_single::{ - msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + msg::{ExecuteMsg, InstantiateMsg, QueryExt, QueryMsg}, types::ProposeMessage, }; use neutron_subdao_proposal_single::msg::QueryMsg as ProposalQueryMsg; @@ -42,7 +42,7 @@ enum ProposeMessageInternal { }, } -type PrePropose = PreProposeContract; +type PrePropose = PreProposeContract; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -152,8 +152,10 @@ pub fn execute( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::QueryBase(msg) => PrePropose::default().query(deps, env, msg), - QueryMsg::TimelockAddress {} => query_timelock_address(deps), + QueryMsg::QueryExtension { + msg: QueryExt::TimelockAddress {}, + } => query_timelock_address(deps), + _ => PrePropose::default().query(deps, env, msg), } } diff --git a/packages/cwd-pre-propose-base/src/execute.rs b/packages/cwd-pre-propose-base/src/execute.rs index 9737e49e..e3a5ebf1 100644 --- a/packages/cwd-pre-propose-base/src/execute.rs +++ b/packages/cwd-pre-propose-base/src/execute.rs @@ -4,6 +4,7 @@ use cosmwasm_std::{ use std::fmt::Debug; use cw2::set_contract_version; +use schemars::JsonSchema; use cw_denom::UncheckedDenom; use cwd_interface::voting::{Query as CwCoreQuery, VotingPowerAtHeightResponse}; @@ -22,9 +23,10 @@ use crate::{ const CONTRACT_NAME: &str = "crates.io::cwd-pre-propose-base"; const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -impl PreProposeContract +impl PreProposeContract where ProposalMessage: Serialize + Debug, + QueryExt: JsonSchema, { pub fn instantiate( &self, @@ -99,7 +101,7 @@ where } } - pub fn query(&self, deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + pub fn query(&self, deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::ProposalModule {} => to_binary(&self.proposal_module.load(deps.storage)?), QueryMsg::Dao {} => to_binary(&self.dao.load(deps.storage)?), @@ -111,6 +113,7 @@ where proposer, }) } + QueryMsg::QueryExtension { .. } => Ok(Binary::default()), } } diff --git a/packages/cwd-pre-propose-base/src/msg.rs b/packages/cwd-pre-propose-base/src/msg.rs index 943f613c..36444211 100644 --- a/packages/cwd-pre-propose-base/src/msg.rs +++ b/packages/cwd-pre-propose-base/src/msg.rs @@ -85,7 +85,10 @@ pub enum ExecuteMsg { #[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum QueryMsg { +pub enum QueryMsg +where + QueryExt: JsonSchema, +{ /// Gets the proposal module that this pre propose module is /// associated with. Returns `Addr`. ProposalModule {}, @@ -97,6 +100,9 @@ pub enum QueryMsg { /// Gets the deposit info for the proposal identified by /// PROPOSAL_ID. Returns `DepositInfoResponse`. DepositInfo { proposal_id: u64 }, + /// Extension for queries. The default implementation will do + /// nothing if queried for will return `Binary::default()`. + QueryExtension { msg: QueryExt }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/packages/cwd-pre-propose-base/src/state.rs b/packages/cwd-pre-propose-base/src/state.rs index b83bf838..fa8dc251 100644 --- a/packages/cwd-pre-propose-base/src/state.rs +++ b/packages/cwd-pre-propose-base/src/state.rs @@ -17,7 +17,7 @@ pub struct Config { pub open_proposal_submission: bool, } -pub struct PreProposeContract { +pub struct PreProposeContract { /// The proposal module that this module is associated with. pub proposal_module: Item<'static, Addr>, /// The DAO (cw-dao-core module) that this module is associated @@ -32,9 +32,10 @@ pub struct PreProposeContract { // assocaited data. To stop the compiler complaining about unused // generics, we build this phantom data. proposal_type: PhantomData, + query_type: PhantomData, } -impl PreProposeContract { +impl PreProposeContract { const fn new( proposal_key: &'static str, dao_key: &'static str, @@ -47,11 +48,12 @@ impl PreProposeContract { config: Item::new(config_key), deposits: Map::new(deposits_key), proposal_type: PhantomData, + query_type: PhantomData, } } } -impl Default for PreProposeContract { +impl Default for PreProposeContract { fn default() -> Self { // Call into constant function here. Presumably, the compiler // is clever enough to inline this. This gives us diff --git a/packages/neutron-dao-pre-propose-overrule/Cargo.toml b/packages/neutron-dao-pre-propose-overrule/Cargo.toml new file mode 100644 index 00000000..2b972c70 --- /dev/null +++ b/packages/neutron-dao-pre-propose-overrule/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "neutron-dao-pre-propose-overrule" +version = "0.1.0" +authors = ["oldremez"] +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/neutron/neutron-dao" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cwd-pre-propose-base = { version = "*", path = "../cwd-pre-propose-base" } +schemars = "0.8" +serde = { version = "1.0.147", default-features = false, features = ["derive"] } diff --git a/packages/neutron-dao-pre-propose-overrule/src/lib.rs b/packages/neutron-dao-pre-propose-overrule/src/lib.rs new file mode 100644 index 00000000..d0e87a0d --- /dev/null +++ b/packages/neutron-dao-pre-propose-overrule/src/lib.rs @@ -0,0 +1 @@ +pub mod msg; diff --git a/packages/neutron-dao-pre-propose-overrule/src/msg.rs b/packages/neutron-dao-pre-propose-overrule/src/msg.rs new file mode 100644 index 00000000..f495963e --- /dev/null +++ b/packages/neutron-dao-pre-propose-overrule/src/msg.rs @@ -0,0 +1,29 @@ +use cwd_pre_propose_base::msg::{ExecuteMsg as ExecuteBase, QueryMsg as QueryBase}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, JsonSchema, Deserialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum ProposeMessage { + ProposeOverrule { + timelock_contract: String, + proposal_id: u64, + }, +} + +pub type ExecuteMsg = ExecuteBase; + +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct InstantiateMsg {} + +#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryExt { + OverruleProposalId { + timelock_address: String, + subdao_proposal_id: u64, + }, +} + +pub type QueryMsg = QueryBase; diff --git a/packages/neutron-subdao-pre-propose-single/src/msg.rs b/packages/neutron-subdao-pre-propose-single/src/msg.rs index c2fb3899..9b7b9b2d 100644 --- a/packages/neutron-subdao-pre-propose-single/src/msg.rs +++ b/packages/neutron-subdao-pre-propose-single/src/msg.rs @@ -24,8 +24,8 @@ pub type ExecuteMsg = ExecuteBase; #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] #[serde(rename_all = "snake_case")] -pub enum QueryMsg { - QueryBase(QueryBase), - /// Gets the address of the timelock contract. +pub enum QueryExt { TimelockAddress {}, } + +pub type QueryMsg = QueryBase; diff --git a/packages/neutron-subdao-timelock-single/src/msg.rs b/packages/neutron-subdao-timelock-single/src/msg.rs index 29cb8b25..c493610c 100644 --- a/packages/neutron-subdao-timelock-single/src/msg.rs +++ b/packages/neutron-subdao-timelock-single/src/msg.rs @@ -5,9 +5,8 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] pub struct InstantiateMsg { - // Timelock duration for all proposals (starts when TimelockProposal message handler is executed). - // In seconds. - pub timelock_duration: u64, + // Overrule pre proposal module from the main DAO + pub overrule_pre_propose: String, } #[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)] @@ -25,7 +24,7 @@ pub enum ExecuteMsg { }, UpdateConfig { owner: Option, - timelock_duration: Option, + overrule_pre_propose: Option, }, } diff --git a/packages/neutron-subdao-timelock-single/src/types.rs b/packages/neutron-subdao-timelock-single/src/types.rs index f3181dab..1360f787 100644 --- a/packages/neutron-subdao-timelock-single/src/types.rs +++ b/packages/neutron-subdao-timelock-single/src/types.rs @@ -1,5 +1,5 @@ use cosmwasm_std::Addr; -use cosmwasm_std::{CosmosMsg, Timestamp}; +use cosmwasm_std::CosmosMsg; use neutron_bindings::bindings::msg::NeutronMsg; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)] pub struct Config { pub owner: Addr, - pub timelock_duration: u64, + pub overrule_pre_propose: Addr, // subDAO core module can timelock proposals. pub subdao: Addr, } @@ -17,9 +17,6 @@ pub struct SingleChoiceProposal { /// The ID of the proposal being returned. pub id: u64, - /// The timestamp at which the proposal was submitted to the timelock contract. - pub timelock_ts: Timestamp, - /// The messages that will be executed should this proposal be executed. pub msgs: Vec>,