diff --git a/Cargo.lock b/Cargo.lock index b1e7d67d1..c609ee805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1641,6 +1641,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "dao-callback-messages" +version = "2.5.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "dao-interface 2.5.0", +] + [[package]] name = "dao-cw721-extensions" version = "2.5.0" @@ -1687,6 +1696,7 @@ dependencies = [ "cw20-base 1.1.2", "cw721 0.18.0", "cw721-base 0.18.0", + "dao-callback-messages", "dao-dao-macros 2.5.0", "dao-interface 2.5.0", "dao-proposal-sudo", diff --git a/Cargo.toml b/Cargo.toml index 1d98d3a94..80b977751 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ cw-wormhole = { path = "./packages/cw-wormhole", version = "2.5.0" } cw20-stake = { path = "./contracts/staking/cw20-stake", version = "2.5.0" } cw721-controllers = { path = "./packages/cw721-controllers", version = "2.5.0" } cw721-roles = { path = "./contracts/external/cw721-roles", version = "2.5.0" } +dao-callback-messages = { path = "./contracts/test/dao-callback-messages", version = "2.5.0" } dao-cw721-extensions = { path = "./packages/dao-cw721-extensions", version = "2.5.0" } dao-dao-core = { path = "./contracts/dao-dao-core", version = "2.5.0" } dao-dao-macros = { path = "./packages/dao-dao-macros", version = "2.5.0" } diff --git a/contracts/dao-dao-core/Cargo.toml b/contracts/dao-dao-core/Cargo.toml index a9ac8dae6..602b74c1e 100644 --- a/contracts/dao-dao-core/Cargo.toml +++ b/contracts/dao-dao-core/Cargo.toml @@ -36,3 +36,4 @@ cw20-base = { workspace = true } cw721-base = { workspace = true } dao-proposal-sudo = { workspace = true } dao-voting-cw20-balance = { workspace = true } +dao-callback-messages = { workspace = true } diff --git a/contracts/dao-dao-core/src/contract.rs b/contracts/dao-dao-core/src/contract.rs index 1ee52a165..d4b914f86 100644 --- a/contracts/dao-dao-core/src/contract.rs +++ b/contracts/dao-dao-core/src/contract.rs @@ -7,7 +7,7 @@ use cosmwasm_std::{ use cw2::{get_contract_version, set_contract_version, ContractVersion}; use cw_paginate_storage::{paginate_map, paginate_map_keys, paginate_map_values}; use cw_storage_plus::Map; -use cw_utils::{parse_reply_instantiate_data, Duration}; +use cw_utils::{parse_reply_execute_data, parse_reply_instantiate_data, Duration}; use dao_interface::{ msg::{ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, QueryMsg}, query::{ @@ -15,7 +15,7 @@ use dao_interface::{ GetItemResponse, PauseInfoResponse, ProposalModuleCountResponse, SubDao, }, state::{ - Admin, Config, ModuleInstantiateCallback, ModuleInstantiateInfo, ProposalModule, + Admin, CallbackMessages, Config, ModuleInstantiateInfo, ProposalModule, ProposalModuleStatus, }, voting, @@ -30,9 +30,10 @@ use crate::state::{ pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-dao-core"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); -const PROPOSAL_MODULE_REPLY_ID: u64 = 0; +const PROPOSAL_MODULE_INSTANTIATE_REPLY_ID: u64 = 0; const VOTE_MODULE_INSTANTIATE_REPLY_ID: u64 = 1; const VOTE_MODULE_UPDATE_REPLY_ID: u64 = 2; +const PROPOSAL_MODULE_EXECUTE_REPLY_ID: u64 = 3; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -71,7 +72,7 @@ pub fn instantiate( .proposal_modules_instantiate_info .into_iter() .map(|info| info.into_wasm_msg(env.contract.address.clone())) - .map(|wasm| SubMsg::reply_on_success(wasm, PROPOSAL_MODULE_REPLY_ID)) + .map(|wasm| SubMsg::reply_on_success(wasm, PROPOSAL_MODULE_INSTANTIATE_REPLY_ID)) .collect(); if proposal_module_msgs.is_empty() { return Err(ContractError::NoActiveProposalModules {}); @@ -228,7 +229,10 @@ pub fn execute_proposal_hook( Ok(Response::default() .add_attribute("action", "execute_proposal_hook") - .add_messages(msgs)) + .add_submessages( + msgs.into_iter() + .map(|msg| SubMsg::reply_on_success(msg, PROPOSAL_MODULE_EXECUTE_REPLY_ID)), + )) } pub fn execute_nominate_admin( @@ -392,7 +396,7 @@ pub fn execute_update_proposal_modules( let to_add: Vec> = to_add .into_iter() .map(|info| info.into_wasm_msg(env.contract.address.clone())) - .map(|wasm| SubMsg::reply_on_success(wasm, PROPOSAL_MODULE_REPLY_ID)) + .map(|wasm| SubMsg::reply_on_success(wasm, PROPOSAL_MODULE_INSTANTIATE_REPLY_ID)) .collect(); Ok(Response::default() @@ -952,7 +956,7 @@ pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result Result { match msg.id { - PROPOSAL_MODULE_REPLY_ID => { + PROPOSAL_MODULE_INSTANTIATE_REPLY_ID => { let res = parse_reply_instantiate_data(msg)?; let prop_module_addr = deps.api.addr_validate(&res.contract_address)?; let total_module_count = TOTAL_PROPOSAL_MODULE_COUNT.load(deps.storage)?; @@ -973,7 +977,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result from_json::(&data) + Some(data) => from_json::(&data) .map(|m| m.msgs) .unwrap_or_else(|_| vec![]), None => vec![], @@ -983,7 +987,6 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result { let res = parse_reply_instantiate_data(msg)?; let vote_module_addr = deps.api.addr_validate(&res.contract_address)?; @@ -999,7 +1002,7 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result from_json::(&data) + Some(data) => from_json::(&data) .map(|m| m.msgs) .unwrap_or_else(|_| vec![]), None => vec![], @@ -1017,6 +1020,19 @@ pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result match parse_reply_execute_data(msg) { + Ok(res) => { + let callback_msgs = match res.data { + Some(data) => from_json::(&data) + .map(|m| m.msgs) + .unwrap_or_else(|_| vec![]), + None => vec![], + }; + + Ok(Response::default().add_messages(callback_msgs)) + } + Err(_) => Ok(Response::default()), + }, _ => Err(ContractError::UnknownReplyID {}), } } diff --git a/contracts/dao-dao-core/src/tests.rs b/contracts/dao-dao-core/src/tests.rs index 81dd040cb..ec61e1f55 100644 --- a/contracts/dao-dao-core/src/tests.rs +++ b/contracts/dao-dao-core/src/tests.rs @@ -82,6 +82,15 @@ fn v1_cw_core_contract() -> Box> { Box::new(contract) } +fn dao_callback_messages_contract() -> Box> { + let contract = ContractWrapper::new( + dao_callback_messages::contract::execute, + dao_callback_messages::contract::instantiate, + dao_callback_messages::contract::query, + ); + Box::new(contract) +} + fn instantiate_gov(app: &mut App, code_id: u64, msg: InstantiateMsg) -> Addr { app.instantiate_contract( code_id, @@ -3197,3 +3206,97 @@ fn test_query_info() { } ) } + +#[test] +pub fn test_callback_messages() { + let (core_addr, mut app) = do_standard_instantiate(true, None); + + // Store and instantiate the dao-callback-messages contract + let callback_id = app.store_code(dao_callback_messages_contract()); + let callback_addr = app + .instantiate_contract( + callback_id, + Addr::unchecked(CREATOR_ADDR), + &Empty {}, + &[], + "dao-callback-messages", + None, + ) + .unwrap(); + + // Get the proposal module + let proposal_modules: Vec = app + .wrap() + .query_wasm_smart( + core_addr.clone(), + &QueryMsg::ProposalModules { + start_after: None, + limit: None, + }, + ) + .unwrap(); + let proposal_module = proposal_modules[0].address.clone(); + + // Test successful callback + let success_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: callback_addr.to_string(), + msg: to_json_binary(&dao_callback_messages::msg::ExecuteMsg::Execute { + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: callback_addr.to_string(), + msg: to_json_binary(&dao_callback_messages::msg::ExecuteMsg::Execute { + msgs: vec![], + }) + .unwrap(), + funds: vec![], + })], + }) + .unwrap(), + funds: vec![], + }); + + let res = app.execute_contract( + proposal_module.clone(), + core_addr.clone(), + &ExecuteMsg::ExecuteProposalHook { + msgs: vec![success_msg], + }, + &[], + ); + assert!(res.is_ok()); + + // Check for error attributes not in the response + let attrs = res + .unwrap() + .events + .iter() + .flat_map(|e| e.attributes.clone()) + .collect::>(); + let callback_failed_attr = attrs + .iter() + .find(|attr| attr.key == "callback_message_failed"); + assert!(callback_failed_attr.is_none()); + + // Test error callback + let error_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: callback_addr.to_string(), + msg: to_json_binary(&dao_callback_messages::msg::ExecuteMsg::Execute { + msgs: vec![CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "non_existent_contract".to_string(), + msg: to_json_binary(&"{}").unwrap(), + funds: vec![], + })], + }) + .unwrap(), + funds: vec![], + }); + + let res = app.execute_contract( + proposal_module, + core_addr, + &ExecuteMsg::ExecuteProposalHook { + msgs: vec![error_msg], + }, + &[], + ); + assert!(res.is_err()); +} diff --git a/contracts/external/dao-migrator/src/contract.rs b/contracts/external/dao-migrator/src/contract.rs index 625d0276b..c99988268 100644 --- a/contracts/external/dao-migrator/src/contract.rs +++ b/contracts/external/dao-migrator/src/contract.rs @@ -9,7 +9,7 @@ use cosmwasm_std::{ use cw2::set_contract_version; use dao_interface::{ query::SubDao, - state::{ModuleInstantiateCallback, ProposalModule}, + state::{CallbackMessages, ProposalModule}, }; use crate::{ @@ -42,7 +42,7 @@ pub fn instantiate( CORE_ADDR.save(deps.storage, &info.sender)?; Ok( - Response::default().set_data(to_json_binary(&ModuleInstantiateCallback { + Response::default().set_data(to_json_binary(&CallbackMessages { msgs: vec![WasmMsg::Execute { contract_addr: env.contract.address.to_string(), msg: to_json_binary(&MigrateV1ToV2 { diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs b/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs index 268d3bae8..321098c53 100644 --- a/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs +++ b/contracts/pre-propose/dao-pre-propose-approver/src/contract.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{ }; use cw2::set_contract_version; -use dao_interface::state::ModuleInstantiateCallback; +use dao_interface::state::CallbackMessages; use dao_pre_propose_approval_single::msg::{ ApproverProposeMessage, ExecuteExt as ApprovalExt, ExecuteMsg as PreProposeApprovalExecuteMsg, }; @@ -59,7 +59,7 @@ pub fn instantiate( let addr = deps.api.addr_validate(&msg.pre_propose_approval_contract)?; PRE_PROPOSE_APPROVAL_CONTRACT.save(deps.storage, &addr)?; - Ok(resp.set_data(to_json_binary(&ModuleInstantiateCallback { + Ok(resp.set_data(to_json_binary(&CallbackMessages { msgs: vec![ CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: addr.to_string(), diff --git a/contracts/test/dao-callback-messages/Cargo.toml b/contracts/test/dao-callback-messages/Cargo.toml new file mode 100644 index 000000000..9194aeeeb --- /dev/null +++ b/contracts/test/dao-callback-messages/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "dao-callback-messages" +authors = ["Gabe "] +description = "A contract for sending callback messages in the data field." +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[dependencies] +cosmwasm-std = { workspace = true } +cosmwasm-schema = { workspace = true } +dao-interface = { workspace = true } diff --git a/contracts/test/dao-callback-messages/README.md b/contracts/test/dao-callback-messages/README.md new file mode 100644 index 000000000..f219c6c21 --- /dev/null +++ b/contracts/test/dao-callback-messages/README.md @@ -0,0 +1,3 @@ +# dao-proposal-sudo + +A contract for sending callback messages through the data field of the response. diff --git a/contracts/test/dao-callback-messages/src/contract.rs b/contracts/test/dao-callback-messages/src/contract.rs new file mode 100644 index 000000000..85f1cac2d --- /dev/null +++ b/contracts/test/dao-callback-messages/src/contract.rs @@ -0,0 +1,43 @@ +#[cfg(not(feature = "library"))] +use cosmwasm_std::entry_point; +use cosmwasm_std::{ + to_json_binary, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdResult, +}; +use dao_interface::state::CallbackMessages; + +use crate::msg::ExecuteMsg; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: Empty, +) -> StdResult { + Ok(Response::new().add_attribute("method", "instantiate")) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + _deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: ExecuteMsg, +) -> StdResult { + match msg { + ExecuteMsg::Execute { msgs } => execute_execute(msgs), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(_deps: Deps, _env: Env, _msg: Empty) -> StdResult { + Ok(Binary::default()) +} + +pub fn execute_execute(msgs: Vec) -> StdResult { + let data = to_json_binary(&CallbackMessages { msgs })?; + + Ok(Response::default() + .add_attribute("action", "execute") + .set_data(data)) +} diff --git a/contracts/test/dao-callback-messages/src/lib.rs b/contracts/test/dao-callback-messages/src/lib.rs new file mode 100644 index 000000000..112ecadc8 --- /dev/null +++ b/contracts/test/dao-callback-messages/src/lib.rs @@ -0,0 +1,2 @@ +pub mod contract; +pub mod msg; diff --git a/contracts/test/dao-callback-messages/src/msg.rs b/contracts/test/dao-callback-messages/src/msg.rs new file mode 100644 index 000000000..e8a56d42a --- /dev/null +++ b/contracts/test/dao-callback-messages/src/msg.rs @@ -0,0 +1,7 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::CosmosMsg; + +#[cw_serde] +pub enum ExecuteMsg { + Execute { msgs: Vec }, +} diff --git a/contracts/test/dao-test-custom-factory/src/contract.rs b/contracts/test/dao-test-custom-factory/src/contract.rs index 67ad19686..95a727155 100644 --- a/contracts/test/dao-test-custom-factory/src/contract.rs +++ b/contracts/test/dao-test-custom-factory/src/contract.rs @@ -15,7 +15,7 @@ use cw_tokenfactory_issuer::msg::{ use cw_utils::{one_coin, parse_reply_instantiate_data}; use dao_interface::{ nft::NftFactoryCallback, - state::ModuleInstantiateCallback, + state::CallbackMessages, token::{InitialBalance, NewTokenInfo, TokenFactoryCallback}, voting::{ActiveThresholdQuery, Query as VotingModuleQueryMsg}, }; @@ -274,7 +274,7 @@ pub fn execute_token_factory_factory_wrong_callback( ) } -/// Example method called in the ModuleInstantiateCallback providing +/// Example method called in the CallbackMessages providing /// an example for checking the DAO has been setup correctly. pub fn execute_validate_nft_dao( deps: DepsMut, @@ -432,10 +432,10 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result = initial_nfts .iter() .flat_map(|nft| -> Result { @@ -494,7 +494,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Result Result, + pub module_instantiate_callback: Option, } diff --git a/packages/dao-interface/src/state.rs b/packages/dao-interface/src/state.rs index 2022640eb..b19c274ea 100644 --- a/packages/dao-interface/src/state.rs +++ b/packages/dao-interface/src/state.rs @@ -81,9 +81,9 @@ impl ModuleInstantiateInfo { } } -/// Callbacks to be executed when a module is instantiated +/// Callbacks to be executed from response data #[cw_serde] -pub struct ModuleInstantiateCallback { +pub struct CallbackMessages { pub msgs: Vec, } diff --git a/packages/dao-interface/src/token.rs b/packages/dao-interface/src/token.rs index c735a15c4..79570ec93 100644 --- a/packages/dao-interface/src/token.rs +++ b/packages/dao-interface/src/token.rs @@ -5,7 +5,7 @@ use cosmwasm_std::Uint128; // We re-export them here for convenience. pub use osmosis_std::types::cosmos::bank::v1beta1::{DenomUnit, Metadata}; -use crate::state::ModuleInstantiateCallback; +use crate::state::CallbackMessages; #[cw_serde] pub struct InitialBalance { @@ -48,5 +48,5 @@ pub struct NewTokenInfo { pub struct TokenFactoryCallback { pub denom: String, pub token_contract: Option, - pub module_instantiate_callback: Option, + pub module_instantiate_callback: Option, }