Repository: DA0-DA0/dao-contracts
Pull request #265 (2022-04-26 01:18:22)

Allow the core contract to pause itself.
Stops literally all execute messages from happening for the duration of the pause.

ezekiiel: >  @JakeHartnell - Should there be a way to un-pause the DAO?

I guess I'm imagining a world where this pause method is effectively a "stop the world until we can figure out what's next". I could see this being used in a situation where there is a vulnerability discovered in the core contract or a governance contract when we don't yet have a replacement.

Individual modules might have more granular methods for pausing themselves (for example, a voting module that can be paused on weekends). If you're pausing the core contract that's the big red something has gone horribly wrong and we need to stop everything button. In a perfect world, this should probably never be used.

Patch:
diff --git a/contracts/cw-core/examples/schema.rs b/contracts/cw-core/examples/schema.rs
index 1fc98f3f..5a1048ba 100644
--- a/contracts/cw-core/examples/schema.rs
+++ b/contracts/cw-core/examples/schema.rs
@@ -5,7 +5,7 @@ use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, s
 use cosmwasm_std::Addr;
 use cw_core::{
     msg::{ExecuteMsg, InstantiateMsg, QueryMsg},
-    query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse},
+    query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse, PauseInfoResponse},
     state::Config,
 };
 use cw_core_interface::voting::{
@@ -23,6 +23,7 @@ fn main() {
     export_schema(&schema_for!(QueryMsg), &out_dir);
 
     export_schema(&schema_for!(DumpStateResponse), &out_dir);
+    export_schema(&schema_for!(PauseInfoResponse), &out_dir);
     export_schema(&schema_for!(GetItemResponse), &out_dir);
     export_schema(&schema_for!(InfoResponse), &out_dir);
     export_schema(&schema_for!(TotalPowerAtHeightResponse), &out_dir);
diff --git a/contracts/cw-core/schema/dump_state_response.json b/contracts/cw-core/schema/dump_state_response.json
index b35688a2..cf09b2dc 100644
--- a/contracts/cw-core/schema/dump_state_response.json
+++ b/contracts/cw-core/schema/dump_state_response.json
@@ -6,6 +6,7 @@
   "required": [
     "config",
     "governance_modules",
+    "pause_info",
     "version",
     "voting_module"
   ],
@@ -25,6 +26,9 @@
         "$ref": "#/definitions/Addr"
       }
     },
+    "pause_info": {
+      "$ref": "#/definitions/PauseInfoResponse"
+    },
     "version": {
       "description": "The governance contract's version.",
       "allOf": [
@@ -97,6 +101,101 @@
           "type": "string"
         }
       }
