Skip to content

Commit

Permalink
Add admin auth to cw-admin-factory (#810)
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso authored Mar 25, 2024
1 parent ef21c63 commit f5a95b4
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 22 deletions.
2 changes: 1 addition & 1 deletion ci/bootstrap-env/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![],
Expand Down
53 changes: 50 additions & 3 deletions contracts/external/cw-admin-factory/schema/cw-admin-factory.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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#",
Expand All @@ -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"
}
}
}
}
}
32 changes: 25 additions & 7 deletions contracts/external/cw-admin-factory/src/contract.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -19,17 +21,21 @@ pub fn instantiate(
deps: DepsMut,
_env: Env,
info: MessageInfo,
_msg: InstantiateMsg,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
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))
}

#[cfg_attr(not(feature = "library"), entry_point)]
pub fn execute(
_deps: DepsMut,
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
Expand All @@ -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<Response, ContractError> {
// 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()),
Expand All @@ -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<Binary> {
match msg {}
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
QueryMsg::Admin {} => Ok(to_json_binary(&AdminResponse {
admin: ADMIN.load(deps.storage)?,
})?),
}
}

#[cfg_attr(not(feature = "library"), entry_point)]
Expand Down
2 changes: 1 addition & 1 deletion contracts/external/cw-admin-factory/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions contracts/external/cw-admin-factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
pub mod contract;
mod error;
pub mod msg;
pub mod state;

#[cfg(test)]
mod tests;
Expand Down
18 changes: 15 additions & 3 deletions contracts/external/cw-admin-factory/src/msg.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}

#[cw_serde]
pub enum ExecuteMsg {
Expand All @@ -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<Addr>,
}
5 changes: 5 additions & 0 deletions contracts/external/cw-admin-factory/src/state.rs
Original file line number Diff line number Diff line change
@@ -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<Option<Addr>> = Item::new("admin");
130 changes: 123 additions & 7 deletions contracts/external/cw-admin-factory/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn Contract<Empty>> {
let contract = ContractWrapper::new(
crate::contract::execute,
Expand Down Expand Up @@ -45,7 +49,7 @@ fn cw_core_contract() -> Box<dyn Contract<Empty>> {
}

#[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());
Expand All @@ -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,
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit f5a95b4

Please sign in to comment.