dao_migrator/
contract.rs

1use std::{collections::HashSet, env};
2
3#[cfg(not(feature = "library"))]
4use cosmwasm_std::entry_point;
5use cosmwasm_std::{
6    to_json_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Reply, Response,
7    StdResult, SubMsg, WasmMsg,
8};
9use cw2::set_contract_version;
10use dao_interface::{
11    query::SubDao,
12    state::{ModuleInstantiateCallback, ProposalModule},
13};
14
15use crate::{
16    error::ContractError,
17    msg::{ExecuteMsg, InstantiateMsg, MigrateV1ToV2, QueryMsg},
18    state::{CORE_ADDR, MODULES_ADDRS, TEST_STATE},
19    types::{
20        CodeIdPair, MigrationMsgs, MigrationParams, ModulesAddrs, TestState, V1CodeIds, V2CodeIds,
21    },
22    utils::state_queries::{
23        query_proposal_count_v1, query_proposal_count_v2, query_proposal_v1, query_proposal_v2,
24        query_single_voting_power_v1, query_single_voting_power_v2, query_total_voting_power_v1,
25        query_total_voting_power_v2,
26    },
27};
28
29pub(crate) const CONTRACT_NAME: &str = "crates.io:dao-migrator";
30pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
31
32pub(crate) const V1_V2_REPLY_ID: u64 = 1;
33
34#[cfg_attr(not(feature = "library"), entry_point)]
35pub fn instantiate(
36    deps: DepsMut,
37    env: Env,
38    info: MessageInfo,
39    msg: InstantiateMsg,
40) -> Result<Response, ContractError> {
41    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
42    CORE_ADDR.save(deps.storage, &info.sender)?;
43
44    Ok(
45        Response::default().set_data(to_json_binary(&ModuleInstantiateCallback {
46            msgs: vec![WasmMsg::Execute {
47                contract_addr: env.contract.address.to_string(),
48                msg: to_json_binary(&MigrateV1ToV2 {
49                    sub_daos: msg.sub_daos,
50                    migration_params: msg.migration_params,
51                    v1_code_ids: msg.v1_code_ids,
52                    v2_code_ids: msg.v2_code_ids,
53                })?,
54                funds: vec![],
55            }
56            .into()],
57        })?),
58    )
59}
60
61#[cfg_attr(not(feature = "library"), entry_point)]
62pub fn execute(
63    deps: DepsMut,
64    env: Env,
65    info: MessageInfo,
66    msg: ExecuteMsg,
67) -> Result<Response, ContractError> {
68    execute_migration_v1_v2(
69        deps,
70        env,
71        info,
72        msg.sub_daos,
73        msg.migration_params,
74        msg.v1_code_ids,
75        msg.v2_code_ids,
76    )
77}
78
79fn execute_migration_v1_v2(
80    deps: DepsMut,
81    env: Env,
82    info: MessageInfo,
83    sub_daos: Vec<SubDao>,
84    migration_params: MigrationParams,
85    v1_code_ids: V1CodeIds,
86    v2_code_ids: V2CodeIds,
87) -> Result<Response, ContractError> {
88    if info.sender != CORE_ADDR.load(deps.storage)? {
89        return Err(ContractError::Unauthorized {});
90    }
91
92    //Check if params doesn't have duplicates
93    let mut uniq = HashSet::new();
94    if !migration_params
95        .proposal_params
96        .iter()
97        .all(|(addr, _)| uniq.insert(addr))
98    {
99        return Err(ContractError::DuplicateProposalParams);
100    }
101
102    // List of code ids pairs we got and the migration msg of each one of them.
103    let proposal_pairs: Vec<(String, CodeIdPair)> = migration_params
104        .proposal_params
105        .clone()
106        .into_iter()
107        .map(|(addr, proposal_params)| {
108            (
109                addr,
110                CodeIdPair::new(
111                    v1_code_ids.proposal_single,
112                    v2_code_ids.proposal_single,
113                    MigrationMsgs::DaoProposalSingle(
114                        dao_proposal_single::msg::MigrateMsg::FromV1 {
115                            close_proposal_on_execution_failure: proposal_params
116                                .close_proposal_on_execution_failure,
117                            pre_propose_info: proposal_params.pre_propose_info,
118                            veto: proposal_params.veto,
119                        },
120                    ),
121                ),
122            )
123        })
124        .collect(); // cw-proposal-single -> dao_proposal_single
125    let voting_pairs: Vec<CodeIdPair> = vec![
126        CodeIdPair::new(
127            v1_code_ids.cw4_voting,
128            v2_code_ids.cw4_voting,
129            MigrationMsgs::DaoVotingCw4(dao_voting_cw4::msg::MigrateMsg {}),
130        ), // cw4-voting -> dao_voting_cw4
131        CodeIdPair::new(
132            v1_code_ids.cw20_staked_balances_voting,
133            v2_code_ids.cw20_staked_balances_voting,
134            MigrationMsgs::DaoVotingCw20Staked(dao_voting_cw20_staked::msg::MigrateMsg {}),
135        ), // cw20-staked-balances-voting -> dao-voting-cw20-staked
136    ];
137    let staking_pair = CodeIdPair::new(
138        v1_code_ids.cw20_stake,
139        v2_code_ids.cw20_stake,
140        MigrationMsgs::Cw20Stake(cw20_stake::msg::MigrateMsg::FromV1 {}),
141    ); // cw20-stake -> cw20_stake
142
143    let mut msgs: Vec<CosmosMsg> = vec![];
144    let mut modules_addrs = ModulesAddrs::default();
145
146    // --------------------
147    // verify voting module
148    // --------------------
149    let voting_module: Addr = deps.querier.query_wasm_smart(
150        info.sender.clone(),
151        &dao_interface::msg::QueryMsg::VotingModule {},
152    )?;
153
154    let voting_code_id =
155        if let Ok(contract_info) = deps.querier.query_wasm_contract_info(voting_module.clone()) {
156            contract_info.code_id
157        } else {
158            // Return false if we don't get contract info, means something went wrong.
159            return Err(ContractError::NoContractInfo {
160                address: voting_module.into(),
161            });
162        };
163
164    if let Some(voting_pair) = voting_pairs
165        .into_iter()
166        .find(|x| x.v1_code_id == voting_code_id)
167    {
168        msgs.push(
169            WasmMsg::Migrate {
170                contract_addr: voting_module.to_string(),
171                new_code_id: voting_pair.v2_code_id,
172                msg: to_json_binary(&voting_pair.migrate_msg).unwrap(),
173            }
174            .into(),
175        );
176        modules_addrs.voting = Some(voting_module.clone());
177
178        // If voting module is staked cw20, we check that they confirmed migration
179        // and migrate the cw20_staked module
180        if let MigrationMsgs::DaoVotingCw20Staked(_) = voting_pair.migrate_msg {
181            if !migration_params
182                .migrate_stake_cw20_manager
183                .unwrap_or_default()
184            {
185                return Err(ContractError::DontMigrateCw20);
186            }
187
188            let cw20_staked_addr: Addr = deps.querier.query_wasm_smart(
189                voting_module,
190                &cw20_staked_balance_voting_v1::msg::QueryMsg::StakingContract {},
191            )?;
192
193            let c20_staked_code_id = if let Ok(contract_info) = deps
194                .querier
195                .query_wasm_contract_info(cw20_staked_addr.clone())
196            {
197                contract_info.code_id
198            } else {
199                // Return false if we don't get contract info, means something went wrong.
200                return Err(ContractError::NoContractInfo {
201                    address: cw20_staked_addr.into(),
202                });
203            };
204
205            // If module is not DAO DAO module
206            if c20_staked_code_id != staking_pair.v1_code_id {
207                return Err(ContractError::CantMigrateModule {
208                    code_id: c20_staked_code_id,
209                });
210            }
211
212            msgs.push(
213                WasmMsg::Migrate {
214                    contract_addr: cw20_staked_addr.to_string(),
215                    new_code_id: staking_pair.v2_code_id,
216                    msg: to_json_binary(&staking_pair.migrate_msg).unwrap(),
217                }
218                .into(),
219            );
220        }
221    } else {
222        return Err(ContractError::VotingModuleNotFound);
223    }
224
225    // -----------------------
226    // verify proposal modules
227    // -----------------------
228    // We take all the proposal modules of the DAO.
229    let proposal_modules: Vec<ProposalModule> = deps.querier.query_wasm_smart(
230        info.sender.clone(),
231        &dao_interface::msg::QueryMsg::ProposalModules {
232            start_after: None,
233            limit: None,
234        },
235    )?;
236
237    // We remove 1 because migration module is a proposal module, and we skip it.
238    if proposal_modules.len() - 1 != (proposal_pairs.len()) {
239        return Err(ContractError::MigrationParamsNotEqualProposalModulesLength);
240    }
241
242    // Loop over proposals and verify that they are valid DAO DAO modules
243    // and set them to be migrated.
244    proposal_modules
245        .iter()
246        .try_for_each(|module| -> Result<(), ContractError> {
247            // Instead of doing 2 loops, just ignore our module, we don't care about the vec after this.
248            if module.address == env.contract.address {
249                return Ok(());
250            }
251
252            let proposal_pair = proposal_pairs
253                .iter()
254                .find(|(addr, _)| addr == module.address.as_str())
255                .ok_or(ContractError::ProposalModuleNotFoundInParams {
256                    addr: module.address.clone().into(),
257                })?
258                .1
259                .clone();
260
261            // Get the code id of the module
262            let proposal_code_id = if let Ok(contract_info) = deps
263                .querier
264                .query_wasm_contract_info(module.address.clone())
265            {
266                Ok(contract_info.code_id)
267            } else {
268                // Return false if we don't get contract info, means something went wrong.
269                Err(ContractError::NoContractInfo {
270                    address: module.address.clone().into(),
271                })
272            }?;
273
274            // check if Code id is valid DAO DAO code id
275            if proposal_code_id == proposal_pair.v1_code_id {
276                msgs.push(
277                    WasmMsg::Migrate {
278                        contract_addr: module.address.to_string(),
279                        new_code_id: proposal_pair.v2_code_id,
280                        msg: to_json_binary(&proposal_pair.migrate_msg).unwrap(),
281                    }
282                    .into(),
283                );
284                modules_addrs.proposals.push(module.address.clone());
285                Ok(())
286            } else {
287                // Return false because we couldn't find the code id on our list.
288                Err(ContractError::CantMigrateModule {
289                    code_id: proposal_code_id,
290                })
291            }?;
292
293            Ok(())
294        })?;
295
296    // We successfully verified all modules of the DAO, we can send migration msgs.
297
298    // Verify we got voting address, and at least 1 proposal single address
299    modules_addrs.verify()?;
300    MODULES_ADDRS.save(deps.storage, &modules_addrs)?;
301    // Do the state query, and save it in storage
302    let state = query_state_v1(deps.as_ref(), modules_addrs)?;
303    TEST_STATE.save(deps.storage, &state)?;
304
305    // Add sub daos to core
306    msgs.push(
307        WasmMsg::Execute {
308            contract_addr: info.sender.to_string(),
309            msg: to_json_binary(&dao_interface::msg::ExecuteMsg::UpdateSubDaos {
310                to_add: sub_daos,
311                to_remove: vec![],
312            })?,
313            funds: vec![],
314        }
315        .into(),
316    );
317
318    // Create the ExecuteProposalHook msg.
319    let proposal_hook_msg = SubMsg::reply_on_success(
320        WasmMsg::Execute {
321            contract_addr: info.sender.to_string(),
322            msg: to_json_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook { msgs })?,
323            funds: vec![],
324        },
325        V1_V2_REPLY_ID,
326    );
327
328    Ok(Response::default().add_submessage(proposal_hook_msg))
329}
330
331#[cfg_attr(not(feature = "library"), entry_point)]
332pub fn query(_deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
333    match msg {}
334}
335
336#[cfg_attr(not(feature = "library"), entry_point)]
337pub fn reply(deps: DepsMut, env: Env, reply: Reply) -> Result<Response, ContractError> {
338    match reply.id {
339        V1_V2_REPLY_ID => {
340            let core_addr = CORE_ADDR.load(deps.storage)?;
341            // This is called after we got all the migrations successfully
342            test_state(deps.as_ref())?;
343
344            // FINALLY remove the migrator from the core
345            // Reason we do it now, is because we first need to test the state
346            // and only then delete our module if everything worked out.
347            let remove_msg = WasmMsg::Execute {
348                contract_addr: core_addr.to_string(),
349                msg: to_json_binary(&dao_interface::msg::ExecuteMsg::ExecuteProposalHook {
350                    msgs: vec![WasmMsg::Execute {
351                        contract_addr: core_addr.to_string(),
352                        msg: to_json_binary(
353                            &dao_interface::msg::ExecuteMsg::UpdateProposalModules {
354                                to_add: vec![],
355                                to_disable: vec![env.contract.address.to_string()],
356                            },
357                        )?,
358                        funds: vec![],
359                    }
360                    .into()],
361                })?,
362                funds: vec![],
363            };
364
365            Ok(Response::default()
366                .add_message(remove_msg)
367                .add_attribute("action", "migrate")
368                .add_attribute("status", "success"))
369        }
370        _ => Err(ContractError::UnrecognisedReplyId),
371    }
372}
373
374fn query_state_v1(deps: Deps, module_addrs: ModulesAddrs) -> Result<TestState, ContractError> {
375    let proposal_counts = query_proposal_count_v1(deps, module_addrs.proposals.clone())?;
376    let (proposals, sample_proposal_data) = query_proposal_v1(deps, module_addrs.proposals)?;
377    let total_voting_power = query_total_voting_power_v1(
378        deps,
379        module_addrs.voting.clone().unwrap(),
380        sample_proposal_data.start_height,
381    )?;
382    let single_voting_power = query_single_voting_power_v1(
383        deps,
384        module_addrs.voting.unwrap(),
385        sample_proposal_data.proposer,
386        sample_proposal_data.start_height,
387    )?;
388
389    Ok(TestState {
390        proposal_counts,
391        proposals,
392        total_voting_power,
393        single_voting_power,
394    })
395}
396
397fn query_state_v2(deps: Deps, module_addrs: ModulesAddrs) -> Result<TestState, ContractError> {
398    let proposal_counts = query_proposal_count_v2(deps, module_addrs.proposals.clone())?;
399    let (proposals, sample_proposal_data) =
400        query_proposal_v2(deps, module_addrs.proposals.clone())?;
401    let total_voting_power = query_total_voting_power_v2(
402        deps,
403        module_addrs.voting.clone().unwrap(),
404        sample_proposal_data.start_height,
405    )?;
406    let single_voting_power = query_single_voting_power_v2(
407        deps,
408        module_addrs.voting.unwrap(),
409        sample_proposal_data.proposer,
410        sample_proposal_data.start_height,
411    )?;
412
413    Ok(TestState {
414        proposal_counts,
415        proposals,
416        total_voting_power,
417        single_voting_power,
418    })
419}
420
421fn test_state(deps: Deps) -> Result<(), ContractError> {
422    let old_state = TEST_STATE.load(deps.storage)?;
423    let modules_addrs = MODULES_ADDRS.load(deps.storage)?;
424    let new_state = query_state_v2(deps, modules_addrs)?;
425
426    if new_state == old_state {
427        Ok(())
428    } else {
429        Err(ContractError::TestFailed)
430    }
431}