diff --git a/ci/bootstrap-env/src/main.rs b/ci/bootstrap-env/src/main.rs index 3a70d194c..c1c321c91 100644 --- a/ci/bootstrap-env/src/main.rs +++ b/ci/bootstrap-env/src/main.rs @@ -134,7 +134,7 @@ fn main() -> Result<()> { orc.instantiate( "cw_admin_factory", "admin_factory_init", - &cw_admin_factory::msg::InstantiateMsg {}, + &cw_admin_factory::msg::InstantiateMsg { admin: None }, &key, None, vec![], diff --git a/contracts/external/cw-admin-factory/schema/cw-admin-factory.json b/contracts/external/cw-admin-factory/schema/cw-admin-factory.json index f1a1e1254..9fafa6028 100644 --- a/contracts/external/cw-admin-factory/schema/cw-admin-factory.json +++ b/contracts/external/cw-admin-factory/schema/cw-admin-factory.json @@ -6,6 +6,15 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "InstantiateMsg", "type": "object", + "properties": { + "admin": { + "description": "The account allowed to execute this contract. If no admin, anyone can execute it.", + "type": [ + "string", + "null" + ] + } + }, "additionalProperties": false }, "execute": { @@ -55,8 +64,21 @@ "query": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "QueryMsg", - "type": "string", - "enum": [] + "oneOf": [ + { + "type": "object", + "required": [ + "admin" + ], + "properties": { + "admin": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] }, "migrate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -65,5 +87,30 @@ "additionalProperties": false }, "sudo": null, - "responses": {} + "responses": { + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "type": "object", + "properties": { + "admin": { + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + } + } } diff --git a/contracts/external/cw-admin-factory/src/contract.rs b/contracts/external/cw-admin-factory/src/contract.rs index f1f9b31df..00eaf73cb 100644 --- a/contracts/external/cw-admin-factory/src/contract.rs +++ b/contracts/external/cw-admin-factory/src/contract.rs @@ -1,14 +1,16 @@ #[cfg(not(feature = "library"))] use cosmwasm_std::entry_point; use cosmwasm_std::{ - Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, WasmMsg, + to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, SubMsg, + WasmMsg, }; use cw2::set_contract_version; use cw_utils::parse_reply_instantiate_data; use crate::error::ContractError; -use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::msg::{AdminResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use crate::state::ADMIN; pub(crate) const CONTRACT_NAME: &str = "crates.io:cw-admin-factory"; pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -19,9 +21,13 @@ pub fn instantiate( deps: DepsMut, _env: Env, info: MessageInfo, - _msg: InstantiateMsg, + msg: InstantiateMsg, ) -> Result { set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let admin = msg.admin.map(|a| deps.api.addr_validate(&a)).transpose()?; + ADMIN.save(deps.storage, &admin)?; + Ok(Response::new() .add_attribute("method", "instantiate") .add_attribute("creator", info.sender)) @@ -29,7 +35,7 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( - _deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg, @@ -39,17 +45,25 @@ 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), } } pub fn instantiate_contract( + deps: DepsMut, env: Env, info: MessageInfo, instantiate_msg: Binary, code_id: u64, label: String, ) -> Result { + // If admin set, require the sender to be the admin. + if let Some(admin) = ADMIN.load(deps.storage)? { + if admin != info.sender { + return Err(ContractError::Unauthorized {}); + } + } + // Instantiate the specified contract with factory as the admin. let instantiate = WasmMsg::Instantiate { admin: Some(env.contract.address.to_string()), @@ -66,8 +80,12 @@ pub fn instantiate_contract( } #[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::Admin {} => Ok(to_json_binary(&AdminResponse { + admin: ADMIN.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..299c8bfe1 100644 --- a/contracts/external/cw-admin-factory/src/error.rs +++ b/contracts/external/cw-admin-factory/src/error.rs @@ -2,7 +2,7 @@ use cosmwasm_std::StdError; use cw_utils::ParseReplyError; use thiserror::Error; -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq)] pub enum ContractError { #[error("{0}")] Std(#[from] StdError), 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..35a472783 100644 --- a/contracts/external/cw-admin-factory/src/msg.rs +++ b/contracts/external/cw-admin-factory/src/msg.rs @@ -1,8 +1,12 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::Binary; +use cosmwasm_std::{Addr, Binary}; #[cw_serde] -pub struct InstantiateMsg {} +pub struct InstantiateMsg { + /// The account allowed to execute this contract. If no admin, anyone can + /// execute it. + pub admin: Option, +} #[cw_serde] pub enum ExecuteMsg { @@ -17,7 +21,15 @@ pub enum ExecuteMsg { #[cw_serde] #[derive(QueryResponses)] -pub enum QueryMsg {} +pub enum QueryMsg { + #[returns(AdminResponse)] + Admin {}, +} #[cw_serde] pub struct MigrateMsg {} + +#[cw_serde] +pub struct AdminResponse { + pub admin: Option, +} 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..218223e2c --- /dev/null +++ b/contracts/external/cw-admin-factory/src/state.rs @@ -0,0 +1,5 @@ +use cosmwasm_std::Addr; +use cw_storage_plus::Item; + +/// The account allowed to execute the contract. If None, anyone is allowed. +pub const ADMIN: Item> = Item::new("admin"); diff --git a/contracts/external/cw-admin-factory/src/tests.rs b/contracts/external/cw-admin-factory/src/tests.rs index 3bee180ee..7e6dbfbed 100644 --- a/contracts/external/cw-admin-factory/src/tests.rs +++ b/contracts/external/cw-admin-factory/src/tests.rs @@ -9,11 +9,15 @@ use cw_multi_test::{App, AppResponse, Contract, ContractWrapper, Executor}; 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}, + contract::{ + instantiate, migrate, reply, CONTRACT_NAME, CONTRACT_VERSION, INSTANTIATE_CONTRACT_REPLY_ID, + }, + msg::{AdminResponse, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}, + ContractError, }; +const ADMIN_ADDR: &str = "admin"; + fn factory_contract() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, @@ -45,7 +49,7 @@ fn cw_core_contract() -> Box> { } #[test] -pub fn test_set_admin() { +pub fn test_set_self_admin() { let mut app = App::default(); let code_id = app.store_code(factory_contract()); let cw20_code_id = app.store_code(cw20_contract()); @@ -58,7 +62,7 @@ pub fn test_set_admin() { marketing: None, }; - let instantiate = InstantiateMsg {}; + let instantiate = InstantiateMsg { admin: None }; let factory_addr = app .instantiate_contract( code_id, @@ -130,10 +134,122 @@ pub fn test_set_admin() { } #[test] -pub fn test_set_admin_mock() { +pub fn test_authorized_set_self_admin() { + let mut app = App::default(); + let code_id = app.store_code(factory_contract()); + let cw20_code_id = app.store_code(cw20_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 { + admin: Some(ADMIN_ADDR.to_string()), + }; + let factory_addr = app + .instantiate_contract( + code_id, + Addr::unchecked(ADMIN_ADDR), + &instantiate, + &[], + "cw-admin-factory", + None, + ) + .unwrap(); + + // Query admin. + let current_admin: AdminResponse = app + .wrap() + .query_wasm_smart(factory_addr.clone(), &QueryMsg::Admin {}) + .unwrap(); + assert_eq!(current_admin.admin, Some(Addr::unchecked(ADMIN_ADDR))); + + // Instantiate core contract using factory. + let cw_core_code_id = app.store_code(cw_core_contract()); + 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_json_binary(&cw20_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "voting module".to_string(), + }, + proposal_modules_instantiate_info: vec![ + ModuleInstantiateInfo { + code_id: cw20_code_id, + msg: to_json_binary(&cw20_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "prop module".to_string(), + }, + ModuleInstantiateInfo { + code_id: cw20_code_id, + msg: to_json_binary(&cw20_instantiate).unwrap(), + admin: Some(Admin::CoreModule {}), + funds: vec![], + label: "prop module 2".to_string(), + }, + ], + initial_items: None, + }; + + // Fails when not the admin. + let err: ContractError = app + .execute_contract( + Addr::unchecked("not_admin"), + factory_addr.clone(), + &ExecuteMsg::InstantiateContractWithSelfAdmin { + instantiate_msg: to_json_binary(&instantiate_core).unwrap(), + code_id: cw_core_code_id, + label: "my contract".to_string(), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Succeeds as the admin. + let res: AppResponse = app + .execute_contract( + Addr::unchecked(ADMIN_ADDR), + factory_addr, + &ExecuteMsg::InstantiateContractWithSelfAdmin { + instantiate_msg: to_json_binary(&instantiate_core).unwrap(), + code_id: cw_core_code_id, + label: "my contract".to_string(), + }, + &[], + ) + .unwrap(); + + // Get the core address from the instantiate event + let instantiate_event = &res.events[2]; + assert_eq!(instantiate_event.ty, "instantiate"); + let core_addr = instantiate_event.attributes[0].value.clone(); + + // Check that admin of core address is itself + let contract_info = app.wrap().query_wasm_contract_info(&core_addr).unwrap(); + assert_eq!(contract_info.admin, Some(core_addr)) +} + +#[test] +pub fn test_set_self_admin_mock() { let mut deps = mock_dependencies(); // Instantiate factory contract - let instantiate_msg = InstantiateMsg {}; + let instantiate_msg = InstantiateMsg { admin: None }; let info = mock_info("creator", &[]); let env = mock_env(); instantiate(deps.as_mut(), env.clone(), info, instantiate_msg).unwrap();