diff --git a/Cargo.lock b/Cargo.lock index 29caf23d6..c3d334ab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,7 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-multi-test", + "cw-ownable", "cw-storage-plus 1.1.0", "cw-utils 1.0.1", "cw2 1.1.0", diff --git a/contracts/external/cw-admin-factory/Cargo.toml b/contracts/external/cw-admin-factory/Cargo.toml index b22399df8..7700abbf8 100644 --- a/contracts/external/cw-admin-factory/Cargo.toml +++ b/contracts/external/cw-admin-factory/Cargo.toml @@ -24,6 +24,7 @@ cw-storage-plus = { workspace = true } cw2 = { workspace = true } thiserror = { workspace = true } cw-utils = { workspace = true } +cw-ownable = { workspace = true } [dev-dependencies] cosmwasm-schema = { workspace = true } diff --git a/contracts/external/cw-admin-factory/src/contract.rs b/contracts/external/cw-admin-factory/src/contract.rs index f1f9b31df..9bf7d953c 100644 --- a/contracts/external/cw-admin-factory/src/contract.rs +++ b/contracts/external/cw-admin-factory/src/contract.rs @@ -1,14 +1,17 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, WasmMsg, + to_binary, Attribute, BankMsg, Binary, Coin, Coins, Deps, DepsMut, Env, MessageInfo, Reply, + Response, StdResult, SubMsg, WasmMsg, }; use cw2::set_contract_version; +use cw_ownable::{assert_owner, get_ownership, initialize_owner, update_ownership}; use cw_utils::parse_reply_instantiate_data; use crate::error::ContractError; use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::FEE; pub(crate) const CONTRACT_NAME: &str = "crates.io:cw-admin-factory"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -19,17 +22,23 @@ pub fn instantiate( deps: DepsMut, _env: Env, info: MessageInfo, - _msg: InstantiateMsg, + msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let ownership = initialize_owner(deps.storage, deps.api, msg.owner.as_deref())?; + let attributes = update_fee_inner(deps, msg.fee)?; + Ok(Response::new() .add_attribute("method", "instantiate") - .add_attribute("creator", info.sender)) + .add_attribute("creator", info.sender) + .add_attributes(attributes) + .add_attributes(ownership.into_attributes())) } #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - _deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, @@ -39,35 +48,110 @@ pub fn execute( instantiate_msg: msg, code_id, label, - } => instantiate_contract(env, info, msg, code_id, label), + } => instantiate_contract(deps, env, info, msg, code_id, label), + ExecuteMsg::UpdateFee { fee } => execute_update_fee(deps, info, fee), + ExecuteMsg::UpdateOwnership(action) => { + let ownership = update_ownership(deps, &env.block, &info.sender, action)?; + Ok(Response::default().add_attributes(ownership.into_attributes())) + } } } pub fn instantiate_contract( + deps: DepsMut, env: Env, info: MessageInfo, instantiate_msg: Binary, code_id: u64, label: String, ) -> Result { + // Validate and get coins struct + let mut funds = Coins::try_from(info.funds)?; + + // Check for a fee and attach a bank send message if found + let mut response = Response::default(); + match FEE.may_load(deps.storage)? { + Some(fee) => { + // Get the funds recipient + let ownership = get_ownership(deps.storage)?; + + if ownership.owner.is_none() { + return Err(ContractError::Ownership( + cw_ownable::OwnershipError::NoOwner {}, + )); + } + + // Subtract the fee from the funds + for coin in &fee { + funds.sub(coin.clone())?; + } + + let msg = BankMsg::Send { + to_address: ownership.owner.unwrap().to_string(), + amount: fee, + }; + + response = response.add_message(msg); + } + None => {} + }; + // Instantiate the specified contract with factory as the admin. let instantiate = WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), code_id, msg: instantiate_msg, - funds: info.funds, + funds: funds.into_vec(), label, }; let msg = SubMsg::reply_on_success(instantiate, INSTANTIATE_CONTRACT_REPLY_ID); - Ok(Response::default() + + Ok(response .add_attribute("action", "instantiate_cw_core") .add_submessage(msg)) } +pub fn execute_update_fee( + deps: DepsMut, + info: MessageInfo, + fee: Option>, +) -> Result { + assert_owner(deps.storage, &info.sender)?; + + let attributes = update_fee_inner(deps, fee)?; + + Ok(Response::default() + .add_attribute("action", "execute_update_fee") + .add_attribute("sender", info.sender) + .add_attributes(attributes)) +} + +/// Updates the fee configuration and returns the fee attributes +fn update_fee_inner( + deps: DepsMut, + fee: Option>, +) -> Result, ContractError> { + let fee = fee.map(|x| Coins::try_from(x)).transpose()?; + let fee_string = fee.as_ref().map_or("None".to_owned(), ToString::to_string); + + match fee { + Some(fee) => FEE.save(deps.storage, &fee.into_vec())?, + None => FEE.remove(deps.storage), + } + + Ok(vec![Attribute { + key: "fee".to_owned(), + value: fee_string, + }]) +} + #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg {} +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Ownership {} => to_binary(&get_ownership(deps.storage)?), + QueryMsg::Fee {} => to_binary(&FEE.may_load(deps.storage)?), + } } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/external/cw-admin-factory/src/error.rs b/contracts/external/cw-admin-factory/src/error.rs index 56c764778..3a177da6f 100644 --- a/contracts/external/cw-admin-factory/src/error.rs +++ b/contracts/external/cw-admin-factory/src/error.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::StdError; +use cosmwasm_std::{CoinsError, StdError}; +use cw_ownable::OwnershipError; use cw_utils::ParseReplyError; use thiserror::Error; @@ -7,6 +8,12 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + Coins(#[from] CoinsError), + + #[error("{0}")] + Ownership(#[from] OwnershipError), + #[error("Unauthorized")] Unauthorized {}, diff --git a/contracts/external/cw-admin-factory/src/lib.rs b/contracts/external/cw-admin-factory/src/lib.rs index 6902586b6..d1800adbc 100644 --- a/contracts/external/cw-admin-factory/src/lib.rs +++ b/contracts/external/cw-admin-factory/src/lib.rs @@ -3,6 +3,7 @@ pub mod contract; mod error; pub mod msg; +pub mod state; #[cfg(test)] mod tests; diff --git a/contracts/external/cw-admin-factory/src/msg.rs b/contracts/external/cw-admin-factory/src/msg.rs index 1cc0d4258..9ac17a27c 100644 --- a/contracts/external/cw-admin-factory/src/msg.rs +++ b/contracts/external/cw-admin-factory/src/msg.rs @@ -1,9 +1,14 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Binary; +use cosmwasm_std::{Binary, Coin}; +use cw_ownable::{cw_ownable_execute, cw_ownable_query}; #[cw_serde] -pub struct InstantiateMsg {} +pub struct InstantiateMsg { + pub owner: Option, + pub fee: Option>, +} +#[cw_ownable_execute] #[cw_serde] pub enum ExecuteMsg { /// Instantiates the target contract with the provided instantiate message and code id and @@ -13,11 +18,18 @@ pub enum ExecuteMsg { code_id: u64, label: String, }, + UpdateFee { + fee: Option>, + }, } +#[cw_ownable_query] #[cw_serde] #[derive(QueryResponses)] -pub enum QueryMsg {} +pub enum QueryMsg { + #[returns(Option>)] + Fee {}, +} #[cw_serde] pub struct MigrateMsg {} diff --git a/contracts/external/cw-admin-factory/src/state.rs b/contracts/external/cw-admin-factory/src/state.rs new file mode 100644 index 000000000..655f66daf --- /dev/null +++ b/contracts/external/cw-admin-factory/src/state.rs @@ -0,0 +1,4 @@ +use cosmwasm_std::Coin; +use cw_storage_plus::Item; + +pub const FEE: Item> = Item::new("fee"); diff --git a/contracts/external/cw-admin-factory/src/tests.rs b/contracts/external/cw-admin-factory/src/tests.rs index 226d1f1ef..b1c014908 100644 --- a/contracts/external/cw-admin-factory/src/tests.rs +++ b/contracts/external/cw-admin-factory/src/tests.rs @@ -2,7 +2,8 @@ use std::vec; use cosmwasm_std::{ testing::{mock_dependencies, mock_env, mock_info}, - to_binary, Addr, Binary, Empty, Reply, SubMsg, SubMsgResponse, SubMsgResult, WasmMsg, + to_binary, Addr, Binary, Coin, Empty, Reply, SubMsg, SubMsgResponse, SubMsgResult, Uint128, + WasmMsg, }; use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; @@ -11,7 +12,7 @@ use dao_interface::state::{Admin, ModuleInstantiateInfo}; use crate::{ contract::instantiate, contract::{migrate, reply, CONTRACT_NAME, CONTRACT_VERSION, INSTANTIATE_CONTRACT_REPLY_ID}, - msg::{ExecuteMsg, InstantiateMsg, MigrateMsg}, + msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, }; fn factory_contract() -> Box> { @@ -58,7 +59,10 @@ pub fn test_set_admin() { marketing: None, }; - let instantiate = InstantiateMsg {}; + let instantiate = InstantiateMsg { + fee: None, + owner: None, + }; let factory_addr = app .instantiate_contract( code_id, @@ -130,7 +134,10 @@ pub fn test_set_admin() { pub fn test_set_admin_mock() { let mut deps = mock_dependencies(); // Instantiate factory contract - let instantiate_msg = InstantiateMsg {}; + let instantiate_msg = InstantiateMsg { + fee: None, + owner: None, + }; let info = mock_info("creator", &[]); let env = mock_env(); instantiate(deps.as_mut(), env.clone(), info, instantiate_msg).unwrap(); @@ -163,3 +170,167 @@ pub fn test_migrate_update_version() { assert_eq!(version.version, CONTRACT_VERSION); assert_eq!(version.contract, CONTRACT_NAME); } + +#[test] +pub fn test_fee() { + let creator = Addr::unchecked("alice"); + let user = Addr::unchecked("bob"); + let balance = vec![Coin { + denom: "juno".to_string(), + amount: Uint128::from(100u128), + }]; + // Start with a fee higher than the user's balance + let fee = vec![Coin { + denom: "juno".to_string(), + amount: Uint128::from(200u128), + }]; + let mut app = App::new(|router, _, storage| { + // initialization moved to App construction + router + .bank + .init_balance(storage, &user, balance.clone()) + .unwrap(); + }); + + let code_id = app.store_code(factory_contract()); + let cw20_code_id = app.store_code(cw20_contract()); + let cw_core_code_id = app.store_code(cw_core_contract()); + + let cw20_instantiate = cw20_base::msg::InstantiateMsg { + name: "DAO".to_string(), + symbol: "DAO".to_string(), + decimals: 6, + initial_balances: vec![], + mint: None, + marketing: None, + }; + + let instantiate = InstantiateMsg { + fee: Some(fee.clone()), + owner: Some(creator.to_string()), + }; + let factory_addr = app + .instantiate_contract( + code_id, + creator.clone(), + &instantiate, + &[], + "cw-admin-factory", + None, + ) + .unwrap(); + + // Check that the fee was set on instantiate + let fee_result: Option> = app + .wrap() + .query_wasm_smart(factory_addr.clone(), &QueryMsg::Fee {}) + .unwrap(); + assert_eq!(fee_result, Some(fee.clone())); + + // Instantiate core contract using factory. + let instantiate_core = dao_interface::msg::InstantiateMsg { + dao_uri: None, + admin: None, + name: "DAO DAO".to_string(), + description: "A DAO that builds DAOs.".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, + voting_module_instantiate_info: ModuleInstantiateInfo { + code_id: cw20_code_id, + msg: to_binary(&cw20_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ + ModuleInstantiateInfo { + code_id: cw20_code_id, + msg: to_binary(&cw20_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + label: "prop module".to_string(), + }, + ModuleInstantiateInfo { + code_id: cw20_code_id, + msg: to_binary(&cw20_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + label: "prop module 2".to_string(), + }, + ], + initial_items: None, + }; + + // Fail on insufficient funds + let res = app.execute_contract( + user.clone(), + factory_addr.clone(), + &ExecuteMsg::InstantiateContractWithSelfAdmin { + instantiate_msg: to_binary(&instantiate_core).unwrap(), + code_id: cw_core_code_id, + label: "my contract".to_string(), + }, + &balance, + ); + assert!(res.is_err()); + + // Update fee to exact balance + app.execute_contract( + creator.clone(), + factory_addr.clone(), + &ExecuteMsg::UpdateFee { + fee: Some(balance.clone()), + }, + &vec![], + ) + .unwrap(); + + // Success with a fee + let res = app.execute_contract( + user.clone(), + factory_addr.clone(), + &ExecuteMsg::InstantiateContractWithSelfAdmin { + instantiate_msg: to_binary(&instantiate_core).unwrap(), + code_id: cw_core_code_id, + label: "my contract".to_string(), + }, + &balance, + ); + assert!(res.is_ok()); + // Check that the owner received funds + assert_eq!( + app.wrap().query_balance(creator.clone(), "juno").unwrap(), + balance[0] + ); + + // Remove fee + app.execute_contract( + creator.clone(), + factory_addr.clone(), + &ExecuteMsg::UpdateFee { fee: None }, + &vec![], + ) + .unwrap(); + + // Success with no fee + let res = app.execute_contract( + user.clone(), + factory_addr.clone(), + &ExecuteMsg::InstantiateContractWithSelfAdmin { + instantiate_msg: to_binary(&instantiate_core).unwrap(), + code_id: cw_core_code_id, + label: "my contract".to_string(), + }, + &vec![], + ); + assert!(res.is_ok()); + + // Fail update fee - not owner + let res = app.execute_contract( + user.clone(), + factory_addr.clone(), + &ExecuteMsg::UpdateFee { + fee: Some(fee.clone()), + }, + &vec![], + ); + assert!(res.is_err()); +}