Repository: DA0-DA0/dao-contracts
Pull request #417 (2022-07-25 21:43:11)

add proposal timestamp migration 

codecov-commenter: # [Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/417?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) Report
> Merging [#417](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/417?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) (b58a921) into [jchalam/proposalModulePrefix2](https://codecov.io/gh/DA0-DA0/dao-contracts/commit/9d813e76492d302e2cbd7b5e65a8bc2ddd6ce04a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) (9d813e7) will **increase** coverage by `0.02%`.
> The diff coverage is `100.00%`.

```diff
@@                        Coverage Diff                        @@
##           jchalam/proposalModulePrefix2     #417      +/-   ##
=================================================================
+ Coverage                          95.58%   95.60%   +0.02%     
=================================================================
  Files                                 33       33              
  Lines                               5026     5051      +25     
=================================================================
+ Hits                                4804     4829      +25     
  Misses                               222      222              
```


| [Impacted Files](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/417?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) | Coverage Δ | |
|---|---|---|
| [contracts/cw-proposal-single/src/contract.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/417/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2N3LXByb3Bvc2FsLXNpbmdsZS9zcmMvY29udHJhY3QucnM=) | `92.60% <100.00%> (+0.59%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0)


Patch:
diff --git a/contracts/cw-proposal-single/schema/migrate_msg.json b/contracts/cw-proposal-single/schema/migrate_msg.json
index 87b18ea7..c7f5b861 100644
--- a/contracts/cw-proposal-single/schema/migrate_msg.json
+++ b/contracts/cw-proposal-single/schema/migrate_msg.json
@@ -1,5 +1,9 @@
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "MigrateMsg",
-  "type": "object"
+  "type": "string",
+  "enum": [
+    "FromV1",
+    "FromCompatible"
+  ]
 }
diff --git a/contracts/cw-proposal-single/src/contract.rs b/contracts/cw-proposal-single/src/contract.rs
index 5f473f7e..07c889e7 100644
--- a/contracts/cw-proposal-single/src/contract.rs
+++ b/contracts/cw-proposal-single/src/contract.rs
@@ -1,18 +1,20 @@
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
 use cosmwasm_std::{
-    to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Reply, Response,
-    StdResult, Storage, SubMsg, WasmMsg,
+    to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Empty, Env, MessageInfo, Order, Reply,
+    Response, StdResult, Storage, SubMsg, Timestamp, Uint128, WasmMsg,
 };
 use cw2::set_contract_version;
 use cw_core_interface::voting::IsActiveResponse;
-use cw_storage_plus::Bound;
-use cw_utils::Duration;
+use cw_storage_plus::{Bound, Map};
+use cw_utils::{Duration, Expiration};
 use indexable_hooks::Hooks;
 use proposal_hooks::{new_proposal_hooks, proposal_status_changed_hooks};
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 use vote_hooks::new_vote_hooks;
 
-use voting::deposit::{get_deposit_msg, get_return_deposit_msg, DepositInfo};
+use voting::deposit::{get_deposit_msg, get_return_deposit_msg, CheckedDepositInfo, DepositInfo};
 use voting::proposal::{DEFAULT_LIMIT, MAX_PROPOSAL_SIZE};
 use voting::reply::{mask_proposal_execution_proposal_id, TaggedReplyId};
 use voting::status::Status;
@@ -726,9 +728,71 @@ pub fn query_info(deps: Deps) -> StdResult<Binary> {
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
-pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
-    // Don't do any state migrations.
-    Ok(Response::default())
+pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
+    /// This is proposal version is from commit e531c760a5d057329afd98d62567aaa4dca2c96f (v1.0.0) and code ID 427.
+    #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+    struct V1Proposal {
+        pub title: String,
+        pub description: String,
+        pub proposer: Addr,
+        pub start_height: u64,
+        pub min_voting_period: Option<Expiration>,
+        pub expiration: Expiration,
+        pub threshold: Threshold,
+        pub total_power: Uint128,
+        pub msgs: Vec<CosmosMsg<Empty>>,
+        pub status: Status,
+        pub votes: Votes,
+        pub allow_revoting: bool,
+        pub deposit_info: Option<CheckedDepositInfo>,
+    }
+
+    match msg {
+        MigrateMsg::FromV1 {} => {
+            // Retrieve current map from storage
+            let current_map: Map<u64, V1Proposal> = Map::new("proposals");
+            let current = current_map
+                .range(deps.storage, None, None, Order::Ascending)
+                .collect::<StdResult<Vec<(u64, V1Proposal)>>>()?;
+
+            // Add migrated entries to new map.
+            // Based on gas usage testing, we estimate that we will be able to migrate ~4200
+            // proposals at a time before reaching the block max_gas limit.
+            current
+                .into_iter()
+                .try_for_each::<_, StdResult<()>>(|(id, prop)| {
+                    let migrated_proposal = SingleChoiceProposal {
+                        title: prop.title,
+                        description: prop.description,
+                        proposer: prop.proposer,
+                        start_height: prop.start_height,
+                        min_voting_period: prop.min_voting_period,
+                        expiration: prop.expiration,
+                        threshold: prop.threshold,
+                        total_power: prop.total_power,
+                        msgs: prop.msgs,
+                        status: prop.status,
+                        votes: prop.votes,
+                        allow_revoting: prop.allow_revoting,
+                        deposit_info: prop.deposit_info,
+                        // Cosmwasm does not expose a way to query the timestamp
+                        // of a block given block height. As such, we assign migrated
+                        // proposals a created timestamp of 0 so that the frontend may
+                        // query for the true timestamp given `start_height`.
+                        created: Timestamp::from_seconds(0),
+                        last_updated: env.block.time,
+                    };
+
+                    PROPOSALS.save(deps.storage, id, &migrated_proposal)?;
+
+                    Ok(())
+                })?;
+
+            Ok(Response::default())
+        }
+
+        MigrateMsg::FromCompatible {} => Ok(Response::default()),
+    }
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
diff --git a/contracts/cw-proposal-single/src/msg.rs b/contracts/cw-proposal-single/src/msg.rs
index aa0d68cb..a40b42bb 100644
--- a/contracts/cw-proposal-single/src/msg.rs
+++ b/contracts/cw-proposal-single/src/msg.rs
@@ -207,4 +207,7 @@ pub enum QueryMsg {
 }
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
-pub struct MigrateMsg {}
+pub enum MigrateMsg {
+    FromV1,
+    FromCompatible,
+}
diff --git a/contracts/cw-proposal-single/src/state.rs b/contracts/cw-proposal-single/src/state.rs
index f73ea2fd..6950d54c 100644
--- a/contracts/cw-proposal-single/src/state.rs
+++ b/contracts/cw-proposal-single/src/state.rs
@@ -61,7 +61,7 @@ pub struct Config {
 pub const CONFIG: Item<Config> = Item::new("config");
 /// The number of proposals that have been created.
 pub const PROPOSAL_COUNT: Item<u64> = Item::new("proposal_count");
-pub const PROPOSALS: Map<u64, SingleChoiceProposal> = Map::new("proposals");
+pub const PROPOSALS: Map<u64, SingleChoiceProposal> = Map::new("proposals_v2");
 pub const BALLOTS: Map<(u64, Addr), Ballot> = Map::new("ballots");
 /// Consumers of proposal state change hooks.
 pub const PROPOSAL_HOOKS: Hooks = Hooks::new("proposal_hooks");
diff --git a/types/contracts/cw-proposal-single/CwProposalSingleContract.ts b/types/contracts/cw-proposal-single/CwProposalSingleContract.ts
index 5bef50a6..a7576598 100644
--- a/types/contracts/cw-proposal-single/CwProposalSingleContract.ts
+++ b/types/contracts/cw-proposal-single/CwProposalSingleContract.ts
@@ -354,9 +354,7 @@ export interface ListVotesResponse {
   votes: VoteInfo[];
   [k: string]: unknown;
 }
-export interface MigrateMsg {
-  [k: string]: unknown;
-}
+export type MigrateMsg = "FromV1" | "FromCompatible";
 export type ProposalCountResponse = number;
 export interface ProposalHooksResponse {
   hooks: string[];
diff --git a/types/yarn.lock b/types/yarn.lock
index dbd120db..d818edb5 100644
--- a/types/yarn.lock
+++ b/types/yarn.lock
@@ -3466,6 +3466,11 @@ util@^0.10.3:
   dependencies:
     inherits "2.0.3"
 
+v8-compile-cache-lib@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
+  integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
+
 wasm-ast-types@^0.3.5:
   version "0.3.5"
   resolved "https://registry.yarnpkg.com/wasm-ast-types/-/wasm-ast-types-0.3.5.tgz#b46d7b5f4f9312f45278f731f9eb496fdf2f4ca3"


Test patch:
diff --git a/contracts/cw-proposal-single/src/tests.rs b/contracts/cw-proposal-single/src/tests.rs
index 53bd830b..f078a1bc 100644
--- a/contracts/cw-proposal-single/src/tests.rs
+++ b/contracts/cw-proposal-single/src/tests.rs
@@ -1,15 +1,22 @@
 use std::u128;
 
-use cosmwasm_std::{to_binary, Addr, Coin, CosmosMsg, Decimal, Empty, Timestamp, Uint128, WasmMsg};
+use cosmwasm_std::{
+    testing::{mock_dependencies, mock_env},
+    to_binary, Addr, Coin, CosmosMsg, Decimal, Empty, Order, Timestamp, Uint128, WasmMsg,
+};
 use cw20::Cw20Coin;
 use cw20_staked_balance_voting::msg::ActiveThreshold;
 use cw_multi_test::{next_block, App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg};
 
 use cw_core::{msg::ModuleInstantiateInfo, state::ProposalModule};
+use cw_storage_plus::Map;
 use cw_utils::Duration;
+use cw_utils::Expiration;
 
 use indexable_hooks::HooksResponse;
 
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
 use testing::{ShouldExecute, TestSingleChoiceVote};
 use voting::{
     deposit::{CheckedDepositInfo, DepositInfo, DepositToken},
@@ -19,6 +26,7 @@ use voting::{
 };
 
 use crate::{
+    contract::migrate,
     msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg},
     proposal::SingleChoiceProposal,
     query::{ProposalListResponse, ProposalResponse, VoteInfo, VoteResponse},
@@ -28,6 +36,23 @@ use crate::{
 
 const CREATOR_ADDR: &str = "creator";
 
+#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
+struct BetaProposal {
+    pub title: String,
+    pub description: String,
+    pub proposer: Addr,
+    pub start_height: u64,
+    pub min_voting_period: Option<Expiration>,
+    pub expiration: Expiration,
+    pub threshold: Threshold,
+    pub total_power: Uint128,
+    pub msgs: Vec<CosmosMsg<Empty>>,
+    pub status: Status,
+    pub votes: Votes,
+    pub allow_revoting: bool,
+    pub deposit_info: Option<CheckedDepositInfo>,
+}
+
 fn cw20_contract() -> Box<dyn Contract<Empty>> {
     let contract = ContractWrapper::new(
         cw20_base::contract::execute,
@@ -3765,7 +3790,7 @@ fn test_large_absolute_count_threshold() {
 }
 
 #[test]
-fn test_migrate() {
+fn test_migrate_from_compatible() {
     let mut app = App::default();
     let govmod_id = app.store_code(proposal_contract());
 
@@ -3809,7 +3834,7 @@ fn test_migrate() {
         CosmosMsg::Wasm(WasmMsg::Migrate {
             contract_addr: govmod_single.to_string(),
             new_code_id: govmod_id,
-            msg: to_binary(&MigrateMsg {}).unwrap(),
+            msg: to_binary(&MigrateMsg::FromCompatible {}).unwrap(),
         }),
     )
     .unwrap();
@@ -3822,6 +3847,82 @@ fn test_migrate() {
     assert_eq!(config, new_config);
 }
 
+#[test]
+fn test_migrate_from_beta() {
+    let mut app = App::default();
+    let govmod_id = app.store_code(proposal_contract());
+
+    let threshold = Threshold::AbsolutePercentage {
+        percentage: PercentageThreshold::Majority {},
+    };
+    let max_voting_period = cw_utils::Duration::Height(6);
+    let instantiate = InstantiateMsg {
+        threshold,
+        max_voting_period,
+        min_voting_period: None,
+        only_members_execute: false,
+        allow_revoting: false,
+        deposit_info: None,
+        close_proposal_on_execution_failure: false,
+    };
+
+    let governance_addr =
+        instantiate_with_cw20_balances_governance(&mut app, govmod_id, instantiate, None);
+    let governance_modules: Vec<ProposalModule> = app
+        .wrap()
+        .query_wasm_smart(
+            governance_addr.clone(),
+            &cw_core::msg::QueryMsg::ProposalModules {
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap().address;
+
+    // Create proposal
+    app.execute_contract(
+        Addr::unchecked(CREATOR_ADDR),
+        govmod_single.clone(),
+        &ExecuteMsg::Propose {
+            title: "Text proposal".to_string(),
+            description: "This is a simple text proposal".to_string(),
+            msgs: vec![],
+        },
+        &[],
+    )
+    .unwrap();
+
+    // Migrate from beta
+    app.execute(
+        governance_addr,
+        CosmosMsg::Wasm(WasmMsg::Migrate {
+            contract_addr: govmod_single.to_string(),
+            new_code_id: govmod_id,
+            msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(),
+        }),
+    )
+    .unwrap();
+
+    let proposals_response: ProposalListResponse = app
+        .wrap()
+        .query_wasm_smart(
+            govmod_single,
+            &QueryMsg::ListProposals {
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+
+    assert_eq!(proposals_response.proposals.len(), 1);
+    let prop = &proposals_response.proposals[0].proposal;
+    assert_eq!(prop.created, app.block_info().time);
+    assert_eq!(prop.last_updated, app.block_info().time);
+}
+
 #[test]
 fn test_proposal_count_initialized_to_zero() {
     let mut app = App::default();
@@ -4324,6 +4425,54 @@ fn test_timestamp_updated() {
     assert_eq!(updated.proposal.status, Status::Closed);
 }
 
+#[test]
+fn test_migrate_mock() {
+    let mut deps = mock_dependencies();
+    let env = mock_env();
+    let current_block = &env.block;
+    let max_voting_period = cw_utils::Duration::Height(6);
+
+    let threshold = Threshold::AbsolutePercentage {
+        percentage: PercentageThreshold::Majority {},
+    };
+
+    // Write to storage in old data format
+    let beta_map: Map<u64, BetaProposal> = Map::new("proposals");
+
+    let beta_proposal = BetaProposal {
+        title: "A simple text proposal".to_string(),
+        description: "This is a simple text proposal".to_string(),
+        proposer: Addr::unchecked(CREATOR_ADDR),
+        start_height: env.block.height,
+        expiration: max_voting_period.after(current_block),
+        min_voting_period: None,
+        threshold,
+        allow_revoting: false,
+        total_power: Uint128::new(100_000_000),
+        msgs: vec![],
+        status: Status::Open,
+        votes: Votes::zero(),
+        deposit_info: None,
+    };
+
+    beta_map.save(&mut deps.storage, 0, &beta_proposal).unwrap();
+
+    let msg = MigrateMsg::FromV1 {};
+    migrate(deps.as_mut(), env.clone(), msg).unwrap();
+
+    // Verify migration.
+    let new_map: Map<u64, SingleChoiceProposal> = Map::new("proposals_v2");
+    let proposals: Vec<(u64, SingleChoiceProposal)> = new_map
+        .range(&deps.storage, None, None, Order::Ascending)
+        .collect::<Result<Vec<(u64, SingleChoiceProposal)>, _>>()
+        .unwrap();
+
+    let migrated_proposal = &proposals[0];
+    assert_eq!(migrated_proposal.0, 0);
+    assert_eq!(migrated_proposal.1.created, Timestamp::from_seconds(0));
+    assert_eq!(migrated_proposal.1.last_updated, env.block.time);
+}
+
 #[test]
 fn test_return_deposit_to_dao_on_proposal_failure() {
     let (mut app, core_addr) = do_test_votes_cw20_balances(