diff --git a/contracts/dao-dao-core/schema/dao-dao-core.json b/contracts/dao-dao-core/schema/dao-dao-core.json index 5b504f602..80991cef8 100644 --- a/contracts/dao-dao-core/schema/dao-dao-core.json +++ b/contracts/dao-dao-core/schema/dao-dao-core.json @@ -290,6 +290,20 @@ }, "additionalProperties": false }, + { + "description": "Unpauses the DAO", + "type": "object", + "required": [ + "unpause" + ], + "properties": { + "unpause": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "description": "Executed when the contract receives a cw20 token. Depending on the contract's configuration the contract will automatically add the token to its treasury.", "type": "object", diff --git a/contracts/dao-dao-core/src/contract.rs b/contracts/dao-dao-core/src/contract.rs index 56cd5c526..1ee52a165 100644 --- a/contracts/dao-dao-core/src/contract.rs +++ b/contracts/dao-dao-core/src/contract.rs @@ -106,10 +106,15 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { - // No actions can be performed while the DAO is paused. + // Check if the DAO is paused if let Some(expiration) = PAUSED.may_load(deps.storage)? { if !expiration.is_expired(&env.block) { - return Err(ContractError::Paused {}); + // If paused, then only allow messages from the Admin or DAO itself + if info.sender != env.contract.address + && info.sender.clone() != ADMIN.load(deps.storage)? + { + return Err(ContractError::Paused {}); + } } } @@ -121,6 +126,7 @@ pub fn execute( execute_proposal_hook(deps.as_ref(), info.sender, msgs) } ExecuteMsg::Pause { duration } => execute_pause(deps, env, info.sender, duration), + ExecuteMsg::Unpause {} => execute_unpause(deps, info.sender), ExecuteMsg::Receive(_) => execute_receive_cw20(deps, info.sender), ExecuteMsg::ReceiveNft(_) => execute_receive_cw721(deps, info.sender), ExecuteMsg::RemoveItem { key } => execute_remove_item(deps, env, info.sender, key), @@ -174,6 +180,21 @@ pub fn execute_pause( .add_attribute("until", until.to_string())) } +pub fn execute_unpause(deps: DepsMut, sender: Addr) -> Result { + let admin = ADMIN.load(deps.storage)?; + + // Only the admin can unpause + if sender != admin { + return Err(ContractError::Unauthorized {}); + } + + PAUSED.remove(deps.storage); + + Ok(Response::new() + .add_attribute("action", "execute_unpause") + .add_attribute("sender", sender)) +} + pub fn execute_admin_msgs( deps: Deps, sender: Addr, diff --git a/contracts/dao-dao-core/src/msg.rs b/contracts/dao-dao-core/src/msg.rs deleted file mode 100644 index 99f0d21b4..000000000 --- a/contracts/dao-dao-core/src/msg.rs +++ /dev/null @@ -1,240 +0,0 @@ -use crate::state::Config; -use crate::{migrate_msg::MigrateParams, query::SubDao}; -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{CosmosMsg, Empty}; -use cw_utils::Duration; -use dao_interface::ModuleInstantiateInfo; - -/// Information about an item to be stored in the items list. -#[cw_serde] -pub struct InitialItem { - /// The name of the item. - pub key: String, - /// The value the item will have at instantiation time. - pub value: String, -} - -#[cw_serde] -pub struct InstantiateMsg { - /// Optional Admin with the ability to execute DAO messages - /// directly. Useful for building SubDAOs controlled by a parent - /// DAO. If no admin is specified the contract is set as its own - /// admin so that the admin may be updated later by governance. - pub admin: Option, - /// The name of the core contract. - pub name: String, - /// A description of the core contract. - pub description: String, - /// An image URL to describe the core module contract. - pub image_url: Option, - - /// If true the contract will automatically add received cw20 - /// tokens to its treasury. - pub automatically_add_cw20s: bool, - /// If true the contract will automatically add received cw721 - /// tokens to its treasury. - pub automatically_add_cw721s: bool, - - /// Instantiate information for the core contract's voting - /// power module. - pub voting_module_instantiate_info: ModuleInstantiateInfo, - /// Instantiate information for the core contract's proposal modules. - /// NOTE: the pre-propose-base package depends on it being the case - /// that the core module instantiates its proposal module. - pub proposal_modules_instantiate_info: Vec, - - /// The items to instantiate this DAO with. Items are arbitrary - /// key-value pairs whose contents are controlled by governance. - /// - /// It is an error to provide two items with the same key. - pub initial_items: Option>, - /// Implements the DAO Star standard: - pub dao_uri: Option, -} - -#[cw_serde] -pub enum ExecuteMsg { - /// Callable by the Admin, if one is configured. - /// Executes messages in order. - ExecuteAdminMsgs { msgs: Vec> }, - /// Callable by proposal modules. The DAO will execute the - /// messages in the hook in order. - ExecuteProposalHook { msgs: Vec> }, - /// Pauses the DAO for a set duration. - /// When paused the DAO is unable to execute proposals - Pause { duration: Duration }, - /// Executed when the contract receives a cw20 token. Depending on - /// the contract's configuration the contract will automatically - /// add the token to its treasury. - Receive(cw20::Cw20ReceiveMsg), - /// Executed when the contract receives a cw721 token. Depending - /// on the contract's configuration the contract will - /// automatically add the token to its treasury. - ReceiveNft(cw721::Cw721ReceiveMsg), - /// Removes an item from the governance contract's item map. - RemoveItem { key: String }, - /// Adds an item to the governance contract's item map. If the - /// item already exists the existing value is overridden. If the - /// item does not exist a new item is added. - SetItem { key: String, value: String }, - /// Callable by the admin of the contract. If ADMIN is None the - /// admin is set as the contract itself so that it may be updated - /// later by vote. If ADMIN is Some a new admin is proposed and - /// that new admin may become the admin by executing the - /// `AcceptAdminNomination` message. - /// - /// If there is already a pending admin nomination the - /// `WithdrawAdminNomination` message must be executed before a - /// new admin may be nominated. - NominateAdmin { admin: Option }, - /// Callable by a nominated admin. Admins are nominated via the - /// `NominateAdmin` message. Accepting a nomination will make the - /// nominated address the new admin. - /// - /// Requiring that the new admin accepts the nomination before - /// becoming the admin protects against a typo causing the admin - /// to change to an invalid address. - AcceptAdminNomination {}, - /// Callable by the current admin. Withdraws the current admin - /// nomination. - WithdrawAdminNomination {}, - /// Callable by the core contract. Replaces the current - /// governance contract config with the provided config. - UpdateConfig { config: Config }, - /// Updates the list of cw20 tokens this contract has registered. - UpdateCw20List { - to_add: Vec, - to_remove: Vec, - }, - /// Updates the list of cw721 tokens this contract has registered. - UpdateCw721List { - to_add: Vec, - to_remove: Vec, - }, - /// Updates the governance contract's governance modules. Module - /// instantiate info in `to_add` is used to create new modules and - /// install them. - UpdateProposalModules { - /// NOTE: the pre-propose-base package depends on it being the - /// case that the core module instantiates its proposal module. - to_add: Vec, - to_disable: Vec, - }, - /// Callable by the core contract. Replaces the current - /// voting module with a new one instantiated by the governance - /// contract. - UpdateVotingModule { module: ModuleInstantiateInfo }, - /// Update the core module to add/remove SubDAOs and their charters - UpdateSubDaos { - to_add: Vec, - to_remove: Vec, - }, -} - -#[cw_serde] -#[derive(QueryResponses)] -pub enum QueryMsg { - /// Get's the DAO's admin. Returns `Addr`. - #[returns(cosmwasm_std::Addr)] - Admin {}, - /// Get's the currently nominated admin (if any). - #[returns(crate::query::AdminNominationResponse)] - AdminNomination {}, - /// Gets the contract's config. - #[returns(Config)] - Config {}, - /// Gets the token balance for each cw20 registered with the - /// contract. - #[returns(crate::query::Cw20BalanceResponse)] - Cw20Balances { - start_after: Option, - limit: Option, - }, - /// Lists the addresses of the cw20 tokens in this contract's - /// treasury. - #[returns(Vec)] - Cw20TokenList { - start_after: Option, - limit: Option, - }, - /// Lists the addresses of the cw721 tokens in this contract's - /// treasury. - #[returns(Vec)] - Cw721TokenList { - start_after: Option, - limit: Option, - }, - /// Dumps all of the core contract's state in a single - /// query. Useful for frontends as performance for queries is more - /// limited by network times than compute times. - #[returns(crate::query::DumpStateResponse)] - DumpState {}, - /// Gets the address associated with an item key. - #[returns(crate::query::GetItemResponse)] - GetItem { key: String }, - /// Lists all of the items associted with the contract. For - /// example, given the items `{ "group": "foo", "subdao": "bar"}` - /// this query would return `[("group", "foo"), ("subdao", - /// "bar")]`. - #[returns(Vec)] - ListItems { - start_after: Option, - limit: Option, - }, - /// Returns contract version info - #[returns(dao_interface::voting::InfoResponse)] - Info {}, - /// Gets all proposal modules associated with the - /// contract. - #[returns(Vec)] - ProposalModules { - start_after: Option, - limit: Option, - }, - /// Gets the active proposal modules associated with the - /// contract. - #[returns(Vec)] - ActiveProposalModules { - start_after: Option, - limit: Option, - }, - /// Gets the number of active and total proposal modules - /// registered with this module. - #[returns(crate::query::ProposalModuleCountResponse)] - ProposalModuleCount {}, - /// Returns information about if the contract is currently paused. - #[returns(crate::query::PauseInfoResponse)] - PauseInfo {}, - /// Gets the contract's voting module. - #[returns(cosmwasm_std::Addr)] - VotingModule {}, - /// Returns all SubDAOs with their charters in a vec. - /// start_after is bound exclusive and asks for a string address. - #[returns(Vec)] - ListSubDaos { - start_after: Option, - limit: Option, - }, - /// Implements the DAO Star standard: - #[returns(crate::query::DaoURIResponse)] - DaoURI {}, - /// Returns the voting power for an address at a given height. - #[returns(dao_interface::voting::VotingPowerAtHeightResponse)] - VotingPowerAtHeight { - address: String, - height: Option, - }, - /// Returns the total voting power at a given block height. - #[returns(dao_interface::voting::TotalPowerAtHeightResponse)] - TotalPowerAtHeight { height: Option }, -} - -#[allow(clippy::large_enum_variant)] -#[cw_serde] -pub enum MigrateMsg { - FromV1 { - dao_uri: Option, - params: Option, - }, - FromCompatible {}, -} diff --git a/contracts/dao-dao-core/src/tests.rs b/contracts/dao-dao-core/src/tests.rs index 88574113c..81dd040cb 100644 --- a/contracts/dao-dao-core/src/tests.rs +++ b/contracts/dao-dao-core/src/tests.rs @@ -1072,7 +1072,7 @@ fn test_admin_permissions() { ); res.unwrap_err(); - // Proposal mdoule can't call ExecuteAdminMsgs + // Proposal module can't call ExecuteAdminMsgs let res = app.execute_contract( proposal_module.address.clone(), core_addr.clone(), @@ -1145,7 +1145,29 @@ fn test_admin_permissions() { ); res.unwrap_err(); - // Admin can call ExecuteAdminMsgs, here an admin pasues the DAO + // Admin cannot directly pause the DAO + let res = app.execute_contract( + Addr::unchecked("admin"), + core_with_admin_addr.clone(), + &ExecuteMsg::Pause { + duration: Duration::Height(10), + }, + &[], + ); + assert!(res.is_err()); + + // Random person cannot pause the DAO + let res = app.execute_contract( + Addr::unchecked("random"), + core_with_admin_addr.clone(), + &ExecuteMsg::Pause { + duration: Duration::Height(10), + }, + &[], + ); + assert!(res.is_err()); + + // Admin can call ExecuteAdminMsgs, here an admin pauses the DAO let res = app.execute_contract( Addr::unchecked("admin"), core_with_admin_addr.clone(), @@ -1162,8 +1184,9 @@ fn test_admin_permissions() { }, &[], ); - res.unwrap(); + assert!(res.is_ok()); + // Ensure we are paused for 10 blocks let paused: PauseInfoResponse = app .wrap() .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::PauseInfo {}) @@ -1178,6 +1201,66 @@ fn test_admin_permissions() { // DAO unpauses after 10 blocks app.update_block(|block| block.height += 11); + // Check we are unpaused + let paused: PauseInfoResponse = app + .wrap() + .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::PauseInfo {}) + .unwrap(); + assert_eq!(paused, PauseInfoResponse::Unpaused {}); + + // Admin pauses DAO again + let res = app.execute_contract( + Addr::unchecked("admin"), + core_with_admin_addr.clone(), + &ExecuteMsg::ExecuteAdminMsgs { + msgs: vec![WasmMsg::Execute { + contract_addr: core_with_admin_addr.to_string(), + msg: to_json_binary(&ExecuteMsg::Pause { + duration: Duration::Height(10), + }) + .unwrap(), + funds: vec![], + } + .into()], + }, + &[], + ); + assert!(res.is_ok()); + + // DAO with admin cannot unpause itself + let res = app.execute_contract( + core_with_admin_addr.clone(), + core_with_admin_addr.clone(), + &ExecuteMsg::Unpause {}, + &[], + ); + assert!(res.is_err()); + + // Random person cannot unpause the DAO + let res = app.execute_contract( + Addr::unchecked("random"), + core_with_admin_addr.clone(), + &ExecuteMsg::Unpause {}, + &[], + ); + assert!(res.is_err()); + + // Admin can unpause the DAO directly + let res = app.execute_contract( + Addr::unchecked("admin"), + core_with_admin_addr.clone(), + &ExecuteMsg::Unpause {}, + &[], + ); + assert!(res.is_ok()); + + // Check we are unpaused + let paused: PauseInfoResponse = app + .wrap() + .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::PauseInfo {}) + .unwrap(); + assert_eq!(paused, PauseInfoResponse::Unpaused {}); + // Admin can nominate a new admin. let res = app.execute_contract( Addr::unchecked("admin"), @@ -2415,27 +2498,23 @@ fn test_pause() { } ); - let err: ContractError = app - .execute_contract( - core_addr.clone(), - core_addr.clone(), - &ExecuteMsg::UpdateConfig { - config: Config { - dao_uri: None, - name: "The Empire Strikes Back Again".to_string(), - description: "haha lol we have pwned your DAO again".to_string(), - image_url: None, - automatically_add_cw20s: true, - automatically_add_cw721s: true, - }, + // This should actually be allowed to enable the admin to execute + let result = app.execute_contract( + core_addr.clone(), + core_addr.clone(), + &ExecuteMsg::UpdateConfig { + config: Config { + dao_uri: None, + name: "The Empire Strikes Back Again".to_string(), + description: "haha lol we have pwned your DAO again".to_string(), + image_url: None, + automatically_add_cw20s: true, + automatically_add_cw721s: true, }, - &[], - ) - .unwrap_err() - .downcast() - .unwrap(); - - assert!(matches!(err, ContractError::Paused { .. })); + }, + &[], + ); + assert!(result.is_ok()); let err: ContractError = app .execute_contract( diff --git a/packages/dao-interface/src/msg.rs b/packages/dao-interface/src/msg.rs index 797865c2e..969288433 100644 --- a/packages/dao-interface/src/msg.rs +++ b/packages/dao-interface/src/msg.rs @@ -63,6 +63,8 @@ pub enum ExecuteMsg { /// Pauses the DAO for a set duration. /// When paused the DAO is unable to execute proposals Pause { duration: Duration }, + /// Unpauses the DAO + Unpause {}, /// Executed when the contract receives a cw20 token. Depending on /// the contract's configuration the contract will automatically /// add the token to its treasury.