+    },
+    "Expiration": {
+      "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+      "oneOf": [
+        {
+          "description": "AtHeight will expire when `env.block.height` >= height",
+          "type": "object",
+          "required": [
+            "at_height"
+          ],
+          "properties": {
+            "at_height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "AtTime will expire when `env.block.time` >= time",
+          "type": "object",
+          "required": [
+            "at_time"
+          ],
+          "properties": {
+            "at_time": {
+              "$ref": "#/definitions/Timestamp"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Never will never expire. Used to express the empty variant",
+          "type": "object",
+          "required": [
+            "never"
+          ],
+          "properties": {
+            "never": {
+              "type": "object"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "PauseInfoResponse": {
+      "description": "Information about if the contract is currently paused.",
+      "oneOf": [
+        {
+          "type": "object",
+          "required": [
+            "Paused"
+          ],
+          "properties": {
+            "Paused": {
+              "type": "object",
+              "required": [
+                "expiration"
+              ],
+              "properties": {
+                "expiration": {
+                  "$ref": "#/definitions/Expiration"
+                }
+              }
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "type": "object",
+          "required": [
+            "Unpaused"
+          ],
+          "properties": {
+            "Unpaused": {
+              "type": "object"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
     }
   }
 }
diff --git a/contracts/cw-core/schema/execute_msg.json b/contracts/cw-core/schema/execute_msg.json
index d20e3cd9..d08685f6 100644
--- a/contracts/cw-core/schema/execute_msg.json
+++ b/contracts/cw-core/schema/execute_msg.json
@@ -99,6 +99,26 @@
       },
       "additionalProperties": false
     },
+    {
+      "type": "object",
+      "required": [
+        "pause"
+      ],
+      "properties": {
+        "pause": {
+          "type": "object",
+          "required": [
+            "duration"
+          ],
+          "properties": {
+            "duration": {
+              "$ref": "#/definitions/Duration"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
     {
       "description": "Adds an item to the governance contract's item map. If the item already exists the existing value is overriden. If the item does not exist a new item is added.",
       "type": "object",
@@ -550,6 +570,40 @@
         }
       ]
     },
+    "Duration": {
+      "description": "Duration is a delta of time. You can add it to a BlockInfo or Expiration to move that further in the future. Note that an height-based Duration and a time-based Expiration cannot be combined",
+      "oneOf": [
+        {
+          "type": "object",
+          "required": [
+            "height"
+          ],
+          "properties": {
+            "height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Time in seconds",
+          "type": "object",
+          "required": [
+            "time"
+          ],
+          "properties": {
+            "time": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
     "Empty": {
       "description": "An empty struct that serves as a placeholder in different places, such as contracts that don't set a custom message.\n\nIt is designed to be expressable in correct JSON and JSON Schema but contains no meaningful data. Previously we used enums without cases, but those cannot represented as valid JSON Schema (https://github.com/CosmWasm/cosmwasm/issues/451)",
       "type": "object"
diff --git a/contracts/cw-core/schema/pause_info_response.json b/contracts/cw-core/schema/pause_info_response.json
new file mode 100644
index 00000000..eb8e1aeb
--- /dev/null
+++ b/contracts/cw-core/schema/pause_info_response.json
@@ -0,0 +1,99 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "PauseInfoResponse",
+  "description": "Information about if the contract is currently paused.",
+  "oneOf": [
+    {
+      "type": "object",
+      "required": [
+        "Paused"
+      ],
+      "properties": {
+        "Paused": {
+          "type": "object",
+          "required": [
+            "expiration"
+          ],
+          "properties": {
+            "expiration": {
+              "$ref": "#/definitions/Expiration"
+            }
+          }
+        }
+      },
+      "additionalProperties": false
+    },
+    {
+      "type": "object",
+      "required": [
+        "Unpaused"
+      ],
+      "properties": {
+        "Unpaused": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    }
+  ],
+  "definitions": {
+    "Expiration": {
+      "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+      "oneOf": [
+        {
+          "description": "AtHeight will expire when `env.block.height` >= height",
+          "type": "object",
+          "required": [
+            "at_height"
+          ],
+          "properties": {
+            "at_height": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "AtTime will expire when `env.block.time` >= time",
+          "type": "object",
+          "required": [
+            "at_time"
+          ],
+          "properties": {
+            "at_time": {
+              "$ref": "#/definitions/Timestamp"
+            }
+          },
+          "additionalProperties": false
+        },
+        {
+          "description": "Never will never expire. Used to express the empty variant",
+          "type": "object",
+          "required": [
+            "never"
+          ],
+          "properties": {
+            "never": {
+              "type": "object"
+            }
+          },
+          "additionalProperties": false
+        }
+      ]
+    },
+    "Timestamp": {
+      "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+      "allOf": [
+        {
+          "$ref": "#/definitions/Uint64"
+        }
+      ]
+    },
+    "Uint64": {
+      "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+      "type": "string"
+    }
+  }
+}
diff --git a/contracts/cw-core/schema/query_msg.json b/contracts/cw-core/schema/query_msg.json
index 7e404832..75101399 100644
--- a/contracts/cw-core/schema/query_msg.json
+++ b/contracts/cw-core/schema/query_msg.json
@@ -15,6 +15,19 @@
       },
       "additionalProperties": false
     },
+    {
+      "description": "Returns information about if the contract is currently paused.",
+      "type": "object",
+      "required": [
+        "pause_info"
+      ],
+      "properties": {
+        "pause_info": {
+          "type": "object"
+        }
+      },
+      "additionalProperties": false
+    },
     {
       "description": "Gets the contract's voting module. Returns Addr.",
       "type": "object",
diff --git a/contracts/cw-core/src/contract.rs b/contracts/cw-core/src/contract.rs
index efb2025c..b5505928 100644
--- a/contracts/cw-core/src/contract.rs
+++ b/contracts/cw-core/src/contract.rs
@@ -8,15 +8,15 @@ use cosmwasm_std::{
 };
 use cw2::{get_contract_version, set_contract_version};
 use cw_storage_plus::{Bound, Map};
-use cw_utils::parse_reply_instantiate_data;
+use cw_utils::{parse_reply_instantiate_data, Duration};
 
 use cw_core_interface::voting;
 
 use crate::error::ContractError;
 use crate::msg::{ExecuteMsg, InitialItemInfo, InstantiateMsg, ModuleInstantiateInfo, QueryMsg};
-use crate::query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse};
+use crate::query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse, PauseInfoResponse};
 use crate::state::{
-    Config, CONFIG, CW20_LIST, CW721_LIST, ITEMS, PENDING_ITEM_INSTANTIATION_NAMES,
+    Config, CONFIG, CW20_LIST, CW721_LIST, ITEMS, PAUSED, PENDING_ITEM_INSTANTIATION_NAMES,
     PROPOSAL_MODULES, PROPOSAL_MODULE_COUNT, VOTING_MODULE,
 };
 
@@ -128,6 +128,13 @@ pub fn execute(
     info: MessageInfo,
     msg: ExecuteMsg,
 ) -> Result<Response, ContractError> {
+    // No actions can be performed while the DAO is paused.
+    if let Some(expiration) = PAUSED.may_load(deps.storage)? {
+        if !expiration.is_expired(&env.block) {
+            return Err(ContractError::Paused {});
+        }
+    }
+
     match msg {
         ExecuteMsg::ExecuteProposalHook { msgs } => {
             execute_proposal_hook(deps.as_ref(), info.sender, msgs)
@@ -151,9 +158,31 @@ pub fn execute(
         ExecuteMsg::UpdateCw721List { to_add, to_remove } => {
             execute_update_cw721_list(deps, env, info.sender, to_add, to_remove)
         }
+        ExecuteMsg::Pause { duration } => execute_pause(deps, env, info.sender, duration),
     }
 }
 
+pub fn execute_pause(
+    deps: DepsMut,
+    env: Env,
+    sender: Addr,
+    pause_duration: Duration,
+) -> Result<Response, ContractError> {
+    // Only the core contract may call this method.
+    if sender != env.contract.address {
+        return Err(ContractError::Unauthorized {});
+    }
+
+    let until = pause_duration.after(&env.block);
+
+    PAUSED.save(deps.storage, &until)?;
+
+    Ok(Response::new()
+        .add_attribute("action", "execute_pause")
+        .add_attribute("sender", sender)
+        .add_attribute("until", until.to_string()))
+}
+
 pub fn execute_proposal_hook(
     deps: Deps,
     sender: Addr,
@@ -371,7 +400,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::ProposalModules { start_at, limit } => {
             query_governance_modules(deps, start_at, limit)
         }
-        QueryMsg::DumpState {} => query_dump_state(deps),
+        QueryMsg::DumpState {} => query_dump_state(deps, env),
         QueryMsg::VotingPowerAtHeight { address, height } => {
             query_voting_power_at_height(deps, address, height)
         }
@@ -384,6 +413,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::Cw20Balances { start_at, limit } => {
             query_cw20_balances(deps, env, start_at, limit)
         }
+        QueryMsg::PauseInfo {} => query_paused(deps, env),
     }
 }
 
@@ -431,16 +461,35 @@ pub fn query_governance_modules(
     to_binary(&modules)
 }
 
-pub fn query_dump_state(deps: Deps) -> StdResult<Binary> {
+fn get_pause_info(deps: Deps, env: Env) -> StdResult<PauseInfoResponse> {
+    Ok(match PAUSED.may_load(deps.storage)? {
+        Some(expiration) => {
+            if expiration.is_expired(&env.block) {
+                PauseInfoResponse::Unpaused {}
+            } else {
+                PauseInfoResponse::Paused { expiration }
+            }
+        }
+        None => PauseInfoResponse::Unpaused {},
+    })
+}
+
+pub fn query_paused(deps: Deps, env: Env) -> StdResult<Binary> {
+    to_binary(&get_pause_info(deps, env)?)
+}
+
+pub fn query_dump_state(deps: Deps, env: Env) -> StdResult<Binary> {
     let config = CONFIG.load(deps.storage)?;
     let voting_module = VOTING_MODULE.load(deps.storage)?;
     let governance_modules = PROPOSAL_MODULES
         .keys(deps.storage, None, None, cosmwasm_std::Order::Descending)
         .collect::<Result<Vec<Addr>, _>>()?;
+    let pause_info = get_pause_info(deps, env)?;
     let version = get_contract_version(deps.storage)?;
     to_binary(&DumpStateResponse {
         config,
         version,
+        pause_info,
         governance_modules,
         voting_module,
     })
diff --git a/contracts/cw-core/src/error.rs b/contracts/cw-core/src/error.rs
index 500e409f..980e6bf4 100644
--- a/contracts/cw-core/src/error.rs
+++ b/contracts/cw-core/src/error.rs
@@ -10,6 +10,9 @@ pub enum ContractError {
     #[error("Unauthorized.")]
     Unauthorized {},
 
+    #[error("The contract is paused.")]
+    Paused {},
+
     #[error("No voting module provided.")]
     NoVotingModule {},
 
diff --git a/contracts/cw-core/src/msg.rs b/contracts/cw-core/src/msg.rs
index 818e4ea3..c2a7a1a1 100644
--- a/contracts/cw-core/src/msg.rs
+++ b/contracts/cw-core/src/msg.rs
@@ -1,4 +1,5 @@
 use cosmwasm_std::{Binary, CosmosMsg, Empty};
+use cw_utils::Duration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
@@ -83,27 +84,41 @@ pub struct InstantiateMsg {
 pub enum ExecuteMsg {
     /// Callable by proposal modules. The DAO will execute the
     /// messages in the hook in order.
-    ExecuteProposalHook { msgs: Vec<CosmosMsg<Empty>> },
+    ExecuteProposalHook {
+        msgs: Vec<CosmosMsg<Empty>>,
+    },
     /// Callable by the core contract. Replaces the current
-    /// core contract config with the provided config.
-    UpdateConfig { config: Config },
+    /// governance contract config with the provided config.
+    UpdateConfig {
+        config: Config,
+    },
     /// Callable by the core contract. Replaces the current
-    /// voting module with a new one instantiated by the core
+    /// voting module with a new one instantiated by the governance
     /// contract.
-    UpdateVotingModule { module: ModuleInstantiateInfo },
-    /// Updates the core contract's proposal modules. Module
+    UpdateVotingModule {
+        module: ModuleInstantiateInfo,
+    },
+    /// Updates the governance contract's governance modules. Module
     /// instantiate info in `to_add` is used to create new modules and
     /// install them.
     UpdateProposalModules {
         to_add: Vec<ModuleInstantiateInfo>,
         to_remove: Vec<String>,
     },
-    /// Adds an item to the core contract's item map. If the
+    Pause {
+        duration: Duration,
+    },
+    /// Adds an item to the governance contract's item map. If the
     /// item already exists the existing value is overriden. If the
     /// item does not exist a new item is added.
-    SetItem { key: String, addr: String },
-    /// Removes an item from the core contract's item map.
-    RemoveItem { key: String },
+    SetItem {
+        key: String,
+        addr: String,
+    },
+    /// Removes an item from the governance contract's item map.
+    RemoveItem {
+        key: String,
+    },
     /// Executed when the contract receives a cw20 token. Depending on
     /// the contract's configuration the contract will automatically
     /// add the token to its treasury.
@@ -130,6 +145,8 @@ pub enum ExecuteMsg {
 pub enum QueryMsg {
     /// Gets the contract's config. Returns Config.
     Config {},
+    /// Returns information about if the contract is currently paused.
+    PauseInfo {},
     /// Gets the contract's voting module. Returns Addr.
     VotingModule {},
     /// Gets the proposal modules assocaited with the
diff --git a/contracts/cw-core/src/query.rs b/contracts/cw-core/src/query.rs
index 45b2d7b4..d42bf860 100644
--- a/contracts/cw-core/src/query.rs
+++ b/contracts/cw-core/src/query.rs
@@ -1,5 +1,6 @@
 use cosmwasm_std::{Addr, Uint128};
 use cw2::ContractVersion;
+use cw_utils::Expiration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
@@ -11,6 +12,8 @@ use crate::state::Config;
 pub struct DumpStateResponse {
     /// The governance contract's config.
     pub config: Config,
+    // True if the contract is currently paused.
+    pub pause_info: PauseInfoResponse,
     /// The governance contract's version.
     pub version: ContractVersion,
     /// The governance modules associated with the governance
@@ -20,6 +23,13 @@ pub struct DumpStateResponse {
     pub voting_module: Addr,
 }
 
+/// Information about if the contract is currently paused.
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+pub enum PauseInfoResponse {
+    Paused { expiration: Expiration },
+    Unpaused {},
+}
+
 /// Returned by the `GetItem` query.
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 pub struct GetItemResponse {
diff --git a/contracts/cw-core/src/state.rs b/contracts/cw-core/src/state.rs
index 1c7ade53..fb72bbaa 100644
--- a/contracts/cw-core/src/state.rs
+++ b/contracts/cw-core/src/state.rs
@@ -1,3 +1,4 @@
+use cw_utils::Expiration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
@@ -22,8 +23,13 @@ pub struct Config {
 }
 
 pub const CONFIG: Item<Config> = Item::new("config");
+
+pub const PAUSED: Item<Expiration> = Item::new("paused");
+
+/// The voting module associated with this contract.
 pub const VOTING_MODULE: Item<Addr> = Item::new("voting_module");
 pub const PROPOSAL_MODULES: Map<Addr, Empty> = Map::new("governance_modules");
+
 pub const ITEMS: Map<String, Addr> = Map::new("items");
 pub const PENDING_ITEM_INSTANTIATION_NAMES: Map<u64, String> =
     Map::new("pending_item_instantiations");


Test patch:
diff --git a/contracts/cw-core/src/tests.rs b/contracts/cw-core/src/tests.rs
index 64a4e0b9..774f6643 100644
--- a/contracts/cw-core/src/tests.rs
+++ b/contracts/cw-core/src/tests.rs
@@ -2,13 +2,14 @@ use cosmwasm_std::{to_binary, Addr, Empty, Uint128, WasmMsg};
 use cw2::ContractVersion;
 use cw_core_interface::voting::VotingPowerAtHeightResponse;
 use cw_multi_test::{App, Contract, ContractWrapper, Executor};
+use cw_utils::{Duration, Expiration};
 
 use crate::{
     msg::{
         Admin, ExecuteMsg, InitialItem, InitialItemInfo, InstantiateMsg, ModuleInstantiateInfo,
         QueryMsg,
     },
-    query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse},
+    query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse, PauseInfoResponse},
     state::Config,
     ContractError,
 };
@@ -1281,3 +1282,237 @@ fn test_cw721_receive_no_auto_add() {
         .unwrap();
     assert_eq!(cw20_list, vec![Addr::unchecked("new"), cw721_addr]);
 }
+
+#[test]
+fn test_pause() {
+    let (core_addr, mut app) = do_standard_instantiate(false);
+
+    let start_height = app.block_info().height;
+
+    let proposal_modules: Vec<Addr> = app
+        .wrap()
+        .query_wasm_smart(
+            core_addr.clone(),
+            &QueryMsg::ProposalModules {
+                start_at: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(proposal_modules.len(), 1);
+    let proposal_module = proposal_modules.into_iter().next().unwrap();
+
+    let paused: PauseInfoResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {})
+        .unwrap();
+    assert_eq!(paused, PauseInfoResponse::Unpaused {});
+    let all_state: DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {})
+        .unwrap();
+    assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {});
+
+    // DAO is not paused. Check that we can execute things.
+    //
+    // Tests intentionally use the core address to send these
+    // messsages to simulate a worst case scenerio where the core
+    // contract has a vulnerability.
+    app.execute_contract(
+        core_addr.clone(),
+        core_addr.clone(),
+        &ExecuteMsg::UpdateConfig {
+            config: Config {
+                name: "The Empire Strikes Back".to_string(),
+                description: "haha lol we have pwned your DAO".to_string(),
+                image_url: None,
+                automatically_add_cw20s: true,
+                automatically_add_cw721s: true,
+            },
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Oh no the DAO is under attack! Quick! Pause the DAO while we
+    // figure out what to do!
+    let err: ContractError = app
+        .execute_contract(
+            proposal_module.clone(),
+            core_addr.clone(),
+            &ExecuteMsg::Pause {
+                duration: Duration::Height(10),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    // Only the DAO may call this on itself. Proposal modules must use
+    // the execute hook.
+    assert_eq!(err, ContractError::Unauthorized {});
+
+    app.execute_contract(
+        proposal_module.clone(),
+        core_addr.clone(),
+        &ExecuteMsg::ExecuteProposalHook {
+            msgs: vec![WasmMsg::Execute {
+                contract_addr: core_addr.to_string(),
+                msg: to_binary(&ExecuteMsg::Pause {
+                    duration: Duration::Height(10),
+                })
+                .unwrap(),
+                funds: vec![],
+            }
+            .into()],
+        },
+        &[],
+    )
+    .unwrap();
+
+    let paused: PauseInfoResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {})
+        .unwrap();
+    assert_eq!(
+        paused,
+        PauseInfoResponse::Paused {
+            expiration: Expiration::AtHeight(start_height + 10)
+        }
+    );
+    let all_state: DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {})
+        .unwrap();
+    assert_eq!(
+        all_state.pause_info,
+        PauseInfoResponse::Paused {
+            expiration: Expiration::AtHeight(start_height + 10)
+        }
+    );
+
+    let err: ContractError = app
+        .execute_contract(
+            core_addr.clone(),
+            core_addr.clone(),
+            &ExecuteMsg::UpdateConfig {
+                config: Config {
+                    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 { .. }));
+
+    let err: ContractError = app
+        .execute_contract(
+            proposal_module.clone(),
+            core_addr.clone(),
+            &ExecuteMsg::ExecuteProposalHook {
+                msgs: vec![WasmMsg::Execute {
+                    contract_addr: core_addr.to_string(),
+                    msg: to_binary(&ExecuteMsg::Pause {
+                        duration: Duration::Height(10),
+                    })
+                    .unwrap(),
+                    funds: vec![],
+                }
+                .into()],
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    assert!(matches!(err, ContractError::Paused { .. }));
+
+    app.update_block(|mut block| block.height += 9);
+
+    // Still not unpaused.
+    let err: ContractError = app
+        .execute_contract(
+            proposal_module.clone(),
+            core_addr.clone(),
+            &ExecuteMsg::ExecuteProposalHook {
+                msgs: vec![WasmMsg::Execute {
+                    contract_addr: core_addr.to_string(),
+                    msg: to_binary(&ExecuteMsg::Pause {
+                        duration: Duration::Height(10),
+                    })
+                    .unwrap(),
+                    funds: vec![],
+                }
+                .into()],
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    assert!(matches!(err, ContractError::Paused { .. }));
+
+    app.update_block(|mut block| block.height += 1);
+
+    let paused: PauseInfoResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {})
+        .unwrap();
+    assert_eq!(paused, PauseInfoResponse::Unpaused {});
+    let all_state: DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr.clone(), &QueryMsg::DumpState {})
+        .unwrap();
+    assert_eq!(all_state.pause_info, PauseInfoResponse::Unpaused {});
+
+    // Now its unpaused so we should be able to pause again.
+    app.execute_contract(
+        proposal_module,
+        core_addr.clone(),
+        &ExecuteMsg::ExecuteProposalHook {
+            msgs: vec![WasmMsg::Execute {
+                contract_addr: core_addr.to_string(),
+                msg: to_binary(&ExecuteMsg::Pause {
+                    duration: Duration::Height(10),
+                })
+                .unwrap(),
+                funds: vec![],
+            }
+            .into()],
+        },
+        &[],
+    )
+    .unwrap();
+
+    let paused: PauseInfoResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {})
+        .unwrap();
+    assert_eq!(
+        paused,
+        PauseInfoResponse::Paused {
+            expiration: Expiration::AtHeight(start_height + 20)
+        }
+    );
+    let all_state: DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr, &QueryMsg::DumpState {})
+        .unwrap();
+    assert_eq!(
+        all_state.pause_info,
+        PauseInfoResponse::Paused {
+            expiration: Expiration::AtHeight(start_height + 20)
+        }
+    );
+}