Repository: DA0-DA0/dao-contracts
Pull request #646 (2023-02-08 13:10:59)

Dao migrator extend multiple proposal modules
Now migrator is able to migrate DAOs that have the edge case of multiple proposal modules, same length of proposal params should be passed as well.

Please note the changes in the accepted params (wrapped the proposal migration params in another type)


Patch:
diff --git a/contracts/dao-core/schema/dao-core.json b/contracts/dao-core/schema/dao-core.json
index e98dc6e6..507ab33e 100644
--- a/contracts/dao-core/schema/dao-core.json
+++ b/contracts/dao-core/schema/dao-core.json
@@ -2060,16 +2060,11 @@
         "additionalProperties": false
       },
       "MigrationModuleParams": {
-        "description": "The params we need to provide for migration msgs",
         "type": "object",
         "required": [
-          "close_proposal_on_execution_failure",
-          "pre_propose_info"
+          "proposal_params"
         ],
         "properties": {
-          "close_proposal_on_execution_failure": {
-            "type": "boolean"
-          },
           "migrate_stake_cw20_manager": {
             "description": "Rather or not to migrate the stake_cw20 contract and its manager. If this is not set to true and a stake_cw20 contract is detected in the DAO's configuration the migration will be aborted.",
             "type": [
@@ -2077,8 +2072,11 @@
               "null"
             ]
           },
-          "pre_propose_info": {
-            "$ref": "#/definitions/PreProposeInfo"
+          "proposal_params": {
+            "type": "array",
+            "items": {
+              "$ref": "#/definitions/ProposalParams"
+            }
           }
         },
         "additionalProperties": false
@@ -2164,6 +2162,23 @@
           }
         ]
       },
