diff --git a/ci/bootstrap-env/src/main.rs b/ci/bootstrap-env/src/main.rs index 3a70d194c..bb7df4847 100644 --- a/ci/bootstrap-env/src/main.rs +++ b/ci/bootstrap-env/src/main.rs @@ -134,7 +134,9 @@ 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/src/contract.rs b/contracts/external/cw-admin-factory/src/contract.rs index f1f9b31df..c1756b4cb 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,26 @@ 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::UpdateAdmin { admin } => execute_update_admin(deps, info, admin), } } 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()), @@ -65,9 +80,36 @@ pub fn instantiate_contract( .add_submessage(msg)) } +pub fn execute_update_admin( + deps: DepsMut, + info: MessageInfo, + admin: Option, +) -> Result { + // Only allow the current admin to update the admin. If no admin, no admin + // can ever be set. + let current_admin = ADMIN.load(deps.storage)?; + if current_admin.map_or(false, |a| a != info.sender) { + return Err(ContractError::Unauthorized {}); + } + + let new_admin = admin.map(|s| deps.api.addr_validate(&s)).transpose()?; + ADMIN.save(deps.storage, &new_admin)?; + + Ok(Response::default() + .add_attribute("action", "update_admin") + .add_attribute( + "admin", + new_admin.map_or("_none".to_string(), |a| a.to_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::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..1a52f6b37 100644 --- a/contracts/external/cw-admin-factory/src/msg.rs +++ b/contracts/external/cw-admin-factory/src/msg.rs @@ -1,8 +1,11 @@ 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. + pub admin: Option, +} #[cw_serde] pub enum ExecuteMsg { @@ -13,11 +16,22 @@ pub enum ExecuteMsg { code_id: u64, label: String, }, + /// Update the admin that is allowed to execute this contract. If there is + /// no admin, this cannot be called and there will never be an admin. + UpdateAdmin { admin: Option }, } #[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..f72790096 100644 --- a/contracts/external/cw-admin-factory/src/tests.rs +++ b/contracts/external/cw-admin-factory/src/tests.rs @@ -9,11 +9,16 @@ 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"; +const NEW_ADMIN_ADDR: &str = "new_admin"; + fn factory_contract() -> Box> { let contract = ContractWrapper::new( crate::contract::execute, @@ -45,7 +50,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 +63,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 +135,198 @@ 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_update_admin() { + let mut app = App::default(); + let code_id = app.store_code(factory_contract()); + + 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))); + + // Fails when not the admin. + let err: ContractError = app + .execute_contract( + Addr::unchecked("not_admin"), + factory_addr.clone(), + &ExecuteMsg::UpdateAdmin { + admin: Some(NEW_ADMIN_ADDR.to_string()), + }, + &[], + ) + .unwrap_err() + .downcast() + .unwrap(); + assert_eq!(err, ContractError::Unauthorized {}); + + // Succeeds as the admin. + app.execute_contract( + Addr::unchecked(ADMIN_ADDR), + factory_addr.clone(), + &ExecuteMsg::UpdateAdmin { + admin: Some(NEW_ADMIN_ADDR.to_string()), + }, + &[], + ) + .unwrap(); + + // Query new admin. + let current_admin: AdminResponse = app + .wrap() + .query_wasm_smart(factory_addr.clone(), &QueryMsg::Admin {}) + .unwrap(); + assert_eq!(current_admin.admin, Some(Addr::unchecked(NEW_ADMIN_ADDR))); + + // Clear the admin. + app.execute_contract( + Addr::unchecked(NEW_ADMIN_ADDR), + factory_addr.clone(), + &ExecuteMsg::UpdateAdmin { admin: None }, + &[], + ) + .unwrap(); + + // Query cleared admin. + let current_admin: AdminResponse = app + .wrap() + .query_wasm_smart(factory_addr, &QueryMsg::Admin {}) + .unwrap(); + assert_eq!(current_admin.admin, None); +} + +#[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();