Repository: DA0-DA0/dao-contracts
Pull request #362 (2022-06-18 01:24:22)

Add migrate method to cw20-stake to allow migration from beta.
This adds a migrate method to `cw20-stake` which allows migration from a beta staking contract to a non-beta staking contract. I have tested this migration using a testnet DAO which can be found here: https://testnet.daodao.zone/dao/juno1m4wsr4wj0hz9uz4kavh0yt8v3ghsf347etqzt66rkqprfn5myajqhw3vpd


Patch:
diff --git a/contracts/cw-core/examples/schema.rs b/contracts/cw-core/examples/schema.rs
index 5d0616c5..2b27dc67 100644
--- a/contracts/cw-core/examples/schema.rs
+++ b/contracts/cw-core/examples/schema.rs
@@ -5,10 +5,7 @@ use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, s
 use cosmwasm_std::Addr;
 use cw_core::{
     msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg},
-    query::{
-        AdminNominationResponse, Cw20BalanceResponse, DumpStateResponse, GetItemResponse,
-        PauseInfoResponse,
-    },
+    query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse, PauseInfoResponse},
     state::Config,
 };
 use cw_core_interface::voting::{
@@ -32,7 +29,6 @@ fn main() {
     export_schema(&schema_for!(InfoResponse), &out_dir);
     export_schema(&schema_for!(TotalPowerAtHeightResponse), &out_dir);
     export_schema(&schema_for!(VotingPowerAtHeightResponse), &out_dir);
-    export_schema(&schema_for!(AdminNominationResponse), &out_dir);
 
     // Auto TS code generation expects the query return type as QueryNameResponse
     // Here we map query resonses to the correct name
diff --git a/contracts/cw-core/schema/admin_nomination_response.json b/contracts/cw-core/schema/admin_nomination_response.json
deleted file mode 100644
index 7e2b4af9..00000000
--- a/contracts/cw-core/schema/admin_nomination_response.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-  "$schema": "http://json-schema.org/draft-07/schema#",
-  "title": "AdminNominationResponse",
-  "description": "Returned by the `AdminNomination` query.",
-  "type": "object",
-  "properties": {
-    "nomination": {
-      "description": "The currently nominated admin or None if no nomination is pending.",
-      "anyOf": [
-        {
-          "$ref": "#/definitions/Addr"
-        },
-        {
-          "type": "null"
-        }
-      ]
-    }
-  },
-  "definitions": {
-    "Addr": {
-      "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
-      "type": "string"
-    }
-  }
-}
diff --git a/contracts/cw-core/schema/cw20_balances_response.json b/contracts/cw-core/schema/cw20_balances_response.json
index 34300995..3dd46383 100644
--- a/contracts/cw-core/schema/cw20_balances_response.json
+++ b/contracts/cw-core/schema/cw20_balances_response.json
@@ -1,7 +1,7 @@
 {
   "$schema": "http://json-schema.org/draft-07/schema#",
   "title": "Cw20BalancesResponse",
-  "description": "Returned by the `Cw20Balances` query.",
+  "description": "Returned by Cw20Balances query.",
   "type": "object",
   "required": [
     "addr",
diff --git a/contracts/cw-core/schema/dump_state_response.json b/contracts/cw-core/schema/dump_state_response.json
index e363b55b..232555e1 100644
--- a/contracts/cw-core/schema/dump_state_response.json
+++ b/contracts/cw-core/schema/dump_state_response.json
@@ -4,7 +4,6 @@
   "description": "Relevant state for the governance module. Returned by the `DumpState` query.",
   "type": "object",
   "required": [
-    "admin",
     "config",
     "pause_info",
     "proposal_modules",
@@ -14,9 +13,12 @@
   "properties": {
     "admin": {
       "description": "Optional DAO Admin",
-      "allOf": [
+      "anyOf": [
         {
           "$ref": "#/definitions/Addr"
+        },
+        {
+          "type": "null"
         }
       ]
     },
diff --git a/contracts/cw-core/schema/execute_msg.json b/contracts/cw-core/schema/execute_msg.json
index de05dbbe..7aee7c6e 100644
--- a/contracts/cw-core/schema/execute_msg.json
+++ b/contracts/cw-core/schema/execute_msg.json
@@ -144,19 +144,23 @@
       "additionalProperties": false
     },
     {
-      "description": "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 that new admin may become the admin by executing the `AcceptAdminNomination` message.\n\nIf there is already a pending admin nomination the `WithdrawAdminNomination` message must be executed before a new admin may be nominated.",
+      "description": "Callable by the Admin, if one is configured, changes the current admin. Setting to None removes the Admin.",
       "type": "object",
       "required": [
-        "nominate_admin"
+        "update_admin"
       ],
       "properties": {
-        "nominate_admin": {
+        "update_admin": {
           "type": "object",
           "properties": {
             "admin": {
-              "type": [
-                "string",
-                "null"
+              "anyOf": [
+                {
+                  "$ref": "#/definitions/Addr"
+                },
+                {
+                  "type": "null"
+                }
               ]
             }
           }
@@ -164,32 +168,6 @@
       },
       "additionalProperties": false
     },
-    {
-      "description": "Callable by a nominated admin. Admins are nominated via the `NominateAdmin` message. Accepting a nomination will make the nominated address the new admin.\n\nRequiring that the new admin accepts the nomination before becoming the admin protects against a typo causing the admin to change to an invalid address.",
-      "type": "object",
-      "required": [
-        "accept_admin_nomination"
-      ],
-      "properties": {
-        "accept_admin_nomination": {
-          "type": "object"
-        }
-      },
-      "additionalProperties": false
-    },
-    {
-      "description": "Callable by the current admin. Withdraws the current admin nomination.",
-      "type": "object",
-      "required": [
-        "withdraw_admin_nomination"
-      ],
-      "properties": {
-        "withdraw_admin_nomination": {
-          "type": "object"
-        }
-      },
-      "additionalProperties": false
-    },
     {
       "description": "Callable by the core contract. Replaces the current governance contract config with the provided config.",
       "type": "object",
@@ -327,6 +305,10 @@
     }
   ],
   "definitions": {
+    "Addr": {
+      "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
+      "type": "string"
+    },
     "Admin": {
       "description": "Information about the admin of a contract.",
       "oneOf": [
diff --git a/contracts/cw-core/schema/instantiate_msg.json b/contracts/cw-core/schema/instantiate_msg.json
index df235663..41f7da75 100644
--- a/contracts/cw-core/schema/instantiate_msg.json
+++ b/contracts/cw-core/schema/instantiate_msg.json
@@ -12,7 +12,7 @@
   ],
   "properties": {
     "admin": {
-      "description": "Optional Admin with the ability to execute DAO messages directly. Useful for building SubDAOs controlled by a parent DAO. If no admin is specified the contract is set as its own admin so that the admin may be updated later by governance.",
+      "description": "Optional Admin with the ability to execute DAO messages directly Useful for building SubDAOs completely controlled by a parent DAO.",
       "type": [
         "string",
         "null"
diff --git a/contracts/cw-core/schema/query_msg.json b/contracts/cw-core/schema/query_msg.json
index 560f1c7f..9ce26da7 100644
--- a/contracts/cw-core/schema/query_msg.json
+++ b/contracts/cw-core/schema/query_msg.json
@@ -3,7 +3,7 @@
   "title": "QueryMsg",
   "oneOf": [
     {
-      "description": "Get's the DAO's admin. Returns `Addr`.",
+      "description": "Get's the DAO's admin (if configured).",
       "type": "object",
       "required": [
         "admin"
@@ -15,19 +15,6 @@
       },
       "additionalProperties": false
     },
-    {
-      "description": "Get's the currently nominated admin (if any). Returns `AdminNominationResponse`.",
-      "type": "object",
-      "required": [
-        "admin_nomination"
-      ],
-      "properties": {
-        "admin_nomination": {
-          "type": "object"
-        }
-      },
-      "additionalProperties": false
-    },
     {
       "description": "Gets the contract's config. Returns Config.",
       "type": "object",
diff --git a/contracts/cw-core/src/contract.rs b/contracts/cw-core/src/contract.rs
index 2c5d3e5c..b2ff2eae 100644
--- a/contracts/cw-core/src/contract.rs
+++ b/contracts/cw-core/src/contract.rs
@@ -15,13 +15,9 @@ use crate::error::ContractError;
 use crate::msg::{
     ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, ModuleInstantiateInfo, QueryMsg,
 };
-use crate::query::{
-    AdminNominationResponse, Cw20BalanceResponse, DumpStateResponse, GetItemResponse,
-    PauseInfoResponse,
-};
+use crate::query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse, PauseInfoResponse};
 use crate::state::{
-    Config, ADMIN, CONFIG, CW20_LIST, CW721_LIST, ITEMS, NOMINATED_ADMIN, PAUSED, PROPOSAL_MODULES,
-    VOTING_MODULE,
+    Config, ADMIN, CONFIG, CW20_LIST, CW721_LIST, ITEMS, PAUSED, PROPOSAL_MODULES, VOTING_MODULE,
 };
 
 // version info for migration info
@@ -53,9 +49,7 @@ pub fn instantiate(
     let admin = msg
         .admin
         .map(|human| deps.api.addr_validate(&human))
-        .transpose()?
-        // If no admin is specified, the contract is its own admin.
-        .unwrap_or_else(|| env.contract.address.clone());
+        .transpose()?;
     ADMIN.save(deps.storage, &admin)?;
 
     let vote_module_msg = msg
@@ -111,6 +105,7 @@ pub fn execute(
         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::UpdateAdmin { admin } => execute_update_admin(deps, info.sender, admin),
         ExecuteMsg::UpdateConfig { config } => {
             execute_update_config(deps, env, info.sender, config)
         }
@@ -126,13 +121,6 @@ pub fn execute(
         ExecuteMsg::UpdateProposalModules { to_add, to_remove } => {
             execute_update_proposal_modules(deps, env, info.sender, to_add, to_remove)
         }
-        ExecuteMsg::NominateAdmin { admin } => {
-            execute_nominate_admin(deps, env, info.sender, admin)
-        }
-        ExecuteMsg::AcceptAdminNomination {} => execute_accept_admin_nomination(deps, info.sender),
-        ExecuteMsg::WithdrawAdminNomination {} => {
-            execute_withdraw_admin_nomination(deps, info.sender)
-        }
     }
 }
 
@@ -164,14 +152,19 @@ pub fn execute_admin_msgs(
 ) -> Result<Response, ContractError> {
     let admin = ADMIN.load(deps.storage)?;
 
-    // Check if the sender is the DAO Admin
-    if sender != admin {
-        return Err(ContractError::Unauthorized {});
-    }
+    match admin {
+        Some(admin) => {
+            // Check if the sender is the DAO Admin
+            if sender != admin {
+                return Err(ContractError::Unauthorized {});
+            }
 
-    Ok(Response::default()
-        .add_attribute("action", "execute_admin_msgs")
-        .add_messages(msgs))
+            Ok(Response::default()
+                .add_attribute("action", "execute_admin_msgs")
+                .add_messages(msgs))
+        }
+        None => Err(ContractError::NoAdmin {}),
+    }
 }
 
 pub fn execute_proposal_hook(
@@ -189,79 +182,37 @@ pub fn execute_proposal_hook(
         .add_messages(msgs))
 }
 
-pub fn execute_nominate_admin(
+pub fn execute_update_admin(
     deps: DepsMut,
-    env: Env,
     sender: Addr,
-    nomination: Option<String>,
+    admin: Option<Addr>,
 ) -> Result<Response, ContractError> {
-    let nomination = nomination.map(|h| deps.api.addr_validate(&h)).transpose()?;
-
     let current_admin = ADMIN.load(deps.storage)?;
-    if current_admin != sender {
-        return Err(ContractError::Unauthorized {});
-    }
-
-    let current_nomination = NOMINATED_ADMIN.may_load(deps.storage)?;
-    if current_nomination.is_some() {
-        return Err(ContractError::PendingNomination {});
-    }
-
-    match &nomination {
-        Some(nomination) => NOMINATED_ADMIN.save(deps.storage, nomination)?,
-        // If no admin set to default of the contract. This allows the
-        // contract to later set a new admin via governance.
-        None => ADMIN.save(deps.storage, &env.contract.address)?,
-    }
-
-    Ok(Response::default()
-        .add_attribute("action", "execute_nominate_admin")
-        .add_attribute(
-            "nomination",
-            nomination
-                .map(|n| n.to_string())
-                .unwrap_or_else(|| "None".to_string()),
-        ))
-}
 
-pub fn execute_accept_admin_nomination(
-    deps: DepsMut,
-    sender: Addr,
-) -> Result<Response, ContractError> {
-    let nomination = NOMINATED_ADMIN
-        .may_load(deps.storage)?
-        .ok_or(ContractError::NoAdminNomination {})?;
-    if sender != nomination {
-        return Err(ContractError::Unauthorized {});
-    }
-    NOMINATED_ADMIN.remove(deps.storage);
-    ADMIN.save(deps.storage, &nomination)?;
-
-    Ok(Response::default()
-        .add_attribute("action", "execute_accept_admin_nomination")
-        .add_attribute("new_admin", sender))
-}
-
-pub fn execute_withdraw_admin_nomination(
-    deps: DepsMut,
-    sender: Addr,
-) -> Result<Response, ContractError> {
-    let admin = ADMIN.load(deps.storage)?;
-    if admin != sender {
-        return Err(ContractError::Unauthorized {});
-    }
+    match current_admin {
+        Some(current_admin) => {
+            // Check sender is the DAO Admin
+            if sender != current_admin {
+                return Err(ContractError::Unauthorized {});
+            }
 
-    // Check that there is indeed a nomination to withdraw.
-    let current_nomination = NOMINATED_ADMIN.may_load(deps.storage)?;
-    if current_nomination.is_none() {
-        return Err(ContractError::NoAdminNomination {});
+            // Save the new DAO Admin (which may be set to None)
+            ADMIN.save(deps.storage, &admin)?;
+
+            Ok(Response::default()
+                .add_attribute("action", "execute_update_admin")
+                .add_attribute(
+                    "new_admin",
+                    admin
+                        .map(|a| a.into_string())
+                        .unwrap_or_else(|| "None".to_string()),
+                ))
+        }
+        None => {
+            // If no DAO admin is configured, return unauthorized
+            Err(ContractError::Unauthorized {})
+        }
     }
-
-    NOMINATED_ADMIN.remove(deps.storage);
-
-    Ok(Response::default()
-        .add_attribute("action", "execute_withdraw_admin_nomination")
-        .add_attribute("sender", sender))
 }
 
 pub fn execute_update_config(
@@ -466,7 +417,6 @@ pub fn execute_receive_cw721(deps: DepsMut, sender: Addr) -> Result<Response, Co
 pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
         QueryMsg::Admin {} => query_admin(deps),
-        QueryMsg::AdminNomination {} => query_admin_nomination(deps),
         QueryMsg::Config {} => query_config(deps),
         QueryMsg::Cw20TokenList { start_at, limit } => query_cw20_list(deps, start_at, limit),
         QueryMsg::Cw20Balances { start_at, limit } => {
@@ -494,11 +444,6 @@ pub fn query_admin(deps: Deps) -> StdResult<Binary> {
     to_binary(&admin)
 }
 
-pub fn query_admin_nomination(deps: Deps) -> StdResult<Binary> {
-    let nomination = NOMINATED_ADMIN.may_load(deps.storage)?;
-    to_binary(&AdminNominationResponse { nomination })
-}
-
 pub fn query_config(deps: Deps) -> StdResult<Binary> {
     let config = CONFIG.load(deps.storage)?;
     to_binary(&config)
diff --git a/contracts/cw-core/src/error.rs b/contracts/cw-core/src/error.rs
index dfb6fc66..d9a551d1 100644
--- a/contracts/cw-core/src/error.rs
+++ b/contracts/cw-core/src/error.rs
@@ -10,6 +10,9 @@ pub enum ContractError {
     #[error("Unauthorized.")]
     Unauthorized {},
 
+    #[error("DAO does not have an admin configured.")]
+    NoAdmin {},
+
     #[error("The contract is paused.")]
     Paused {},
 
@@ -31,14 +34,9 @@ pub enum ContractError {
     #[error("Unsigned integer overflow.")]
     Overflow {},
 
+    #[error("You can only instantiate {0} items during instantiation, but you tried to instantiate {1}.")]
+    TooManyItems(u64, usize),
+
     #[error("Key is missing from storage")]
     KeyMissing {},
-
-    #[error("No pending admin nomination.")]
-    NoAdminNomination {},
-
-    #[error(
-        "The pending admin nomination must be withdrawn before a new nomination can be created."
-    )]
-    PendingNomination {},
 }
diff --git a/contracts/cw-core/src/msg.rs b/contracts/cw-core/src/msg.rs
index 5ef1991f..22e61114 100644
--- a/contracts/cw-core/src/msg.rs
+++ b/contracts/cw-core/src/msg.rs
@@ -1,4 +1,4 @@
-use cosmwasm_std::{Binary, CosmosMsg, Empty};
+use cosmwasm_std::{Addr, Binary, CosmosMsg, Empty};
 use cw_utils::Duration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -43,10 +43,8 @@ pub struct InitialItem {
 
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 pub struct InstantiateMsg {
-    /// Optional Admin with the ability to execute DAO messages
-    /// directly. Useful for building SubDAOs controlled by a parent
-    /// DAO. If no admin is specified the contract is set as its own
-    /// admin so that the admin may be updated later by governance.
+    /// Optional Admin with the ability to execute DAO messages directly
+    /// Useful for building SubDAOs completely controlled by a parent DAO.
     pub admin: Option<String>,
     /// The name of the core contract.
     pub name: String,
@@ -102,27 +100,9 @@ pub enum ExecuteMsg {
     /// item already exists the existing value is overriden. If the
     /// item does not exist a new item is added.
     SetItem { key: String, addr: 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
-    /// that new admin may become the admin by executing the
-    /// `AcceptAdminNomination` message.
-    ///
-    /// If there is already a pending admin nomination the
-    /// `WithdrawAdminNomination` message must be executed before a
-    /// new admin may be nominated.
-    NominateAdmin { admin: Option<String> },
-    /// Callable by a nominated admin. Admins are nominated via the
-    /// `NominateAdmin` message. Accepting a nomination will make the
-    /// nominated address the new admin.
-    ///
-    /// Requiring that the new admin accepts the nomination before
-    /// becoming the admin protects against a typo causing the admin
-    /// to change to an invalid address.
-    AcceptAdminNomination {},
-    /// Callable by the current admin. Withdraws the current admin
-    /// nomination.
-    WithdrawAdminNomination {},
+    /// Callable by the Admin, if one is configured, changes the
+    /// current admin. Setting to None removes the Admin.
+    UpdateAdmin { admin: Option<Addr> },
     /// Callable by the core contract. Replaces the current
     /// governance contract config with the provided config.
     UpdateConfig { config: Config },
@@ -153,11 +133,8 @@ pub enum ExecuteMsg {
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum QueryMsg {
-    /// Get's the DAO's admin. Returns `Addr`.
+    /// Get's the DAO's admin (if configured).
     Admin {},
-    /// Get's the currently nominated admin (if any). Returns
-    /// `AdminNominationResponse`.
-    AdminNomination {},
     /// Gets the contract's config. Returns Config.
     Config {},
     /// Gets the token balance for each cw20 registered with the
diff --git a/contracts/cw-core/src/query.rs b/contracts/cw-core/src/query.rs
index 2aa4bf97..ee10975d 100644
--- a/contracts/cw-core/src/query.rs
+++ b/contracts/cw-core/src/query.rs
@@ -11,7 +11,7 @@ use crate::state::Config;
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
 pub struct DumpStateResponse {
     /// Optional DAO Admin
-    pub admin: Addr,
+    pub admin: Option<Addr>,
     /// The governance contract's config.
     pub config: Config,
     // True if the contract is currently paused.
@@ -40,7 +40,7 @@ pub struct GetItemResponse {
     pub item: Option<String>,
 }
 
-/// Returned by the `Cw20Balances` query.
+/// Returned by Cw20Balances query.
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 pub struct Cw20BalanceResponse {
     /// The address of the token.
@@ -48,11 +48,3 @@ pub struct Cw20BalanceResponse {
     /// The contract's balance.
     pub balance: Uint128,
 }
-
-/// Returned by the `AdminNomination` query.
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
-pub struct AdminNominationResponse {
-    /// The currently nominated admin or None if no nomination is
-    /// pending.
-    pub nomination: Option<Addr>,
-}
diff --git a/contracts/cw-core/src/state.rs b/contracts/cw-core/src/state.rs
index f2c267dc..705ebb0b 100644
--- a/contracts/cw-core/src/state.rs
+++ b/contracts/cw-core/src/state.rs
@@ -22,23 +22,7 @@ pub struct Config {
     pub automatically_add_cw721s: bool,
 }
 
-/// The admin of the contract. Typically a DAO. The contract admin may
-/// unilaterally execute messages on this contract.
-///
-/// In cases where no admin is wanted the admin should be set to the
-/// contract itself. This will happen by default if no admin is
-/// specified in `NominateAdmin` and instantiate messages.
-pub const ADMIN: Item<Addr> = Item::new("admin");
-
-/// A new admin that has been nominated by the current admin. The
-/// nominated admin must accept the proposal before becoming the admin
-/// themselves.
-///
-/// NOTE: If no admin is currently nominated this will not have a
-/// value set. To load this value, use
-/// `NOMINATED_ADMIN.may_load(deps.storage)`.
-pub const NOMINATED_ADMIN: Item<Addr> = Item::new("nominated_admin");
-
+pub const ADMIN: Item<Option<Addr>> = Item::new("admin");
 pub const CONFIG: Item<Config> = Item::new("config");
 
 pub const PAUSED: Item<Expiration> = Item::new("paused");
diff --git a/contracts/cw20-stake/src/contract.rs b/contracts/cw20-stake/src/contract.rs
index 8a8f2e77..baf970f9 100644
--- a/contracts/cw20-stake/src/contract.rs
+++ b/contracts/cw20-stake/src/contract.rs
@@ -2,16 +2,16 @@
 use cosmwasm_std::entry_point;
 
 use cosmwasm_std::{
-    from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response,
-    StdError, StdResult, Uint128,
+    from_binary, from_slice, to_binary, to_vec, Addr, Binary, Deps, DepsMut, Empty, Env,
+    MessageInfo, Response, StdError, StdResult, Uint128,
 };
 
 use cw20::Cw20ReceiveMsg;
 
 use crate::hooks::{stake_hook_msgs, unstake_hook_msgs};
 use crate::msg::{
-    ExecuteMsg, GetConfigResponse, GetHooksResponse, InstantiateMsg, QueryMsg, ReceiveMsg,
-    StakedBalanceAtHeightResponse, StakedValueResponse, TotalStakedAtHeightResponse,
+    ExecuteMsg, GetConfigResponse, GetHooksResponse, InstantiateMsg, MigrateMsg, QueryMsg,
+    ReceiveMsg, StakedBalanceAtHeightResponse, StakedValueResponse, TotalStakedAtHeightResponse,
     TotalValueResponse,
 };
 use crate::state::{
@@ -436,6 +436,39 @@ pub fn query_hooks(deps: Deps) -> StdResult<GetHooksResponse> {
     })
 }
 
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
+    use serde::{Deserialize, Serialize};
+
+    #[derive(Serialize, Deserialize, Clone)]
+    struct BetaConfig {
+        pub admin: Addr,
+        pub token_address: Addr,
+        pub unstaking_duration: Option<Duration>,
+    }
+
+    match msg {
+        MigrateMsg::FromBeta { manager } => {
+            let data = deps
+                .storage
+                .get(b"config")
+                .ok_or_else(|| StdError::not_found("config"))?;
+            let beta_config: BetaConfig = from_slice(&data)?;
+            let new_config = Config {
+                owner: Some(beta_config.admin),
+                manager: manager
+                    .map(|human| deps.api.addr_validate(&human))
+                    .transpose()?,
+                token_address: beta_config.token_address,
+                unstaking_duration: beta_config.unstaking_duration,
+            };
+            deps.storage.set(b"config", &to_vec(&new_config)?);
+            Ok(Response::default())
+        }
+        MigrateMsg::FromCompatible {} => Ok(Response::default()),
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use std::borrow::BorrowMut;
diff --git a/contracts/cw20-stake/src/msg.rs b/contracts/cw20-stake/src/msg.rs
index 973fd66c..08ca1f38 100644
--- a/contracts/cw20-stake/src/msg.rs
+++ b/contracts/cw20-stake/src/msg.rs
@@ -66,6 +66,13 @@ pub enum QueryMsg {
     GetHooks {},
 }
 
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum MigrateMsg {
+    FromBeta { manager: Option<String> },
+    FromCompatible {},
+}
+
 #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub struct StakedBalanceAtHeightResponse {


Test patch:
diff --git a/contracts/cw-core/src/tests.rs b/contracts/cw-core/src/tests.rs
index cf1f9f9c..68cf1578 100644
--- a/contracts/cw-core/src/tests.rs
+++ b/contracts/cw-core/src/tests.rs
@@ -8,10 +8,7 @@ use crate::{
     msg::{
         Admin, ExecuteMsg, InitialItem, InstantiateMsg, MigrateMsg, ModuleInstantiateInfo, QueryMsg,
     },
-    query::{
-        AdminNominationResponse, Cw20BalanceResponse, DumpStateResponse, GetItemResponse,
-        PauseInfoResponse,
-    },
+    query::{Cw20BalanceResponse, DumpStateResponse, GetItemResponse, PauseInfoResponse},
     state::Config,
     ContractError,
 };
@@ -893,23 +890,22 @@ fn test_admin_permissions() {
     let res = app.execute_contract(
         Addr::unchecked("rando"),
         core_addr.clone(),
-        &ExecuteMsg::NominateAdmin {
-            admin: Some("rando".to_string()),
+        &ExecuteMsg::UpdateAdmin {
+            admin: Some(Addr::unchecked("rando")),
         },
         &[],
     );
     assert!(res.is_err());
 
-    // Nominate admin can be called by core contract as no admin was
-    // specified so the admin defaulted to the core contract.
+    // Update Admin can't be called, even by the core module
     let res = app.execute_contract(
-        proposal_module.clone(),
+        core_addr.clone(),
         core_addr.clone(),
         &ExecuteMsg::ExecuteProposalHook {
             msgs: vec![WasmMsg::Execute {
                 contract_addr: core_addr.to_string(),
-                msg: to_binary(&ExecuteMsg::NominateAdmin {
-                    admin: Some("meow".to_string()),
+                msg: to_binary(&ExecuteMsg::UpdateAdmin {
+                    admin: Some(Addr::unchecked("meow")),
                 })
                 .unwrap(),
                 funds: vec![],
@@ -918,7 +914,7 @@ fn test_admin_permissions() {
         },
         &[],
     );
-    assert!(res.is_ok());
+    assert!(res.is_err());
 
     // Instantiate new DAO with an admin
     let (core_with_admin_addr, mut app) =
@@ -976,295 +972,23 @@ fn test_admin_permissions() {
     // DAO unpauses after 10 blocks
     app.update_block(|mut block| block.height += 11);
 
-    // Admin can nominate a new admin.
+    // Admin can update the admin.
     let res = app.execute_contract(
         Addr::unchecked("admin"),
         core_with_admin_addr.clone(),
-        &ExecuteMsg::NominateAdmin {
-            admin: Some("meow".to_string()),
+        &ExecuteMsg::UpdateAdmin {
+            admin: Some(Addr::unchecked("meow")),
         },
         &[],
     );
     assert!(res.is_ok());
 
-    let nomination: AdminNominationResponse = app
-        .wrap()
-        .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::AdminNomination {})
-        .unwrap();
-    assert_eq!(
-        nomination,
-        AdminNominationResponse {
-            nomination: Some(Addr::unchecked("meow"))
-        }
-    );
-
-    // Check that admin has not yet been updated
-    let res: Addr = app
-        .wrap()
-        .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::Admin {})
-        .unwrap();
-    assert_eq!(res, Addr::unchecked("admin"));
-
-    // Only the nominated address may accept the nomination.
-    let err: ContractError = app
-        .execute_contract(
-            Addr::unchecked("random"),
-            core_with_admin_addr.clone(),
-            &ExecuteMsg::AcceptAdminNomination {},
-            &[],
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-    assert_eq!(err, ContractError::Unauthorized {});
-
-    // Accept the nomination.
-    app.execute_contract(
-        Addr::unchecked("meow"),
-        core_with_admin_addr.clone(),
-        &ExecuteMsg::AcceptAdminNomination {},
-        &[],
-    )
-    .unwrap();
-
     // Check that admin has been updated
-    let res: Addr = app
-        .wrap()
-        .query_wasm_smart(core_with_admin_addr.clone(), &QueryMsg::Admin {})
-        .unwrap();
-    assert_eq!(res, Addr::unchecked("meow"));
-
-    // Check that the pending admin has been cleared.
-    let nomination: AdminNominationResponse = app
-        .wrap()
-        .query_wasm_smart(core_with_admin_addr, &QueryMsg::AdminNomination {})
-        .unwrap();
-    assert_eq!(nomination, AdminNominationResponse { nomination: None });
-}
-
-#[test]
-fn test_admin_nomination() {
-    let (core_addr, mut app) = do_standard_instantiate(true, Some("admin".to_string()));
-
-    // Check that there is no pending nominations.
-    let nomination: AdminNominationResponse = app
-        .wrap()
-        .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {})
-        .unwrap();
-    assert_eq!(nomination, AdminNominationResponse { nomination: None });
-
-    // Nominate a new admin.
-    app.execute_contract(
-        Addr::unchecked("admin"),
-        core_addr.clone(),
-        &ExecuteMsg::NominateAdmin {
-            admin: Some("ekez".to_string()),
-        },
-        &[],
-    )
-    .unwrap();
-
-    // Check that the nomination is in place.
-    let nomination: AdminNominationResponse = app
-        .wrap()
-        .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {})
-        .unwrap();
-    assert_eq!(
-        nomination,
-        AdminNominationResponse {
-            nomination: Some(Addr::unchecked("ekez"))
-        }
-    );
-
-    // Non-admin can not withdraw.
-    let err: ContractError = app
-        .execute_contract(
-            Addr::unchecked("ekez"),
-            core_addr.clone(),
-            &ExecuteMsg::WithdrawAdminNomination {},
-            &[],
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-    assert_eq!(err, ContractError::Unauthorized {});
-
-    // Admin can withdraw.
-    app.execute_contract(
-        Addr::unchecked("admin"),
-        core_addr.clone(),
-        &ExecuteMsg::WithdrawAdminNomination {},
-        &[],
-    )
-    .unwrap();
-
-    // Check that the nomination is withdrawn.
-    let nomination: AdminNominationResponse = app
-        .wrap()
-        .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {})
-        .unwrap();
-    assert_eq!(nomination, AdminNominationResponse { nomination: None });
-
-    // Can not withdraw if no nomination is pending.
-    let err: ContractError = app
-        .execute_contract(
-            Addr::unchecked("admin"),
-            core_addr.clone(),
-            &ExecuteMsg::WithdrawAdminNomination {},
-            &[],
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-    assert_eq!(err, ContractError::NoAdminNomination {});
-
-    // Can not claim nomination b/c it has been withdrawn.
-    let err: ContractError = app
-        .execute_contract(
-            Addr::unchecked("ekez"),
-            core_addr.clone(),
-            &ExecuteMsg::AcceptAdminNomination {},
-            &[],
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-    assert_eq!(err, ContractError::NoAdminNomination {});
-
-    // Nominate a new admin.
-    app.execute_contract(
-        Addr::unchecked("admin"),
-        core_addr.clone(),
-        &ExecuteMsg::NominateAdmin {
-            admin: Some("meow".to_string()),
-        },
-        &[],
-    )
-    .unwrap();
-
-    // A new nomination can not be created if there is already a
-    // pending nomination.
-    let err: ContractError = app
-        .execute_contract(
-            Addr::unchecked("admin"),
-            core_addr.clone(),
-            &ExecuteMsg::NominateAdmin {
-                admin: Some("arthur".to_string()),
-            },
-            &[],
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-    assert_eq!(err, ContractError::PendingNomination {});
-
-    // Only nominated admin may accept.
-    let err: ContractError = app
-        .execute_contract(
-            Addr::unchecked("ekez"),
-            core_addr.clone(),
-            &ExecuteMsg::AcceptAdminNomination {},
-            &[],
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-    assert_eq!(err, ContractError::Unauthorized {});
-
-    app.execute_contract(
-        Addr::unchecked("meow"),
-        core_addr.clone(),
-        &ExecuteMsg::AcceptAdminNomination {},
-        &[],
-    )
-    .unwrap();
-
-    // Check that meow is the new admin.
-    let admin: Addr = app
-        .wrap()
-        .query_wasm_smart(core_addr.clone(), &QueryMsg::Admin {})
-        .unwrap();
-    assert_eq!(admin, Addr::unchecked("meow".to_string()));
-
-    let start_height = app.block_info().height;
-    // Check that the new admin can do admin things and the old can not.
-    let err: ContractError = app
-        .execute_contract(
-            Addr::unchecked("admin"),
-            core_addr.clone(),
-            &ExecuteMsg::ExecuteAdminMsgs {
-                msgs: vec![WasmMsg::Execute {
-                    contract_addr: core_addr.to_string(),
-                    msg: to_binary(&ExecuteMsg::Pause {
-                        duration: Duration::Height(10),
-                    })
-                    .unwrap(),
-                    funds: vec![],
-                }
-                .into()],
-            },
-            &[],
-        )
-        .unwrap_err()
-        .downcast()
-        .unwrap();
-    assert_eq!(err, ContractError::Unauthorized {});
-
-    let res = app.execute_contract(
-        Addr::unchecked("meow"),
-        core_addr.clone(),
-        &ExecuteMsg::ExecuteAdminMsgs {
-            msgs: vec![WasmMsg::Execute {
-                contract_addr: core_addr.to_string(),
-                msg: to_binary(&ExecuteMsg::Pause {
-                    duration: Duration::Height(10),
-                })
-                .unwrap(),
-                funds: vec![],
-            }
-            .into()],
-        },
-        &[],
-    );
-    assert!(res.is_ok());
-
-    let paused: PauseInfoResponse = app
-        .wrap()
-        .query_wasm_smart(core_addr.clone(), &QueryMsg::PauseInfo {})
-        .unwrap();
-    assert_eq!(
-        paused,
-        PauseInfoResponse::Paused {
-            expiration: Expiration::AtHeight(start_height + 10)
-        }
-    );
-
-    // DAO unpauses after 10 blocks
-    app.update_block(|mut block| block.height += 11);
-
-    // Remove the admin.
-    app.execute_contract(
-        Addr::unchecked("meow"),
-        core_addr.clone(),
-        &ExecuteMsg::NominateAdmin { admin: None },
-        &[],
-    )
-    .unwrap();
-
-    // Check that this has not caused an admin to be nominated.
-    let nomination: AdminNominationResponse = app
-        .wrap()
-        .query_wasm_smart(core_addr.clone(), &QueryMsg::AdminNomination {})
-        .unwrap();
-    assert_eq!(nomination, AdminNominationResponse { nomination: None });
-
-    // Check that admin has been updated. As there was no admin
-    // nominated the admin should revert back to the contract address.
-    let res: Addr = app
+    let res: Option<Addr> = app
         .wrap()
-        .query_wasm_smart(core_addr.clone(), &QueryMsg::Admin {})
+        .query_wasm_smart(core_with_admin_addr, &QueryMsg::Admin {})
         .unwrap();
-    assert_eq!(res, core_addr);
+    assert_eq!(res, Some(Addr::unchecked("meow")));
 }
 
 #[test]