+      "ProposalParams": {
+        "description": "The params we need to provide for migration msgs",
+        "type": "object",
+        "required": [
+          "close_proposal_on_execution_failure",
+          "pre_propose_info"
+        ],
+        "properties": {
+          "close_proposal_on_execution_failure": {
+            "type": "boolean"
+          },
+          "pre_propose_info": {
+            "$ref": "#/definitions/PreProposeInfo"
+          }
+        },
+        "additionalProperties": false
+      },
       "SubDao": {
         "type": "object",
         "required": [
diff --git a/contracts/dao-core/src/migrate_msg.rs b/contracts/dao-core/src/migrate_msg.rs
index 78648531..64798298 100644
--- a/contracts/dao-core/src/migrate_msg.rs
+++ b/contracts/dao-core/src/migrate_msg.rs
@@ -39,6 +39,12 @@ pub struct V2CodeIds {
 }
 
 /// The params we need to provide for migration msgs
+#[cw_serde]
+pub struct ProposalParams {
+    pub close_proposal_on_execution_failure: bool,
+    pub pre_propose_info: PreProposeInfo,
+}
+
 #[cw_serde]
 pub struct MigrationModuleParams {
     // General
@@ -48,8 +54,7 @@ pub struct MigrationModuleParams {
     /// migration will be aborted.
     pub migrate_stake_cw20_manager: Option<bool>,
     // dao_proposal_single
-    pub close_proposal_on_execution_failure: bool,
-    pub pre_propose_info: PreProposeInfo,
+    pub proposal_params: Vec<ProposalParams>,
 }
 
 #[cw_serde]
diff --git a/contracts/external/dao-migrator/src/contract.rs b/contracts/external/dao-migrator/src/contract.rs
index a51ad81d..b57aa441 100644
--- a/contracts/external/dao-migrator/src/contract.rs
+++ b/contracts/external/dao-migrator/src/contract.rs
@@ -88,15 +88,21 @@ fn execute_migration_v1_v2(
     }
 
     // List of code ids pairs we got and the migration msg of each one of them.
-    let proposal_pair = CodeIdPair::new(
-        v1_code_ids.proposal_single,
-        v2_code_ids.proposal_single,
-        MigrationMsgs::DaoProposalSingle(dao_proposal_single::msg::MigrateMsg::FromV1 {
-            close_proposal_on_execution_failure: migration_params
-                .close_proposal_on_execution_failure,
-            pre_propose_info: migration_params.pre_propose_info,
-        }),
-    ); // cw-proposal-single -> dao_proposal_single
+    let proposal_pairs: Vec<CodeIdPair> = migration_params
+        .proposal_params
+        .iter()
+        .map(|proposal_params| {
+            CodeIdPair::new(
+                v1_code_ids.proposal_single,
+                v2_code_ids.proposal_single,
+                MigrationMsgs::DaoProposalSingle(dao_proposal_single::msg::MigrateMsg::FromV1 {
+                    close_proposal_on_execution_failure: proposal_params
+                        .close_proposal_on_execution_failure,
+                    pre_propose_info: proposal_params.pre_propose_info.clone(),
+                }),
+            )
+        })
+        .collect(); // cw-proposal-single -> dao_proposal_single
     let voting_pairs: Vec<CodeIdPair> = vec![
         CodeIdPair::new(
             v1_code_ids.cw4_voting,
@@ -209,11 +215,15 @@ fn execute_migration_v1_v2(
         },
     )?;
 
+    // We add 1 because migration module is added last, and we skip it.
+    if proposal_modules.len() != (proposal_pairs.len() + 1) {
+        return Err(ContractError::MigrationParamsNotEqualProposalModulesLength);
+    }
+
     // Loop over proposals and verify that they are valid DAO DAO modules
     // and set them to be migrated.
-    proposal_modules
-        .into_iter()
-        .try_for_each(|module| -> Result<(), ContractError> {
+    proposal_modules.into_iter().enumerate().try_for_each(
+        |(proposal_index, module)| -> Result<(), ContractError> {
             // Instead of doing 2 loops, just ignore our module, we don't care about the vec after this.
             if module.address == env.contract.address {
                 return Ok(());
@@ -233,12 +243,12 @@ fn execute_migration_v1_v2(
             }?;
 
             // check if Code id is valid DAO DAO code id
-            if proposal_code_id == proposal_pair.v1_code_id {
+            if proposal_code_id == proposal_pairs[proposal_index].v1_code_id {
                 msgs.push(
                     WasmMsg::Migrate {
                         contract_addr: module.address.to_string(),
-                        new_code_id: proposal_pair.v2_code_id,
-                        msg: to_binary(&proposal_pair.migrate_msg).unwrap(),
+                        new_code_id: proposal_pairs[proposal_index].v2_code_id,
+                        msg: to_binary(&proposal_pairs[proposal_index].migrate_msg).unwrap(),
                     }
                     .into(),
                 );
@@ -252,7 +262,8 @@ fn execute_migration_v1_v2(
             }?;
 
             Ok(())
-        })?;
+        },
+    )?;
 
     // We successfully verified all modules of the DAO, we can send migration msgs.
 
diff --git a/contracts/external/dao-migrator/src/error.rs b/contracts/external/dao-migrator/src/error.rs
index b91aa5a5..3f113101 100644
--- a/contracts/external/dao-migrator/src/error.rs
+++ b/contracts/external/dao-migrator/src/error.rs
@@ -36,4 +36,7 @@ pub enum ContractError {
 
     #[error("Failed to verify proposal in {module_addr}")]
     NoProposalsOnModule { module_addr: String },
+
+    #[error("Proposal migration params length is not equal to proposal modules length")]
+    MigrationParamsNotEqualProposalModulesLength,
 }
diff --git a/contracts/external/dao-migrator/src/types.rs b/contracts/external/dao-migrator/src/types.rs
index 54994a0d..960431de 100644
--- a/contracts/external/dao-migrator/src/types.rs
+++ b/contracts/external/dao-migrator/src/types.rs
@@ -42,6 +42,12 @@ impl V2CodeIds {
 }
 
 /// The params we need to provide for migration msgs
+#[cw_serde]
+pub struct ProposalParams {
+    pub close_proposal_on_execution_failure: bool,
+    pub pre_propose_info: dao_voting::pre_propose::PreProposeInfo,
+}
+
 #[cw_serde]
 pub struct MigrationParams {
     // General
@@ -51,8 +57,7 @@ pub struct MigrationParams {
     /// migration will be aborted.
     pub migrate_stake_cw20_manager: Option<bool>,
     // dao_proposal_single
-    pub close_proposal_on_execution_failure: bool,
-    pub pre_propose_info: dao_voting::pre_propose::PreProposeInfo,
+    pub proposal_params: Vec<ProposalParams>,
 }
 
 /// Wrapper enum that helps us to hold different types of migration msgs
diff --git a/typescript/contracts/dao-core/DaoCore.client.ts b/typescript/contracts/dao-core/DaoCore.client.ts
index cbd50bd5..e0c87b1c 100644
--- a/typescript/contracts/dao-core/DaoCore.client.ts
+++ b/typescript/contracts/dao-core/DaoCore.client.ts
@@ -6,7 +6,7 @@
 
 import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
 import { StdFee } from "@cosmjs/amino";
-import { Admin, Binary, InstantiateMsg, InitialItem, ModuleInstantiateInfo, ExecuteMsg, CosmosMsgForEmpty, BankMsg, Uint128, StakingMsg, DistributionMsg, IbcMsg, Timestamp, Uint64, WasmMsg, GovMsg, VoteOption, Duration, Coin, Empty, IbcTimeout, IbcTimeoutBlock, Cw20ReceiveMsg, Cw721ReceiveMsg, Config, SubDao, QueryMsg, MigrateMsg, PreProposeInfo, MigrateParams, MigrateV1ToV2, MigrationModuleParams, V1CodeIds, V2CodeIds, Addr, ProposalModuleStatus, ArrayOfProposalModule, ProposalModule, AdminNominationResponse, Cw20BalanceResponse, ArrayOfAddr, DaoURIResponse, PauseInfoResponse, Expiration, DumpStateResponse, ContractVersion, GetItemResponse, InfoResponse, ArrayOfString, ArrayOfSubDao, ProposalModuleCountResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse } from "./DaoCore.types";
+import { Admin, Binary, InstantiateMsg, InitialItem, ModuleInstantiateInfo, ExecuteMsg, CosmosMsgForEmpty, BankMsg, Uint128, StakingMsg, DistributionMsg, IbcMsg, Timestamp, Uint64, WasmMsg, GovMsg, VoteOption, Duration, Coin, Empty, IbcTimeout, IbcTimeoutBlock, Cw20ReceiveMsg, Cw721ReceiveMsg, Config, SubDao, QueryMsg, MigrateMsg, PreProposeInfo, MigrateParams, MigrateV1ToV2, MigrationModuleParams, ProposalParams, V1CodeIds, V2CodeIds, Addr, ProposalModuleStatus, ArrayOfProposalModule, ProposalModule, AdminNominationResponse, Cw20BalanceResponse, ArrayOfAddr, DaoURIResponse, PauseInfoResponse, Expiration, DumpStateResponse, ContractVersion, GetItemResponse, InfoResponse, ArrayOfString, ArrayOfSubDao, ProposalModuleCountResponse, TotalPowerAtHeightResponse, VotingPowerAtHeightResponse } from "./DaoCore.types";
 export interface DaoCoreReadOnlyInterface {
   contractAddress: string;
   admin: () => Promise<Addr>;
diff --git a/typescript/contracts/dao-core/DaoCore.types.ts b/typescript/contracts/dao-core/DaoCore.types.ts
index a0e7b4e9..309a81ca 100644
--- a/typescript/contracts/dao-core/DaoCore.types.ts
+++ b/typescript/contracts/dao-core/DaoCore.types.ts
@@ -365,8 +365,11 @@ export interface MigrateV1ToV2 {
   v2_code_ids: V2CodeIds;
 }
 export interface MigrationModuleParams {
-  close_proposal_on_execution_failure: boolean;
   migrate_stake_cw20_manager?: boolean | null;
+  proposal_params: ProposalParams[];
+}
+export interface ProposalParams {
+  close_proposal_on_execution_failure: boolean;
   pre_propose_info: PreProposeInfo;
 }
 export interface V1CodeIds {


Test patch:
diff --git a/contracts/external/dao-migrator/src/testing/helpers.rs b/contracts/external/dao-migrator/src/testing/helpers.rs
index d44b0351..20d81900 100644
--- a/contracts/external/dao-migrator/src/testing/helpers.rs
+++ b/contracts/external/dao-migrator/src/testing/helpers.rs
@@ -29,7 +29,7 @@ pub struct ExecuteParams {
 #[derive(Clone)]
 pub struct ModuleAddrs {
     pub core: Addr,
-    pub proposal: Addr,
+    pub proposals: Vec<Addr>,
     pub voting: Addr,
     pub staking: Option<Addr>,
     pub token: Option<Addr>,
@@ -114,9 +114,8 @@ pub fn get_cw4_init_msg(code_ids: CodeIds) -> cw4_voting_v1::msg::InstantiateMsg
 
 pub fn get_module_addrs(app: &mut App, core_addr: Addr) -> ModuleAddrs {
     // Get modules addrs
-    let proposal_addr = {
-        let modules: Vec<Addr> = app
-            .wrap()
+    let proposal_addrs: Vec<Addr> = {
+        app.wrap()
             .query_wasm_smart(
                 &core_addr,
                 &cw_core_v1::msg::QueryMsg::ProposalModules {
@@ -124,9 +123,7 @@ pub fn get_module_addrs(app: &mut App, core_addr: Addr) -> ModuleAddrs {
                     limit: None,
                 },
             )
-            .unwrap();
-        assert!(modules.len() == 1);
-        modules.into_iter().next().unwrap()
+            .unwrap()
     };
 
     let voting_addr: Addr = app
@@ -152,22 +149,22 @@ pub fn get_module_addrs(app: &mut App, core_addr: Addr) -> ModuleAddrs {
 
     ModuleAddrs {
         core: core_addr,
-        proposal: proposal_addr,
+        proposals: proposal_addrs,
         staking: staking_addr,
         voting: voting_addr,
         token: token_addr,
     }
 }
 
-pub fn set_dummy_proposal(app: &mut App, sender: Addr, addrs: ModuleAddrs) {
+pub fn set_dummy_proposal(app: &mut App, sender: Addr, core_addr: Addr, proposal_addr: Addr) {
     app.execute_contract(
         sender,
-        addrs.proposal.clone(),
+        proposal_addr,
         &cw_proposal_single_v1::msg::ExecuteMsg::Propose {
             title: "t".to_string(),
             description: "d".to_string(),
             msgs: vec![WasmMsg::Execute {
-                contract_addr: addrs.core.to_string(),
+                contract_addr: core_addr.to_string(),
                 msg: to_binary(&cw_core_v1::msg::ExecuteMsg::UpdateCw20List {
                     to_add: vec![],
                     to_remove: vec![],
@@ -206,7 +203,7 @@ pub fn set_cw20_to_dao(app: &mut App, sender: Addr, addrs: ModuleAddrs) {
 
     app.execute_contract(
         sender.clone(),
-        addrs.proposal.clone(),
+        addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Propose {
             title: "t".to_string(),
             description: "d".to_string(),
@@ -227,7 +224,7 @@ pub fn set_cw20_to_dao(app: &mut App, sender: Addr, addrs: ModuleAddrs) {
 
     app.execute_contract(
         sender.clone(),
-        addrs.proposal.clone(),
+        addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Vote {
             proposal_id: 1,
             vote: voting_v1::Vote::Yes,
@@ -238,7 +235,7 @@ pub fn set_cw20_to_dao(app: &mut App, sender: Addr, addrs: ModuleAddrs) {
 
     app.execute_contract(
         sender,
-        addrs.proposal.clone(),
+        addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Execute { proposal_id: 1 },
         &[],
     )
diff --git a/contracts/external/dao-migrator/src/testing/setup.rs b/contracts/external/dao-migrator/src/testing/setup.rs
index 5120bb19..6f372b32 100644
--- a/contracts/external/dao-migrator/src/testing/setup.rs
+++ b/contracts/external/dao-migrator/src/testing/setup.rs
@@ -6,7 +6,7 @@ use dao_interface::{Admin, ModuleInstantiateInfo};
 
 use crate::{
     testing::helpers::get_module_addrs,
-    types::{MigrationParams, V1CodeIds},
+    types::{MigrationParams, ProposalParams, V1CodeIds},
 };
 
 use super::helpers::{
@@ -87,6 +87,101 @@ pub fn init_v1(app: &mut App, sender: Addr, voting_type: VotingType) -> (Addr, V
     (core_addr, v1_code_ids)
 }
 
+pub fn init_v1_with_multiple_proposals(
+    app: &mut App,
+    sender: Addr,
+    voting_type: VotingType,
+) -> (Addr, V1CodeIds) {
+    let (code_ids, v1_code_ids) = get_v1_code_ids(app);
+
+    let (voting_code_id, msg) = match voting_type {
+        VotingType::Cw4 => (
+            code_ids.cw4_voting,
+            to_binary(&get_cw4_init_msg(code_ids.clone())).unwrap(),
+        ),
+        VotingType::Cw20 => (
+            code_ids.cw20_voting,
+            to_binary(&get_cw20_init_msg(code_ids.clone())).unwrap(),
+        ),
+    };
+
+    let core_addr = app
+        .instantiate_contract(
+            code_ids.core,
+            sender.clone(),
+            &cw_core_v1::msg::InstantiateMsg {
+                admin: Some(SENDER_ADDR.to_string()),
+                name: "n".to_string(),
+                description: "d".to_string(),
+                image_url: Some("i".to_string()),
+                automatically_add_cw20s: false,
+                automatically_add_cw721s: true,
+                voting_module_instantiate_info: cw_core_v1::msg::ModuleInstantiateInfo {
+                    code_id: voting_code_id,
+                    msg,
+                    admin: cw_core_v1::msg::Admin::CoreContract {},
+                    label: "voting".to_string(),
+                },
+                proposal_modules_instantiate_info: vec![
+                    cw_core_v1::msg::ModuleInstantiateInfo {
+                        code_id: code_ids.proposal_single,
+                        msg: to_binary(&cw_proposal_single_v1::msg::InstantiateMsg {
+                            threshold: voting_v1::Threshold::AbsolutePercentage {
+                                percentage: voting_v1::PercentageThreshold::Majority {},
+                            },
+                            max_voting_period: cw_utils_v1::Duration::Height(6),
+                            min_voting_period: None,
+                            only_members_execute: false,
+                            allow_revoting: false,
+                            deposit_info: None,
+                        })
+                        .unwrap(),
+                        admin: cw_core_v1::msg::Admin::CoreContract {},
+                        label: "proposal".to_string(),
+                    },
+                    cw_core_v1::msg::ModuleInstantiateInfo {
+                        code_id: code_ids.proposal_single,
+                        msg: to_binary(&cw_proposal_single_v1::msg::InstantiateMsg {
+                            threshold: voting_v1::Threshold::AbsolutePercentage {
+                                percentage: voting_v1::PercentageThreshold::Majority {},
+                            },
+                            max_voting_period: cw_utils_v1::Duration::Height(6),
+                            min_voting_period: None,
+                            only_members_execute: false,
+                            allow_revoting: false,
+                            deposit_info: None,
+                        })
+                        .unwrap(),
+                        admin: cw_core_v1::msg::Admin::CoreContract {},
+                        label: "proposal".to_string(),
+                    },
+                ],
+                initial_items: Some(vec![cw_core_v1::msg::InitialItem {
+                    key: "key".to_string(),
+                    value: "value".to_string(),
+                }]),
+            },
+            &[],
+            "core",
+            Some(sender.to_string()),
+        )
+        .unwrap();
+
+    app.update_block(next_block);
+
+    app.execute(
+        sender,
+        WasmMsg::UpdateAdmin {
+            contract_addr: core_addr.to_string(),
+            admin: core_addr.to_string(),
+        }
+        .into(),
+    )
+    .unwrap();
+
+    (core_addr, v1_code_ids)
+}
+
 /// Instantiate a basic DAO with proposal and voting modules.
 pub fn setup_dao_v1(voting_type: VotingType) -> (App, ModuleAddrs, V1CodeIds) {
     let mut app = App::default();
@@ -96,18 +191,44 @@ pub fn setup_dao_v1(voting_type: VotingType) -> (App, ModuleAddrs, V1CodeIds) {
     let module_addrs = get_module_addrs(app.borrow_mut(), core_addr);
 
     match voting_type {
-        VotingType::Cw4 => set_dummy_proposal(app.borrow_mut(), sender, module_addrs.clone()),
+        VotingType::Cw4 => set_dummy_proposal(
+            app.borrow_mut(),
+            sender,
+            module_addrs.core.clone(),
+            module_addrs.proposals[0].clone(),
+        ),
         VotingType::Cw20 => set_cw20_to_dao(app.borrow_mut(), sender, module_addrs.clone()),
     };
 
     (app, module_addrs, v1_code_ids)
 }
 
+/// Instantiate a basic DAO with 2 proposal modules.
+pub fn setup_dao_v1_multiple_proposals() -> (App, ModuleAddrs, V1CodeIds) {
+    let mut app = App::default();
+    let sender = Addr::unchecked(SENDER_ADDR);
+
+    let (core_addr, v1_code_ids) =
+        init_v1_with_multiple_proposals(app.borrow_mut(), sender.clone(), VotingType::Cw20);
+    let module_addrs = get_module_addrs(app.borrow_mut(), core_addr);
+
+    set_cw20_to_dao(app.borrow_mut(), sender.clone(), module_addrs.clone());
+    set_dummy_proposal(
+        app.borrow_mut(),
+        sender,
+        module_addrs.core.clone(),
+        module_addrs.proposals[1].clone(),
+    );
+
+    (app, module_addrs, v1_code_ids)
+}
+
 pub fn execute_migration(
     app: &mut App,
     module_addrs: &ModuleAddrs,
     v1_code_ids: V1CodeIds,
     params: Option<ExecuteParams>,
+    multiple_proposals: bool,
 ) -> Result<AppResponse, anyhow::Error> {
     let sender = Addr::unchecked(SENDER_ADDR);
     let migrator_code_id = app.store_code(migrator_contract());
@@ -116,10 +237,21 @@ pub fn execute_migration(
         sub_daos: Some(vec![]),
         migrate_cw20: Some(true),
     });
+    let mut proposal_params = vec![ProposalParams {
+        close_proposal_on_execution_failure: true,
+        pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {},
+    }];
+
+    if multiple_proposals {
+        proposal_params.push(ProposalParams {
+            close_proposal_on_execution_failure: true,
+            pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {},
+        })
+    }
 
     app.execute_contract(
         sender.clone(),
-        module_addrs.proposal.clone(),
+        module_addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Propose {
             title: "t2".to_string(),
             description: "d2".to_string(),
@@ -127,7 +259,11 @@ pub fn execute_migration(
                 WasmMsg::Migrate {
                     contract_addr: module_addrs.core.to_string(),
                     new_code_id: new_code_ids.core,
-                    msg: to_binary(&dao_core::msg::MigrateMsg::FromV1 { dao_uri: None, params: None }).unwrap(),
+                    msg: to_binary(&dao_core::msg::MigrateMsg::FromV1 {
+                        dao_uri: None,
+                        params: None,
+                    })
+                    .unwrap(),
                 }
                 .into(),
                 WasmMsg::Execute {
@@ -139,8 +275,7 @@ pub fn execute_migration(
                                 sub_daos: params.sub_daos.unwrap(),
                                 migration_params: MigrationParams {
                                     migrate_stake_cw20_manager: params.migrate_cw20,
-                                    close_proposal_on_execution_failure: true,
-                                    pre_propose_info: dao_voting::pre_propose::PreProposeInfo::AnyoneMayPropose {},
+                                    proposal_params,
                                 },
                                 v1_code_ids,
                                 v2_code_ids,
@@ -158,12 +293,13 @@ pub fn execute_migration(
             ],
         },
         &[],
-    ).unwrap();
+    )
+    .unwrap();
 
     let perposals: cw_proposal_single_v1::query::ProposalListResponse = app
         .wrap()
         .query_wasm_smart(
-            module_addrs.proposal.clone(),
+            module_addrs.proposals[0].clone(),
             &cw_proposal_single_v1::msg::QueryMsg::ReverseProposals {
                 start_before: None,
                 limit: Some(1),
@@ -174,7 +310,7 @@ pub fn execute_migration(
 
     app.execute_contract(
         sender.clone(),
-        module_addrs.proposal.clone(),
+        module_addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Vote {
             proposal_id,
             vote: voting_v1::Vote::Yes,
@@ -185,7 +321,7 @@ pub fn execute_migration(
 
     app.execute_contract(
         sender,
-        module_addrs.proposal.clone(),
+        module_addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Execute { proposal_id },
         &[],
     )
@@ -196,6 +332,7 @@ pub fn execute_migration_from_core(
     module_addrs: &ModuleAddrs,
     v1_code_ids: V1CodeIds,
     params: Option<ExecuteParams>,
+    multiple_proposals: bool,
 ) -> Result<AppResponse, anyhow::Error> {
     let sender = Addr::unchecked(SENDER_ADDR);
     let migrator_code_id = app.store_code(migrator_contract());
@@ -204,10 +341,21 @@ pub fn execute_migration_from_core(
         sub_daos: Some(vec![]),
         migrate_cw20: Some(true),
     });
+    let mut proposal_params = vec![dao_core::migrate_msg::ProposalParams {
+        close_proposal_on_execution_failure: true,
+        pre_propose_info: dao_core::migrate_msg::PreProposeInfo::AnyoneMayPropose {},
+    }];
+
+    if multiple_proposals {
+        proposal_params.push(dao_core::migrate_msg::ProposalParams {
+            close_proposal_on_execution_failure: true,
+            pre_propose_info: dao_core::migrate_msg::PreProposeInfo::AnyoneMayPropose {},
+        })
+    }
 
     app.execute_contract(
         sender.clone(),
-        module_addrs.proposal.clone(),
+        module_addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Propose {
             title: "t2".to_string(),
             description: "d2".to_string(),
@@ -222,9 +370,7 @@ pub fn execute_migration_from_core(
                             sub_daos: params.sub_daos.unwrap(),
                             migration_params: dao_core::migrate_msg::MigrationModuleParams {
                                 migrate_stake_cw20_manager: params.migrate_cw20,
-                                close_proposal_on_execution_failure: true,
-                                pre_propose_info:
-                                    dao_core::migrate_msg::PreProposeInfo::AnyoneMayPropose {},
+                                proposal_params,
                             },
                             v1_code_ids: v1_code_ids.to(),
                             v2_code_ids: v2_code_ids.to(),
@@ -242,7 +388,7 @@ pub fn execute_migration_from_core(
     let perposals: cw_proposal_single_v1::query::ProposalListResponse = app
         .wrap()
         .query_wasm_smart(
-            module_addrs.proposal.clone(),
+            module_addrs.proposals[0].clone(),
             &cw_proposal_single_v1::msg::QueryMsg::ReverseProposals {
                 start_before: None,
                 limit: Some(1),
@@ -253,7 +399,7 @@ pub fn execute_migration_from_core(
 
     app.execute_contract(
         sender.clone(),
-        module_addrs.proposal.clone(),
+        module_addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Vote {
             proposal_id,
             vote: voting_v1::Vote::Yes,
@@ -264,7 +410,7 @@ pub fn execute_migration_from_core(
 
     app.execute_contract(
         sender,
-        module_addrs.proposal.clone(),
+        module_addrs.proposals[0].clone(),
         &cw_proposal_single_v1::msg::ExecuteMsg::Execute { proposal_id },
         &[],
     )
diff --git a/contracts/external/dao-migrator/src/testing/test_migration.rs b/contracts/external/dao-migrator/src/testing/test_migration.rs
index f1a69994..20346f6e 100644
--- a/contracts/external/dao-migrator/src/testing/test_migration.rs
+++ b/contracts/external/dao-migrator/src/testing/test_migration.rs
@@ -14,18 +14,20 @@ use crate::{
     ContractError,
 };
 
+use super::setup::setup_dao_v1_multiple_proposals;
+
 pub fn basic_test(voting_type: VotingType, from_core: bool) {
     let (mut app, module_addrs, v1_code_ids) = setup_dao_v1(voting_type.clone());
 
     let mut test_state_v1 = match voting_type {
         VotingType::Cw4 => query_state_v1_cw4(
             &mut app,
-            module_addrs.proposal.clone(),
+            module_addrs.proposals[0].clone(),
             module_addrs.voting.clone(),
         ),
         VotingType::Cw20 => query_state_v1_cw20(
             &mut app,
-            module_addrs.proposal.clone(),
+            module_addrs.proposals[0].clone(),
             module_addrs.voting.clone(),
         ),
     };
@@ -34,18 +36,25 @@ pub fn basic_test(voting_type: VotingType, from_core: bool) {
 
     match from_core {
         true => {
-            execute_migration_from_core(app.borrow_mut(), &module_addrs, v1_code_ids, None).unwrap()
+            execute_migration_from_core(app.borrow_mut(), &module_addrs, v1_code_ids, None, false)
+                .unwrap()
+        }
+        false => {
+            execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None, false).unwrap()
         }
-        false => execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None).unwrap(),
     };
 
     let test_state_v2 = match voting_type {
-        VotingType::Cw4 => {
-            query_state_v2_cw4(&mut app, module_addrs.proposal.clone(), module_addrs.voting)
-        }
-        VotingType::Cw20 => {
-            query_state_v2_cw20(&mut app, module_addrs.proposal.clone(), module_addrs.voting)
-        }
+        VotingType::Cw4 => query_state_v2_cw4(
+            &mut app,
+            module_addrs.proposals[0].clone(),
+            module_addrs.voting,
+        ),
+        VotingType::Cw20 => query_state_v2_cw20(
+            &mut app,
+            module_addrs.proposals[0].clone(),
+            module_addrs.voting,
+        ),
     };
 
     assert_eq!(test_state_v1, test_state_v2);
@@ -61,7 +70,7 @@ pub fn basic_test(voting_type: VotingType, from_core: bool) {
         )
         .unwrap();
     assert_eq!(modules.len(), 2);
-    assert_eq!(modules[0].address, module_addrs.proposal);
+    assert_eq!(modules[0].address, module_addrs.proposals[0]);
     assert_eq!(modules[1].status, ProposalModuleStatus::Disabled);
 }
 
@@ -76,12 +85,62 @@ fn test_execute_migration() {
     basic_test(VotingType::Cw4, true);
 }
 
+#[test]
+fn test_multiple_proposal_modules() {
+    let (mut app, module_addrs, v1_code_ids) = setup_dao_v1_multiple_proposals();
+
+    let mut test_state_v1 = query_state_v1_cw20(
+        &mut app,
+        module_addrs.proposals[0].clone(),
+        module_addrs.voting.clone(),
+    );
+    //NOTE: We add 1 to count because we create a new proposal in execute_migration
+    test_state_v1.proposal_count += 1;
+
+    execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None, true).unwrap();
+
+    let test_state_v2 = query_state_v2_cw20(
+        &mut app,
+        module_addrs.proposals[0].clone(),
+        module_addrs.voting,
+    );
+
+    assert_eq!(test_state_v1, test_state_v2);
+}
+
+#[test]
+fn test_multiple_proposal_modules_failing() {
+    // Test single proposal with multiple proposal params.
+    let (mut app, module_addrs, v1_code_ids) = setup_dao_v1(VotingType::Cw20);
+
+    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None, true)
+        .unwrap_err()
+        .downcast::<ContractError>()
+        .unwrap();
+    assert_eq!(
+        err,
+        ContractError::MigrationParamsNotEqualProposalModulesLength
+    );
+
+    // Test multiple proposals with single proposal params.
+    let (mut app, module_addrs, v1_code_ids) = setup_dao_v1_multiple_proposals();
+
+    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None, false)
+        .unwrap_err()
+        .downcast::<ContractError>()
+        .unwrap();
+    assert_eq!(
+        err,
+        ContractError::MigrationParamsNotEqualProposalModulesLength
+    );
+}
+
 #[test]
 fn test_wrong_code_id() {
     let (mut app, module_addrs, mut v1_code_ids) = setup_dao_v1(VotingType::Cw20);
     let old_v1_code_ids = v1_code_ids.clone();
     v1_code_ids.proposal_single = 555;
-    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None)
+    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None, false)
         .unwrap_err()
         .downcast::<ContractError>()
         .unwrap();
@@ -95,7 +154,7 @@ fn test_wrong_code_id() {
     let (mut app, module_addrs, mut v1_code_ids) = setup_dao_v1(VotingType::Cw20);
     let old_v1_code_ids = v1_code_ids.clone();
     v1_code_ids.cw20_stake = 555;
-    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None)
+    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None, false)
         .unwrap_err()
         .downcast::<ContractError>()
         .unwrap();
@@ -108,7 +167,7 @@ fn test_wrong_code_id() {
 
     let (mut app, module_addrs, mut v1_code_ids) = setup_dao_v1(VotingType::Cw20);
     v1_code_ids.cw20_staked_balances_voting = 555;
-    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None)
+    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None, false)
         .unwrap_err()
         .downcast::<ContractError>()
         .unwrap();
@@ -116,7 +175,7 @@ fn test_wrong_code_id() {
 
     let (mut app, module_addrs, mut v1_code_ids) = setup_dao_v1(VotingType::Cw4);
     v1_code_ids.cw4_voting = 555;
-    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None)
+    let err = execute_migration(app.borrow_mut(), &module_addrs, v1_code_ids, None, false)
         .unwrap_err()
         .downcast::<ContractError>()
         .unwrap();
@@ -135,6 +194,7 @@ fn test_dont_migrate_cw20() {
             sub_daos: Some(vec![]),
             migrate_cw20: None,
         }),
+        false,
     )
     .unwrap_err()
     .downcast::<ContractError>()
@@ -149,6 +209,7 @@ fn test_dont_migrate_cw20() {
             sub_daos: Some(vec![]),
             migrate_cw20: Some(false),
         }),
+        false,
     )
     .unwrap_err()
     .downcast::<ContractError>()
@@ -172,6 +233,7 @@ fn test_sub_daos() {
             sub_daos: Some(vec![sub_dao.clone()]),
             migrate_cw20: Some(true),
         }),
+        false,
     )
     .unwrap();