Repository: DA0-DA0/dao-contracts
Pull request #567 (2022-11-28 00:42:16)

Various little cleanups
I messed up the rebase and this got lost in the last merge to main. This reapplies it. Duplicate of https://github.com/DA0-DA0/dao-contracts/pull/563


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 06d9d106..e66be58c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1161,6 +1161,7 @@ dependencies = [
  "cosmwasm-schema",
  "cosmwasm-std",
  "cw-denom",
+ "cw-hooks",
  "cw-multi-test",
  "cw-utils 0.16.0",
  "cw2 0.16.0",
diff --git a/README.md b/README.md
index 3f031773..2d2c4eda 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,14 @@ and [Oak
 Security](https://github.com/oak-security/audit-reports/blob/2377ba8cfcfd505283c789d706311b06771d6db4/DAO%20DAO/2022-06-22%20Audit%20Report%20-%20DAO%20DAO%20v1.0.pdf)
 on the `v1.0.0` tag. An audit for the v2 packages is forthcoming.
 
+## Links and Resources
+
+- [DAO DAO DAO](https://daodao.zone/dao/juno10h0hc64jv006rr8qy0zhlu4jsxct8qwa0vtaleayh0ujz0zynf2s2r7v8q)
+- [Discord](https://discord.gg/sAaGuyW3D2)
+- [Docs](https://docs.daodao.zone)
+- [Manually Instantiating a DAO](https://github.com/DA0-DA0/dao-contracts/wiki/Instantiating-a-DAO)
+- [Twitter](https://github.com/DA0-DA0)
+- [What is a DAO?](https://docs.daodao.zone/docs/introduction/what-is-dao)
 
 ## Developers
 
diff --git a/contracts/README.md b/contracts/README.md
index a4a886dd..cdb9992f 100644
--- a/contracts/README.md
+++ b/contracts/README.md
@@ -1,3 +1,5 @@
+# DAO Contracts
+
 - `dao-core` - the core module for DAOs.
 - `external` - contracts used by DAOs that are not part of a DAO
   module.
@@ -5,9 +7,6 @@
 - `proposal` - proposal modules.
 - `voting` - voting modules.
 - `staking` - cw20 staking functionality and a staking rewards
-  system. These contracts are used by [Wasmswap] as well as DAO DAO.
-
-For a description of each module type, see [our wiki].
+  system. These contracts are used by [Wasmswap](https://github.com/Wasmswap) as well as DAO DAO.
 
-[Wasmswap]: https://github.com/Wasmswap
-[our wiki]: https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design
+For a description of each module type, see [our wiki](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design).
diff --git a/contracts/dao-core/README.md b/contracts/dao-core/README.md
index 808989a1..72e1bfbd 100644
--- a/contracts/dao-core/README.md
+++ b/contracts/dao-core/README.md
@@ -5,8 +5,7 @@ management of voting power and proposal modules, executes messages,
 and holds the DAO's treasury.
 
 For more information about how these modules fit together see
-[this](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-v1-Contracts-Design)
-wiki page.
+[this wiki page](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design).
 
 In additon to the wiki spec this contract may also pause. To do so a
 `Pause` message must by executed by a proposal module. Pausing the
diff --git a/contracts/dao-core/schema/dao-core.json b/contracts/dao-core/schema/dao-core.json
index e27f43ce..4018ce38 100644
--- a/contracts/dao-core/schema/dao-core.json
+++ b/contracts/dao-core/schema/dao-core.json
@@ -321,14 +321,14 @@
           "set_item": {
             "type": "object",
             "required": [
-              "addr",
-              "key"
+              "key",
+              "value"
             ],
             "properties": {
-              "addr": {
+              "key": {
                 "type": "string"
               },
-              "key": {
+              "value": {
                 "type": "string"
               }
             },
diff --git a/contracts/dao-core/src/contract.rs b/contracts/dao-core/src/contract.rs
index 86ecf8f1..90abc6dd 100644
--- a/contracts/dao-core/src/contract.rs
+++ b/contracts/dao-core/src/contract.rs
@@ -112,7 +112,7 @@ pub fn execute(
         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),
-        ExecuteMsg::SetItem { key, addr } => execute_set_item(deps, env, info.sender, key, addr),
+        ExecuteMsg::SetItem { key, value } => execute_set_item(deps, env, info.sender, key, value),
         ExecuteMsg::UpdateConfig { config } => {
             execute_update_config(deps, env, info.sender, config)
         }
diff --git a/contracts/dao-core/src/msg.rs b/contracts/dao-core/src/msg.rs
index 76f6fe30..45410651 100644
--- a/contracts/dao-core/src/msg.rs
+++ b/contracts/dao-core/src/msg.rs
@@ -76,7 +76,7 @@ pub enum ExecuteMsg {
     /// 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 },
+    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
diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/README.md b/contracts/pre-propose/dao-pre-propose-approval-single/README.md
index bd5bc473..84de2b61 100644
--- a/contracts/pre-propose/dao-pre-propose-approval-single/README.md
+++ b/contracts/pre-propose/dao-pre-propose-approval-single/README.md
@@ -6,7 +6,7 @@ This contract implements an approval flow for proposals, it also handles deposit
 
 This contract is instantatied with an `approver` address. This address is allowed to approve or reject the proposal.
 
-```
+```text
       ┌──────────┐
       │          │
       │  Account │
diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs
index 48ac98a9..8bcbe95b 100644
--- a/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs
+++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/contract.rs
@@ -48,11 +48,9 @@ pub fn execute(
     match msg {
         ExecuteMsg::Propose { msg } => execute_propose(deps, env, info, msg),
 
-        // TODO(zeke): why not use behavior of base?
         ExecuteMsg::AddProposalSubmittedHook { address } => {
             execute_add_approver_hook(deps, info, address)
         }
-        // TODO(zeke): why not use behavior of base?
         ExecuteMsg::RemoveProposalSubmittedHook { address } => {
             execute_remove_approver_hook(deps, info, address)
         }
diff --git a/contracts/pre-propose/dao-pre-propose-approval-single/src/lib.rs b/contracts/pre-propose/dao-pre-propose-approval-single/src/lib.rs
index c084158e..47ba4170 100644
--- a/contracts/pre-propose/dao-pre-propose-approval-single/src/lib.rs
+++ b/contracts/pre-propose/dao-pre-propose-approval-single/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 pub mod msg;
 pub mod state;
diff --git a/contracts/pre-propose/dao-pre-propose-approver/README.md b/contracts/pre-propose/dao-pre-propose-approver/README.md
index 973731df..4d51bde8 100644
--- a/contracts/pre-propose/dao-pre-propose-approver/README.md
+++ b/contracts/pre-propose/dao-pre-propose-approver/README.md
@@ -8,7 +8,7 @@ On instantiation, this contract registers a hook with the approval contract to a
 
 When this contract recieves a proposal as hook from `cwd-pre-propose-approval-single`, it makes an approval propose in the approval DAO. If approved, the approval proposal calls the approve message on this contract when executed. If the proposal is rejected and closed it fires off reject call.
 
-``` 
+```text
 ┌──────────┐         Approver DAO Registers Prop Submission Hook
 │          │       ┌──────────────────────────────────────────────┐
 │  Account │       │                                              │
diff --git a/contracts/pre-propose/dao-pre-propose-approver/src/lib.rs b/contracts/pre-propose/dao-pre-propose-approver/src/lib.rs
index c084158e..47ba4170 100644
--- a/contracts/pre-propose/dao-pre-propose-approver/src/lib.rs
+++ b/contracts/pre-propose/dao-pre-propose-approver/src/lib.rs
@@ -1,3 +1,5 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
 pub mod contract;
 pub mod msg;
 pub mod state;
diff --git a/contracts/pre-propose/dao-pre-propose-single/Cargo.toml b/contracts/pre-propose/dao-pre-propose-single/Cargo.toml
index 48470561..283b7831 100644
--- a/contracts/pre-propose/dao-pre-propose-single/Cargo.toml
+++ b/contracts/pre-propose/dao-pre-propose-single/Cargo.toml
@@ -36,4 +36,5 @@ cw-denom = { workspace = true }
 dao-interface = { workspace = true }
 dao-testing = { workspace = true }
 dao-proposal-hooks = { workspace = true }
-dao-proposal-single = { workspace = true }
\ No newline at end of file
+dao-proposal-single = { workspace = true }
+cw-hooks = { workspace = true }
\ No newline at end of file
diff --git a/contracts/proposal/dao-proposal-multiple/README.md b/contracts/proposal/dao-proposal-multiple/README.md
index f44670e2..72520af7 100644
--- a/contracts/proposal/dao-proposal-multiple/README.md
+++ b/contracts/proposal/dao-proposal-multiple/README.md
@@ -10,6 +10,8 @@ where the proposal is no longer being evaluated (e.g. met the quorum and
 been rejected), this allows voters to reflect their opinion even though
 it has no effect on the final proposal's status.
 
+You can read more about this module in [our wiki](https://github.com/DA0-DA0/dao-contracts/wiki/Multiple-Choice-Proposal-Module).
+
 ## Undesired behavior
 
 The undesired behavior of this contract is tested under `testing/adversarial_tests.rs`.
diff --git a/contracts/proposal/dao-proposal-single/README.md b/contracts/proposal/dao-proposal-single/README.md
index 9c9b0caa..78c547f1 100644
--- a/contracts/proposal/dao-proposal-single/README.md
+++ b/contracts/proposal/dao-proposal-single/README.md
@@ -11,7 +11,7 @@ been rejected), this allows voters to reflect their opinion even though
 it has no effect on the final proposal's status.
 
 For more information about how these modules fit together see
-[this](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-v1-Contracts-Design)
+[this](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design)
 wiki page.
 
 For information about how this module counts votes and handles passing
diff --git a/contracts/voting/dao-voting-cw20-staked/README.md b/contracts/voting/dao-voting-cw20-staked/README.md
index 629b3928..78fb5260 100644
--- a/contracts/voting/dao-voting-cw20-staked/README.md
+++ b/contracts/voting/dao-voting-cw20-staked/README.md
@@ -3,8 +3,11 @@
 A voting power module which determines voting power based on the
 staked token balance of specific addresses at given heights.
 
-Also features the functionality to set an active threshold, this
-threshold allows DAOs to be marked as inactive if it is not met.  This
+This contract implements the interface needed to be a DAO
+DAO [voting
+module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module).
+It also features the functionality to set an active threshold, this
+threshold allows DAOs to be marked as inactive if it is not met. This
 threshold can either be an absolute count of tokens staked or a
 percentage of the token's total supply.
 
diff --git a/contracts/voting/dao-voting-cw4/README.md b/contracts/voting/dao-voting-cw4/README.md
index 96b42387..14793999 100644
--- a/contracts/voting/dao-voting-cw4/README.md
+++ b/contracts/voting/dao-voting-cw4/README.md
@@ -4,9 +4,12 @@ A simple voting power module which determines voting power based on
 the weight of a user in a cw4-group contract. This allocates voting
 power in the same way that one would expect a multisig to.
 
+This contract implements the interface needed to be a DAO
+DAO [voting
+module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module).
 For more information about how these modules fit together see
-[this](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-v1-Contracts-Design)
-wiki page.
+[this](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design)
+wiki page. 
 
 ## Receiving updates
 
diff --git a/contracts/voting/dao-voting-native-staked/README.md b/contracts/voting/dao-voting-native-staked/README.md
index bc25443c..eacb5d9e 100644
--- a/contracts/voting/dao-voting-native-staked/README.md
+++ b/contracts/voting/dao-voting-native-staked/README.md
@@ -3,5 +3,7 @@
 Simple native token voting contract which assumes the native denom
 provided is not used for staking for securing the network e.g. IBC
 denoms or secondary tokens (ION). Staked balances may be queried at an
-arbitrary height. This contract implements the interface needed to be
-a DAO DAO voting module.
+arbitrary height. This contract implements the interface needed to be a DAO
+DAO [voting
+module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module).
+
diff --git a/contracts/voting/dao-voting-staking-denom-staked/README.md b/contracts/voting/dao-voting-staking-denom-staked/README.md
index 8b2bd25c..4c85ffa7 100644
--- a/contracts/voting/dao-voting-staking-denom-staked/README.md
+++ b/contracts/voting/dao-voting-staking-denom-staked/README.md
@@ -3,7 +3,9 @@
 This voting module determines voting power based on staked native
 tokens in the bank module. You could, for example, use this to allow
 Juno stakers to vote on proposals using the same voting power they use
-when doing native SDK governance votes.
+when doing native SDK governance votes. This contract implements the 
+interface needed to be a DAO DAO [voting
+module](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#the-voting-module).
 
 ~WARNING~ This module behaves differently than other DAO DAO voting
 modules. Specifically:
diff --git a/packages/dao-pre-propose-base/README.md b/packages/dao-pre-propose-base/README.md
index 827a7e67..025f8026 100644
--- a/packages/dao-pre-propose-base/README.md
+++ b/packages/dao-pre-propose-base/README.md
@@ -1,11 +1,13 @@
 # Pre-Propose Base
 
-This provides a base package that may be used to implement pre-propose
-modules. It is modeled after the
+This provides a base package that may be used to implement [pre-propose
+modules](https://github.com/DA0-DA0/dao-contracts/wiki/DAO-DAO-Contracts-Design#pre-propose-modules).
+It is modeled after the
 [cw721-base](https://github.com/CosmWasm/cw-nfts/tree/27ffdc6c24c2d173be6c677d04bec1420191184d/contracts/cw721-base)
 contract for implementing NFT contracts.
 
-See the
-[pre-propose-single](../../contracts/pre-propose/dao-pre-propose-single)
+See the [pre-propose-single](../../contracts/pre-propose/dao-pre-propose-single)
 contract for an example of using this package to implement a proposal
 module with deposits.
+
+Our wiki has more info on [pre-propose module design](https://github.com/DA0-DA0/dao-contracts/wiki/Pre-propose-module-design).
diff --git a/packages/dao-pre-propose-base/src/execute.rs b/packages/dao-pre-propose-base/src/execute.rs
index f9f18228..225a465f 100644
--- a/packages/dao-pre-propose-base/src/execute.rs
+++ b/packages/dao-pre-propose-base/src/execute.rs
@@ -111,21 +111,9 @@ where
         info: MessageInfo,
         msg: ProposalMessage,
     ) -> Result<Response, PreProposeError> {
-        let config = self.config.load(deps.storage)?;
+        self.check_can_submit(deps.as_ref(), info.sender.clone())?;
 
-        if !config.open_proposal_submission {
-            let dao = self.dao.load(deps.storage)?;
-            let voting_power: VotingPowerAtHeightResponse = deps.querier.query_wasm_smart(
-                dao.into_string(),
-                &CwCoreQuery::VotingPowerAtHeight {
-                    address: info.sender.to_string(),
-                    height: None,
-                },
-            )?;
-            if voting_power.power.is_zero() {
-                return Err(PreProposeError::NotMember {});
-            }
-        }
+        let config = self.config.load(deps.storage)?;
 
         let deposit_messages = if let Some(ref deposit_info) = config.deposit_info {
             deposit_info.check_native_deposit_paid(&info)?;
diff --git a/packages/dao-pre-propose-base/src/lib.rs b/packages/dao-pre-propose-base/src/lib.rs
index e95d8f60..be004003 100644
--- a/packages/dao-pre-propose-base/src/lib.rs
+++ b/packages/dao-pre-propose-base/src/lib.rs
@@ -4,3 +4,6 @@ pub mod error;
 pub mod execute;
 pub mod msg;
 pub mod state;
+
+#[cfg(test)]
+mod tests;
diff --git a/packages/dao-proposal-hooks/README.md b/packages/dao-proposal-hooks/README.md
index dd30e1b2..2ab9ba5b 100644
--- a/packages/dao-proposal-hooks/README.md
+++ b/packages/dao-proposal-hooks/README.md
@@ -2,3 +2,9 @@
 
 This package provides an interface for managing and dispatching
 proposal hooks from a proposal module.
+
+There are two types of proposal hooks:
+- **New Proposal Hook:** fired when a new proposal is created.
+- **Proposal Staus Changed Hook:** fired when a proposal's status changes. 
+
+Our wiki contains more info on [Proposal Hooks](https://github.com/DA0-DA0/dao-contracts/wiki/Proposal-Hooks-Interactions).
diff --git a/packages/dao-vote-hooks/README.md b/packages/dao-vote-hooks/README.md
index 29e6b0f9..ae2be9cc 100644
--- a/packages/dao-vote-hooks/README.md
+++ b/packages/dao-vote-hooks/README.md
@@ -1,4 +1,7 @@
 # CosmWasm DAO Vote Hooks
 
 This package provides an interface for managing and dispatching
-vote hooks from a proposal module.
+vote hooks from a proposal module. Vote hooks are fired when new
+votes are cast.
+
+You can read more about vote hooks in our [wiki](https://github.com/DA0-DA0/dao-contracts/wiki/Proposal-Hooks-Interactions).
diff --git a/typescript/contracts/dao-core/DaoCore.client.ts b/typescript/contracts/dao-core/DaoCore.client.ts
index 4c160a63..c0f870a2 100644
--- a/typescript/contracts/dao-core/DaoCore.client.ts
+++ b/typescript/contracts/dao-core/DaoCore.client.ts
@@ -328,11 +328,11 @@ export interface DaoCoreInterface extends DaoCoreReadOnlyInterface {
     key: string;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   setItem: ({
-    addr,
-    key
+    key,
+    value
   }: {
-    addr: string;
     key: string;
+    value: string;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   nominateAdmin: ({
     admin
@@ -487,16 +487,16 @@ export class DaoCoreClient extends DaoCoreQueryClient implements DaoCoreInterfac
     }, fee, memo, funds);
   };
   setItem = async ({
-    addr,
-    key
+    key,
+    value
   }: {
-    addr: string;
     key: string;
+    value: string;
   }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
     return await this.client.execute(this.sender, this.contractAddress, {
       set_item: {
-        addr,
-        key
+        key,
+        value
       }
     }, fee, memo, funds);
   };
diff --git a/typescript/contracts/dao-core/DaoCore.types.ts b/typescript/contracts/dao-core/DaoCore.types.ts
index a98d02a6..a241a327 100644
--- a/typescript/contracts/dao-core/DaoCore.types.ts
+++ b/typescript/contracts/dao-core/DaoCore.types.ts
@@ -56,8 +56,8 @@ export type ExecuteMsg = {
   };
 } | {
   set_item: {
-    addr: string;
     key: string;
+    value: string;
   };
 } | {
   nominate_admin: {


Test patch:
diff --git a/ci/integration-tests/src/tests/cw_core_test.rs b/ci/integration-tests/src/tests/cw_core_test.rs
index 2ecdfdad..662d5793 100644
--- a/ci/integration-tests/src/tests/cw_core_test.rs
+++ b/ci/integration-tests/src/tests/cw_core_test.rs
@@ -141,7 +141,7 @@ fn execute_items(chain: &mut Chain) {
                     contract_addr: dao.addr.clone(),
                     msg: to_binary(&dao_core::msg::ExecuteMsg::SetItem {
                         key: "meme".to_string(),
-                        addr: "foobar".to_string(),
+                        value: "foobar".to_string(),
                     })
                     .unwrap(),
                     funds: vec![],
diff --git a/contracts/dao-core/src/tests.rs b/contracts/dao-core/src/tests.rs
index d66ad235..e50f409f 100644
--- a/contracts/dao-core/src/tests.rs
+++ b/contracts/dao-core/src/tests.rs
@@ -4,10 +4,14 @@ use cosmwasm_std::{
     testing::{mock_dependencies, mock_env},
     to_binary, Addr, CosmosMsg, Empty, Storage, Uint128, WasmMsg,
 };
+use cw2::ContractVersion;
 use cw_multi_test::{App, Contract, ContractWrapper, Executor};
 use cw_storage_plus::{Item, Map};
 use cw_utils::{Duration, Expiration};
-use dao_interface::{voting::VotingPowerAtHeightResponse, Admin, ModuleInstantiateInfo};
+use dao_interface::{
+    voting::{InfoResponse, VotingPowerAtHeightResponse},
+    Admin, ModuleInstantiateInfo,
+};
 
 use crate::{
     contract::{derive_proposal_module_prefix, migrate, CONTRACT_NAME, CONTRACT_VERSION},
@@ -601,6 +605,20 @@ fn test_removed_modules_can_not_execute() {
         }
     ));
 
+    // Check that the enabled query works.
+    let enabled_modules: Vec<ProposalModule> = app
+        .wrap()
+        .query_wasm_smart(
+            &gov_addr,
+            &QueryMsg::ActiveProposalModules {
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(enabled_modules, vec![new_proposal_module.clone()]);
+
     // The new proposal module should be able to perform actions.
     app.execute_contract(
         Addr::unchecked(CREATOR_ADDR),
@@ -1423,11 +1441,11 @@ fn test_passthrough_voting_queries() {
     );
 }
 
-fn set_item(app: &mut App, gov_addr: Addr, key: String, addr: String) {
+fn set_item(app: &mut App, gov_addr: Addr, key: String, value: String) {
     app.execute_contract(
         gov_addr.clone(),
         gov_addr,
-        &ExecuteMsg::SetItem { key, addr },
+        &ExecuteMsg::SetItem { key, value },
         &[],
     )
     .unwrap();
@@ -1466,6 +1484,40 @@ fn list_items(
         .unwrap()
 }
 
+#[test]
+fn test_item_permissions() {
+    let (gov_addr, mut app) = do_standard_instantiate(true, None);
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked("ekez"),
+            gov_addr.clone(),
+            &ExecuteMsg::SetItem {
+                key: "k".to_string(),
+                value: "v".to_string(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::Unauthorized {});
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked("ekez"),
+            gov_addr,
+            &ExecuteMsg::RemoveItem {
+                key: "k".to_string(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::Unauthorized {});
+}
+
 #[test]
 fn test_add_remove_get() {
     let (gov_addr, mut app) = do_standard_instantiate(true, None);
@@ -1815,6 +1867,22 @@ fn test_cw20_receive_auto_add() {
         .unwrap();
     assert!(matches!(err, ContractError::Std(_)));
 
+    // Test that non-DAO can not update the list.
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked("ekez"),
+            gov_addr.clone(),
+            &ExecuteMsg::UpdateCw20List {
+                to_add: vec![],
+                to_remove: vec![gov_token.to_string()],
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert!(matches!(err, ContractError::Unauthorized {}));
+
     app.execute_contract(
         Addr::unchecked(gov_addr.clone()),
         gov_addr.clone(),
@@ -2015,6 +2083,22 @@ fn test_cw721_receive() {
         .unwrap();
     assert!(matches!(err, ContractError::Std(_)));
 
+    // Test that non-DAO can not update the list.
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked("ekez"),
+            gov_addr.clone(),
+            &ExecuteMsg::UpdateCw721List {
+                to_add: vec![],
+                to_remove: vec![cw721_addr.to_string()],
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert!(matches!(err, ContractError::Unauthorized {}));
+
     // Add a real cw721.
     app.execute_contract(
         Addr::unchecked(gov_addr.clone()),
@@ -2901,3 +2985,21 @@ pub fn test_migrate_update_version() {
     assert_eq!(version.version, CONTRACT_VERSION);
     assert_eq!(version.contract, CONTRACT_NAME);
 }
+
+#[test]
+fn test_query_info() {
+    let (core_addr, app) = do_standard_instantiate(true, None);
+    let res: InfoResponse = app
+        .wrap()
+        .query_wasm_smart(core_addr, &QueryMsg::Info {})
+        .unwrap();
+    assert_eq!(
+        res,
+        InfoResponse {
+            info: ContractVersion {
+                contract: CONTRACT_NAME.to_string(),
+                version: CONTRACT_VERSION.to_string()
+            }
+        }
+    )
+}
diff --git a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs
index 716a4151..a93c78b5 100644
--- a/contracts/pre-propose/dao-pre-propose-single/src/tests.rs
+++ b/contracts/pre-propose/dao-pre-propose-single/src/tests.rs
@@ -237,6 +237,30 @@ fn increase_allowance(app: &mut App, sender: &str, receiver: &Addr, cw20: Addr,
     .unwrap();
 }
 
+fn add_hook(app: &mut App, sender: &str, module: &Addr, hook: &str) {
+    app.execute_contract(
+        Addr::unchecked(sender),
+        module.clone(),
+        &ExecuteMsg::AddProposalSubmittedHook {
+            address: hook.to_string(),
+        },
+        &[],
+    )
+    .unwrap();
+}
+
+fn remove_hook(app: &mut App, sender: &str, module: &Addr, hook: &str) {
+    app.execute_contract(
+        Addr::unchecked(sender),
+        module.clone(),
+        &ExecuteMsg::RemoveProposalSubmittedHook {
+            address: hook.to_string(),
+        },
+        &[],
+    )
+    .unwrap();
+}
+
 fn get_balance_cw20<T: Into<String>, U: Into<String>>(
     app: &App,
     contract_addr: T,
@@ -287,6 +311,12 @@ fn get_dao(app: &App, module: Addr) -> Addr {
         .unwrap()
 }
 
+fn query_hooks(app: &App, module: Addr) -> cw_hooks::HooksResponse {
+    app.wrap()
+        .query_wasm_smart(module, &QueryMsg::ProposalSubmittedHooks {})
+        .unwrap()
+}
+
 fn get_proposal_module(app: &App, module: Addr) -> Addr {
     app.wrap()
         .query_wasm_smart(module, &QueryMsg::ProposalModule {})
@@ -1305,3 +1335,21 @@ fn test_withdraw() {
     let balance = get_balance_native(&app, core_addr.as_str(), "ujuno");
     assert_eq!(balance, Uint128::new(30));
 }
+
+#[test]
+fn test_hook_management() {
+    let app = &mut App::default();
+    let DefaultTestSetup {
+        core_addr,
+        proposal_single: _,
+        pre_propose,
+    } = setup_default_test(app, None, true);
+
+    add_hook(app, core_addr.as_str(), &pre_propose, "one");
+    add_hook(app, core_addr.as_str(), &pre_propose, "two");
+
+    remove_hook(app, core_addr.as_str(), &pre_propose, "one");
+
+    let hooks = query_hooks(app, pre_propose).hooks;
+    assert_eq!(hooks, vec!["two".to_string()])
+}
diff --git a/packages/dao-pre-propose-base/src/tests.rs b/packages/dao-pre-propose-base/src/tests.rs
new file mode 100644
index 00000000..f04c6ed5
--- /dev/null
+++ b/packages/dao-pre-propose-base/src/tests.rs
@@ -0,0 +1,202 @@
+use cosmwasm_std::{
+    from_binary,
+    testing::{mock_dependencies, mock_env, mock_info},
+    to_binary, Addr, Binary, ContractResult, Empty, Response, SubMsg, WasmMsg,
+};
+use cw_hooks::HooksResponse;
+use dao_voting::status::Status;
+
+use crate::{
+    error::PreProposeError,
+    msg::{ExecuteMsg, QueryMsg},
+    state::{Config, PreProposeContract},
+};
+
+type Contract = PreProposeContract<Empty, Empty, Empty, Empty>;
+
+#[test]
+fn test_completed_hook_status_invariant() {
+    let mut deps = mock_dependencies();
+    let info = mock_info("pm", &[]);
+
+    let module = Contract::default();
+
+    module
+        .proposal_module
+        .save(&mut deps.storage, &Addr::unchecked("pm"))
+        .unwrap();
+
+    let res = module.execute(
+        deps.as_mut(),
+        mock_env(),
+        info,
+        ExecuteMsg::ProposalCompletedHook {
+            proposal_id: 1,
+            new_status: Status::Passed,
+        },
+    );
+
+    assert_eq!(
+        res.unwrap_err(),
+        PreProposeError::NotClosedOrExecuted {
+            status: Status::Passed
+        }
+    );
+}
+
+#[test]
+fn test_completed_hook_auth() {
+    let mut deps = mock_dependencies();
+    let info = mock_info("evil", &[]);
+    let module = Contract::default();
+
+    module
+        .proposal_module
+        .save(&mut deps.storage, &Addr::unchecked("pm"))
+        .unwrap();
+
+    let res = module.execute(
+        deps.as_mut(),
+        mock_env(),
+        info,
+        ExecuteMsg::ProposalCompletedHook {
+            proposal_id: 1,
+            new_status: Status::Passed,
+        },
+    );
+
+    assert_eq!(res.unwrap_err(), PreProposeError::NotModule {});
+}
+
+#[test]
+fn test_proposal_submitted_hooks() {
+    let mut deps = mock_dependencies();
+    let module = Contract::default();
+
+    module
+        .dao
+        .save(&mut deps.storage, &Addr::unchecked("d"))
+        .unwrap();
+    module
+        .proposal_module
+        .save(&mut deps.storage, &Addr::unchecked("pm"))
+        .unwrap();
+    module
+        .config
+        .save(
+            &mut deps.storage,
+            &Config {
+                deposit_info: None,
+                open_proposal_submission: true,
+            },
+        )
+        .unwrap();
+
+    // The DAO can add a hook.
+    let info = mock_info("d", &[]);
+    module
+        .execute_add_proposal_submitted_hook(deps.as_mut(), info, "one".to_string())
+        .unwrap();
+    let hooks: HooksResponse = from_binary(
+        &module
+            .query(
+                deps.as_ref(),
+                mock_env(),
+                QueryMsg::ProposalSubmittedHooks {},
+            )
+            .unwrap(),
+    )
+    .unwrap();
+    assert_eq!(hooks.hooks, vec!["one".to_string()]);
+
+    // Non-DAO addresses can not add hooks.
+    let info = mock_info("n", &[]);
+    let err = module
+        .execute_add_proposal_submitted_hook(deps.as_mut(), info, "two".to_string())
+        .unwrap_err();
+    assert_eq!(err, PreProposeError::NotDao {});
+
+    deps.querier.update_wasm(|_| {
+        // for responding to the next proposal ID query that gets fired by propose.
+        cosmwasm_std::SystemResult::Ok(ContractResult::Ok(to_binary(&1u64).unwrap()))
+    });
+
+    // The hooks fire when a proposal is created.
+    let res = module
+        .execute(
+            deps.as_mut(),
+            mock_env(),
+            mock_info("a", &[]),
+            ExecuteMsg::Propose {
+                msg: Empty::default(),
+            },
+        )
+        .unwrap();
+    assert_eq!(
+        res.messages[1],
+        SubMsg::new(WasmMsg::Execute {
+            contract_addr: "one".to_string(),
+            msg: to_binary(&Empty::default()).unwrap(),
+            funds: vec![],
+        })
+    );
+
+    // Non-DAO addresses can not remove hooks.
+    let info = mock_info("n", &[]);
+    let err = module
+        .execute_remove_proposal_submitted_hook(deps.as_mut(), info, "one".to_string())
+        .unwrap_err();
+    assert_eq!(err, PreProposeError::NotDao {});
+
+    // The DAO can remove a hook.
+    let info = mock_info("d", &[]);
+    module
+        .execute_remove_proposal_submitted_hook(deps.as_mut(), info, "one".to_string())
+        .unwrap();
+    let hooks: HooksResponse = from_binary(
+        &module
+            .query(
+                deps.as_ref(),
+                mock_env(),
+                QueryMsg::ProposalSubmittedHooks {},
+            )
+            .unwrap(),
+    )
+    .unwrap();
+    assert!(hooks.hooks.is_empty());
+}
+
+#[test]
+fn test_query_ext_does_nothing() {
+    let deps = mock_dependencies();
+    let module = Contract::default();
+
+    let res = module
+        .query(
+            deps.as_ref(),
+            mock_env(),
+            QueryMsg::QueryExtension {
+                msg: Empty::default(),
+            },
+        )
+        .unwrap();
+    assert_eq!(res, Binary::default())
+}
+
+#[test]
+fn test_execute_ext_does_nothing() {
+    let mut deps = mock_dependencies();
+    let module = Contract::default();
+
+    let res = module
+        .execute(
+            deps.as_mut(),
+            mock_env(),
+            mock_info("addr", &[]),
+            ExecuteMsg::Extension {
+                msg: Empty::default(),
+            },
+        )
+        .unwrap();
+    assert_eq!(res, Response::default())
+}