Repository: DA0-DA0/dao-contracts
Pull request #597 (2022-12-29 22:42:59)

Two-step ownership transfer for staking contracts
this implements a two-step ownership transfer for all of our staking contracts using the [`cw_ownable`](https://crates.io/crates/cw-ownable) crate.

for each contract in `staking/*`:

1. the `manager` is removed
2. `cw_ownable` integrated
3. migration from version one to two is implemented

it is my opinion that having a contract-level owner and CosmWasm-level admin that is a DAO has approximately the same semantics as having an owner and a manager. assuming the admin is a DAO, and the owner an operations multisig:

> the operations multisig has the ability to update unstaking durations, but does not have the ability to update the admin. the admin has the ability to remove the operations multisig and migrate the contract.

that sentence could be reused in a situation where there is an owner, manager, and admin and still be accurate.

0xekez: imo, having a manager and owner as these contracts are currently configured is EXACTLY the same semantically as having an owner and a contract level admin. the way these were used (RAW DAO rest in peace..) would fit the admin / owner model as well.
codecov-commenter: # [Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/597?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) Report
Base: **95.08**% // Head: **95.06**% // Decreases project coverage by **`-0.01%`** :warning:
> Coverage data is based on head [(`f7c7d0c`)](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/597?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) compared to base [(`1c15e58`)](https://codecov.io/gh/DA0-DA0/dao-contracts/commit/1c15e58f484ba9caf76e235733ecd9ad37bb630f?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).
> Patch coverage: 100.00% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #597      +/-   ##
==========================================
- Coverage   95.08%   95.06%   -0.02%     
==========================================
  Files          41       41              
  Lines        3782     3772      -10     
==========================================
- Hits         3596     3586      -10     
  Misses        186      186              
```


| [Impacted Files](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) | Coverage Δ | |
|---|---|---|
| [...king/cw20-stake-reward-distributor/src/contract.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/597/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL3N0YWtpbmcvY3cyMC1zdGFrZS1yZXdhcmQtZGlzdHJpYnV0b3Ivc3JjL2NvbnRyYWN0LnJz) | `99.09% <100.00%> (-0.08%)` | :arrow_down: |

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)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/597?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).

github-actions[bot]: <h1>Cosm-Orc Gas Usage</h1> 
 
 | Contract | Op Name | Gas Used | Old Gas Used | Gas Diff | File | 
 | --- | --- | --- | --- | --- | --- | 
| stake_cw20_reward_distributor | Store__Store | 2712460 | 2431608 | +11.5501% | ci/integration-tests/src/helpers/chain.rs:97 |
 
 <details><summary>Raw Report for f7c7d0c62d04103d208e6852f7ce9bcd6e2db7b9 </summary><br/> 

| Contract | Op Name | Gas Used | Gas Wanted | File | 
 | --- | --- | --- | --- | --- |
| dao_core | Execute__exc_admin_msgs_pause_dao | 194613 | 269202 | ci/integration-tests/src/tests/cw_core_test.rs:76 |
| dao_core | Instantiate__exc_admin_msgs_create_dao | 1272918 | 1886577 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_admin_msgs_create_dao_with_admin | 1273968 | 1888152 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__inst_admin_create_dao | 1273968 | 1888152 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Store__Store | 5663239 | 8472069 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_core | Instantiate__inst_dao_no_admin | 1272918 | 1886577 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Execute__exc_items_rm | 192820 | 266513 | ci/integration-tests/src/tests/cw_core_test.rs:171 |
| dao_core | Execute__exc_items_set | 194488 | 269013 | ci/integration-tests/src/tests/cw_core_test.rs:136 |
| dao_core | Instantiate__exc_items_create_dao | 1273968 | 1888152 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_stake_create_dao | 1272894 | 1886541 | ci/integration-tests/src/helpers/helper.rs:98 |
| cw721_base | Instantiate__instantiate_cw721_base | 164837 | 224540 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:22 |
| cw721_base | Store__Store | 3369402 | 5031314 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw721_base | Execute__mint_nft | 140542 | 188097 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:87 |
| cw721_base | Execute__stake_nft | 234591 | 329168 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:70 |
| dao_voting_cw721_staked | Execute__claim_nfts | 5710869 | 8543393 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:120 |
| dao_voting_cw721_staked | Instantiate__instantiate_dao_voting_cw721_staked | 178406 | 244893 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:49 |
| dao_voting_cw721_staked | Store__Store | 3327711 | 4968777 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw721_staked | Execute__unstake_nfts | 230319 | 322760 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:105 |
| multiple_contracts | Execute__batch_cw721_stake_max_claims | 4196439 | 6254235 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:251 |
| cw20_base | Store__Store | 4180849 | 6248484 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw20_base | Execute__exc_stake_stake_tokens | 234799 | 321348 | ci/integration-tests/src/tests/cw20_stake_test.rs:76 |
| cw20_stake | Store__Store | 3693999 | 5518209 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw4_group | Store__Store | 2783570 | 4152566 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw_admin_factory | Store__Store | 2003453 | 2982390 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw_token_swap | Store__Store | 2365061 | 3524802 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_approval_single | Store__Store | 4903233 | 7332060 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_approver | Store__Store | 3659835 | 5466963 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_multiple | Store__Store | 4233291 | 6327147 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_pre_propose_single | Store__Store | 4154927 | 6209601 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_proposal_multiple | Store__Store | 6360832 | 9518459 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_proposal_single | Store__Store | 6656556 | 9962045 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw20_staked | Store__Store | 3623617 | 5412636 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw4 | Store__Store | 2670847 | 3983481 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_native_staked | Store__Store | 2960295 | 4417649 | ci/integration-tests/src/helpers/chain.rs:97 |
| stake_cw20_external_rewards | Store__Store | 3097923 | 4624095 | ci/integration-tests/src/helpers/chain.rs:97 |
| stake_cw20_reward_distributor | Store__Store | 2712460 | 4045901 | ci/integration-tests/src/helpers/chain.rs:97 |
</details>
0xekez: let's wait for this before landing this PR as we'll have to update the initialization call: https://github.com/steak-enjoyers/cw-plus-plus/pull/7
larry0x: @0xekez i’ve merged it. Will make a new release a bit later
0xekez: pre-empting some review: i understand that there is some less-than-dry code in these changes, specifically:

1. `execute_update_ownership`
2. tests for ownership transfers
3. migration version checks

the code that is duplicated is so simple and brief, that imo it is more readable just written out each time instead of hiding it behind a level of indirection and an abstraction.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index dec2ecae..38ccab7a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -72,7 +72,7 @@ version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
  "winapi",
 ]
@@ -199,7 +199,7 @@ dependencies = [
  "cw-admin-factory",
  "cw-utils 0.16.0",
  "cw20 0.16.0",
- "cw20-stake",
+ "cw20-stake 2.0.0-beta",
  "dao-core",
  "dao-interface",
  "dao-pre-propose-single",
@@ -332,7 +332,7 @@ version = "0.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "673d31bd830c0772d78545de20d975129b6ab2f7db4e4e9313c3b8777d319194"
 dependencies = [
- "prost 0.11.3",
+ "prost 0.11.5",
  "prost-types",
  "tendermint-proto",
  "tonic",
@@ -504,12 +504,26 @@ dependencies = [
  "cw-storage-plus 0.16.0",
  "cw-utils 0.16.0",
  "cw2 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "dao-core",
  "dao-interface",
  "thiserror",
 ]
 
+[[package]]
+name = "cw-controllers"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f0bc6019b4d3d81e11f5c384bcce7173e2210bd654d75c6c9668e12cca05dfa"
+dependencies = [
+ "cosmwasm-std",
+ "cw-storage-plus 0.13.4",
+ "cw-utils 0.13.4",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "cw-controllers"
 version = "0.16.0"
@@ -575,7 +589,7 @@ dependencies = [
  "cosmwasm-std",
  "cw-multi-test",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "thiserror",
 ]
 
@@ -608,6 +622,29 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw-ownable"
+version = "0.3.0"
+source = "git+https://github.com/steak-enjoyers/cw-plus-plus?rev=50d4d9333305894457e5028072a0465f4635b15b#50d4d9333305894457e5028072a0465f4635b15b"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cw-ownable-derive",
+ "cw-storage-plus 1.0.1",
+ "cw-utils 1.0.1",
+ "thiserror",
+]
+
+[[package]]
+name = "cw-ownable-derive"
+version = "0.2.0"
+source = "git+https://github.com/steak-enjoyers/cw-plus-plus?rev=50d4d9333305894457e5028072a0465f4635b15b#50d4d9333305894457e5028072a0465f4635b15b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "cw-paginate"
 version = "0.1.0"
@@ -698,7 +735,7 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "thiserror",
 ]
 
@@ -737,7 +774,7 @@ checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7"
 dependencies = [
  "cosmwasm-schema",
  "cosmwasm-std",
- "cw2 1.0.0",
+ "cw2 1.0.1",
  "schemars",
  "semver",
  "serde",
@@ -771,13 +808,13 @@ dependencies = [
 
 [[package]]
 name = "cw2"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03bdf3747540b47bc1bdaf50ba3aa5e4276ab0c2ce73e8b367ebe260cc37ff9c"
+checksum = "8fb70cee2cf0b4a8ff7253e6bc6647107905e8eb37208f87d54f67810faa62f8"
 dependencies = [
  "cosmwasm-schema",
  "cosmwasm-std",
- "cw-storage-plus 0.16.0",
+ "cw-storage-plus 1.0.1",
  "schemars",
  "serde",
 ]
@@ -807,6 +844,22 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "cw20-base"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0306e606581f4fb45e82bcbb7f0333179ed53dd949c6523f01a99b4bfc1475a0"
+dependencies = [
+ "cosmwasm-std",
+ "cw-storage-plus 0.13.4",
+ "cw-utils 0.13.4",
+ "cw2 0.13.4",
+ "cw20 0.13.4",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "cw20-base"
 version = "0.16.0"
@@ -825,6 +878,24 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw20-stake"
+version = "0.2.6"
+source = "git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0#e531c760a5d057329afd98d62567aaa4dca2c96f"
+dependencies = [
+ "cosmwasm-std",
+ "cosmwasm-storage",
+ "cw-controllers 0.13.4",
+ "cw-storage-plus 0.13.4",
+ "cw-utils 0.13.4",
+ "cw2 0.13.4",
+ "cw20 0.13.4",
+ "cw20-base 0.13.4",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "cw20-stake"
 version = "2.0.0-beta"
@@ -833,14 +904,57 @@ dependencies = [
  "cosmwasm-schema",
  "cosmwasm-std",
  "cosmwasm-storage",
- "cw-controllers",
+ "cw-controllers 0.16.0",
  "cw-multi-test",
+ "cw-ownable",
  "cw-paginate 2.0.0-beta",
  "cw-storage-plus 0.16.0",
+ "cw-utils 0.13.4",
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
+ "cw20-stake 0.2.6",
+ "thiserror",
+]
+
+[[package]]
+name = "cw20-stake-external-rewards"
+version = "2.0.0-beta"
+dependencies = [
+ "anyhow",
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cosmwasm-storage",
+ "cw-controllers 0.16.0",
+ "cw-multi-test",
+ "cw-ownable",
+ "cw-storage-plus 0.16.0",
+ "cw-utils 0.16.0",
+ "cw2 0.16.0",
+ "cw20 0.13.4",
+ "cw20 0.16.0",
+ "cw20-base 0.16.0",
+ "cw20-stake 2.0.0-beta",
+ "stake-cw20-external-rewards",
+ "thiserror",
+]
+
+[[package]]
+name = "cw20-stake-reward-distributor"
+version = "2.0.0-beta"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cw-multi-test",
+ "cw-ownable",
+ "cw-storage-plus 0.16.0",
+ "cw-utils 0.16.0",
+ "cw2 0.16.0",
+ "cw20 0.16.0",
+ "cw20-base 0.16.0",
+ "cw20-stake 2.0.0-beta",
+ "stake-cw20-reward-distributor",
  "thiserror",
 ]
 
@@ -890,7 +1004,7 @@ checksum = "dba1d15bff40b97bde05f36a0d44ac3369a085a4bda6c4673e33a9359513fdd2"
 dependencies = [
  "cosmwasm-schema",
  "cosmwasm-std",
- "cw-controllers",
+ "cw-controllers 0.16.0",
  "cw-storage-plus 0.16.0",
  "cw-utils 0.16.0",
  "cw2 0.16.0",
@@ -959,7 +1073,6 @@ version = "2.0.0-beta"
 dependencies = [
  "cosmwasm-schema",
  "cosmwasm-std",
- "cosmwasm-storage",
  "cw-core",
  "cw-multi-test",
  "cw-paginate 2.0.0-beta",
@@ -967,7 +1080,7 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "cw721 0.16.0",
  "cw721-base",
  "dao-interface",
@@ -1015,7 +1128,7 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "cw4-group",
  "dao-core",
  "dao-interface",
@@ -1041,7 +1154,7 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "cw4-group",
  "dao-core",
  "dao-interface",
@@ -1085,7 +1198,7 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "cw4-group",
  "dao-core",
  "dao-interface",
@@ -1110,7 +1223,7 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "cw4-group",
  "dao-core",
  "dao-interface",
@@ -1135,7 +1248,7 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "dao-core",
  "dao-interface",
  "dao-proposal-hooks",
@@ -1170,8 +1283,8 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
- "cw20-stake",
+ "cw20-base 0.16.0",
+ "cw20-stake 2.0.0-beta",
  "cw3 0.16.0",
  "cw4",
  "cw4-group",
@@ -1212,8 +1325,8 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
- "cw20-stake",
+ "cw20-base 0.16.0",
+ "cw20-stake 2.0.0-beta",
  "cw3 0.16.0",
  "cw4",
  "cw4-group",
@@ -1264,8 +1377,8 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
- "cw20-stake",
+ "cw20-base 0.16.0",
+ "cw20-stake 2.0.0-beta",
  "cw4",
  "cw4-group",
  "cw721-base",
@@ -1321,7 +1434,7 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
+ "cw20-base 0.16.0",
  "dao-interface",
  "dao-macros",
  "thiserror",
@@ -1339,8 +1452,8 @@ dependencies = [
  "cw-utils 0.16.0",
  "cw2 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
- "cw20-stake",
+ "cw20-base 0.16.0",
+ "cw20-stake 2.0.0-beta",
  "dao-interface",
  "dao-macros",
  "thiserror",
@@ -1371,7 +1484,7 @@ dependencies = [
  "anyhow",
  "cosmwasm-schema",
  "cosmwasm-std",
- "cw-controllers",
+ "cw-controllers 0.16.0",
  "cw-multi-test",
  "cw-paginate 2.0.0-beta",
  "cw-storage-plus 0.16.0",
@@ -1394,7 +1507,7 @@ dependencies = [
  "cosmwasm-schema",
  "cosmwasm-std",
  "cosmwasm-storage",
- "cw-controllers",
+ "cw-controllers 0.16.0",
  "cw-multi-test",
  "cw-paginate 2.0.0-beta",
  "cw-storage-plus 0.16.0",
@@ -1790,6 +1903,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -1972,8 +2094,8 @@ dependencies = [
  "cosmwasm-std",
  "cw-utils 0.16.0",
  "cw20 0.16.0",
- "cw20-base",
- "cw20-stake",
+ "cw20-base 0.16.0",
+ "cw20-stake 2.0.0-beta",
  "cw721 0.16.0",
  "cw721-base",
  "dao-core",
@@ -2056,9 +2178,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.138"
+version = "0.2.139"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
 
 [[package]]
 name = "linked-hash-map"
@@ -2143,19 +2265,19 @@ dependencies = [
 
 [[package]]
 name = "num_cpus"
-version = "1.14.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.2.6",
  "libc",
 ]
 
 [[package]]
 name = "once_cell"
-version = "1.16.0"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
 
 [[package]]
 name = "opaque-debug"
@@ -2235,9 +2357,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
 
 [[package]]
 name = "pest"
-version = "2.5.1"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0"
+checksum = "0f6e86fb9e7026527a0d46bc308b841d73170ef8f443e1807f6ef88526a816d4"
 dependencies = [
  "thiserror",
  "ucd-trie",
@@ -2245,9 +2367,9 @@ dependencies = [
 
 [[package]]
 name = "pest_derive"
-version = "2.5.1"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdc078600d06ff90d4ed238f0119d84ab5d43dbaad278b0e33a8820293b32344"
+checksum = "96504449aa860c8dcde14f9fba5c58dc6658688ca1fe363589d6327b8662c603"
 dependencies = [
  "pest",
  "pest_generator",
@@ -2255,9 +2377,9 @@ dependencies = [
 
 [[package]]
 name = "pest_generator"
-version = "2.5.1"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28a1af60b1c4148bb269006a750cff8e2ea36aff34d2d96cf7be0b14d1bed23c"
+checksum = "798e0220d1111ae63d66cb66a5dcb3fc2d986d520b98e49e1852bfdb11d7c5e7"
 dependencies = [
  "pest",
  "pest_meta",
@@ -2268,9 +2390,9 @@ dependencies = [
 
 [[package]]
 name = "pest_meta"
-version = "2.5.1"
+version = "2.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fec8605d59fc2ae0c6c1aefc0c7c7a9769732017c0ce07f7a9cfffa7b4404f20"
+checksum = "984298b75898e30a843e278a9f2452c31e349a073a0ce6fd950a12a74464e065"
 dependencies = [
  "once_cell",
  "pest",
@@ -2357,12 +2479,12 @@ dependencies = [
 
 [[package]]
 name = "prost"
-version = "0.11.3"
+version = "0.11.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0b18e655c21ff5ac2084a5ad0611e827b3f92badf79f4910b5a5c58f4d87ff0"
+checksum = "c01db6702aa05baa3f57dec92b8eeeeb4cb19e894e73996b32a4093289e54592"
 dependencies = [
  "bytes",
- "prost-derive 0.11.2",
+ "prost-derive 0.11.5",
 ]
 
 [[package]]
@@ -2380,9 +2502,9 @@ dependencies = [
 
 [[package]]
 name = "prost-derive"
-version = "0.11.2"
+version = "0.11.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "164ae68b6587001ca506d3bf7f1000bfa248d0e1217b618108fba4ec1d0cc306"
+checksum = "c8842bad1a5419bca14eac663ba798f6bc19c413c2fdceb5f3ba3b0932d96720"
 dependencies = [
  "anyhow",
  "itertools",
@@ -2393,12 +2515,12 @@ dependencies = [
 
 [[package]]
 name = "prost-types"
-version = "0.11.2"
+version = "0.11.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "747761bc3dc48f9a34553bf65605cf6cb6288ba219f3450b4275dbd81539551a"
+checksum = "017f79637768cde62820bc2d4fe0e45daaa027755c323ad077767c6c5f173091"
 dependencies = [
  "bytes",
- "prost 0.11.3",
+ "prost 0.11.5",
 ]
 
 [[package]]
@@ -2665,9 +2787,9 @@ checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
 
 [[package]]
 name = "serde"
-version = "1.0.151"
+version = "1.0.152"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
 dependencies = [
  "serde_derive",
 ]
@@ -2692,9 +2814,9 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.151"
+version = "1.0.152"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8"
+checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2839,37 +2961,38 @@ dependencies = [
 
 [[package]]
 name = "stake-cw20-external-rewards"
-version = "2.0.0-beta"
+version = "0.2.6"
+source = "git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0#e531c760a5d057329afd98d62567aaa4dca2c96f"
 dependencies = [
- "anyhow",
- "cosmwasm-schema",
  "cosmwasm-std",
  "cosmwasm-storage",
- "cw-controllers",
- "cw-multi-test",
- "cw-storage-plus 0.16.0",
- "cw-utils 0.16.0",
- "cw2 0.16.0",
- "cw20 0.16.0",
- "cw20-base",
- "cw20-stake",
+ "cw-controllers 0.13.4",
+ "cw-storage-plus 0.13.4",
+ "cw-utils 0.13.4",
+ "cw2 0.13.4",
+ "cw20 0.13.4",
+ "cw20-base 0.13.4",
+ "cw20-stake 0.2.6",
+ "schemars",
+ "serde",
  "thiserror",
 ]
 
 [[package]]
 name = "stake-cw20-reward-distributor"
-version = "2.0.0-beta"
+version = "0.1.0"
+source = "git+https://github.com/DA0-DA0/dao-contracts.git?tag=v1.0.0#e531c760a5d057329afd98d62567aaa4dca2c96f"
 dependencies = [
- "cosmwasm-schema",
  "cosmwasm-std",
  "cosmwasm-storage",
- "cw-multi-test",
- "cw-storage-plus 0.16.0",
- "cw-utils 0.16.0",
- "cw2 0.16.0",
- "cw20 0.16.0",
- "cw20-base",
- "cw20-stake",
+ "cw-storage-plus 0.13.4",
+ "cw-utils 0.13.4",
+ "cw2 0.13.4",
+ "cw20 0.13.4",
+ "cw20-base 0.13.4",
+ "cw20-stake 0.2.6",
+ "schemars",
+ "serde",
  "thiserror",
 ]
 
@@ -2938,7 +3061,7 @@ dependencies = [
  "k256",
  "num-traits",
  "once_cell",
- "prost 0.11.3",
+ "prost 0.11.5",
  "prost-types",
  "ripemd160",
  "serde",
@@ -2978,7 +3101,7 @@ dependencies = [
  "flex-error",
  "num-derive",
  "num-traits",
- "prost 0.11.3",
+ "prost 0.11.5",
  "prost-types",
  "serde",
  "serde_bytes",
@@ -3215,8 +3338,8 @@ dependencies = [
  "hyper-timeout",
  "percent-encoding",
  "pin-project",
- "prost 0.11.3",
- "prost-derive 0.11.2",
+ "prost 0.11.5",
+ "prost-derive 0.11.5",
  "tokio",
  "tokio-stream",
  "tokio-util",
diff --git a/Cargo.toml b/Cargo.toml
index bebe20e2..7f3d841b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -40,7 +40,6 @@ cw2 = "0.16"
 cw20 = "0.16"
 cw721 = "0.16"
 cw20-base = "0.16"
-cw-core-v1 = { package = "cw-core", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
 cw-multi-test = "0.16"
 cw721-base = "0.16"
 cw-controllers = "0.16"
@@ -49,15 +48,18 @@ cw4-group = "0.16"
 rand = "0.8"
 cw4 = "0.16"
 cw3 = "0.16"
-indexmap = "1.9"
 serde = "1.0"
 proc-macro2 = "1.0"
 quote = "1.0"
 syn = { version = "1.0", features = ["derive"] }
 
+# One commit ahead of version 0.3.0. Allows initialization with an
+# optional owner.
+cw-ownable = { git = "https://github.com/steak-enjoyers/cw-plus-plus", rev = "50d4d9333305894457e5028072a0465f4635b15b" }
+
+
 cw-admin-factory = { path = "./contracts/external/cw-admin-factory" }
 dao-core = { path = "./contracts/dao-core" }
-
 dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single" }
 dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple" }
 dao-pre-propose-single = { path = "./contracts/pre-propose/dao-pre-propose-single" }
@@ -82,6 +84,12 @@ dao-vote-hooks = { path = "./packages/dao-vote-hooks" }
 cw-paginate = { path = "./packages/cw-paginate" }
 dao-interface = { path = "./packages/dao-interface" }
 dao-voting = { path = "./packages/dao-voting" }
+
+# v1 dependencies. used for state migrations.
+cw-core-v1 = { package = "cw-core", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
 cw-proposal-single-v1 = { package = "cw-proposal-single", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
 voting-v1 = { package = "voting", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
 cw-utils-v1 = {package = "cw-utils", version = "0.13"}
+cw20-stake-reward-distributor-v1 = { package = "stake-cw20-reward-distributor", version = "0.1.0", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
+cw20-stake-external-rewards-v1 = { package = "stake-cw20-external-rewards", version = "0.2.6", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
+cw20-stake-v1 = { package = "cw20-stake", version = "0.2.6", git = "https://github.com/DA0-DA0/dao-contracts.git", tag = "v1.0.0" }
\ No newline at end of file
diff --git a/contracts/dao-core/Cargo.toml b/contracts/dao-core/Cargo.toml
index 6babe308..d751d4bc 100644
--- a/contracts/dao-core/Cargo.toml
+++ b/contracts/dao-core/Cargo.toml
@@ -17,7 +17,6 @@ library = []
 
 [dependencies]
 cosmwasm-std = { workspace = true, features = ["ibc3"] }
-cosmwasm-storage = { workspace = true }
 cosmwasm-schema = { workspace = true }
 cw-storage-plus = { workspace = true }
 cw2 = { workspace = true }
diff --git a/contracts/staking/cw20-stake-external-rewards/Cargo.toml b/contracts/staking/cw20-stake-external-rewards/Cargo.toml
index c6a02d5b..1f84f169 100644
--- a/contracts/staking/cw20-stake-external-rewards/Cargo.toml
+++ b/contracts/staking/cw20-stake-external-rewards/Cargo.toml
@@ -1,11 +1,8 @@
 [package]
-name = "stake-cw20-external-rewards"
+name = "cw20-stake-external-rewards"
 version = "2.0.0-beta"
-authors = ["Ben2x4 <Ben2x4@tutanota.com>"]
+authors = ["Ben2x4 <Ben2x4@tutanota.com>", "ekez <ekez@withoutdoing.com>"]
 edition = "2018"
-license = "Apache-2.0"
-repository = "https://github.com/DA0-DA0/cw-dao/contracts/cw20-stakeable"
-description = "CW20 token that can be staked and staked balance can be queried at any height"
 
 [lib]
 crate-type = ["cdylib", "rlib"]
@@ -25,9 +22,12 @@ cw20 = { workspace = true }
 cw-utils = { workspace = true }
 cw20-base = {  workspace = true, features = ["library"] }
 cw2 = { workspace = true }
-
 thiserror = { workspace = true }
 cw20-stake = { workspace = true, features = ["library"]}
+cw-ownable = { workspace = true }
+
+cw20-stake-external-rewards-v1 = { workspace = true }
+cw20-013 = { package = "cw20", version = "0.13" }
 
 [dev-dependencies]
 cw-multi-test = { workspace = true }
diff --git a/contracts/staking/cw20-stake-external-rewards/examples/schema.rs b/contracts/staking/cw20-stake-external-rewards/examples/schema.rs
index 7238ef7a..ca1d1c88 100644
--- a/contracts/staking/cw20-stake-external-rewards/examples/schema.rs
+++ b/contracts/staking/cw20-stake-external-rewards/examples/schema.rs
@@ -1,5 +1,5 @@
 use cosmwasm_schema::write_api;
-use stake_cw20_external_rewards::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
+use cw20_stake_external_rewards::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
 
 fn main() {
     write_api! {
diff --git a/contracts/staking/cw20-stake-external-rewards/schema/stake-cw20-external-rewards.json b/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json
similarity index 61%
rename from contracts/staking/cw20-stake-external-rewards/schema/stake-cw20-external-rewards.json
rename to contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json
index 1cdc1f4b..3a619294 100644
--- a/contracts/staking/cw20-stake-external-rewards/schema/stake-cw20-external-rewards.json
+++ b/contracts/staking/cw20-stake-external-rewards/schema/cw20-stake-external-rewards.json
@@ -1,5 +1,5 @@
 {
-  "contract_name": "stake-cw20-external-rewards",
+  "contract_name": "cw20-stake-external-rewards",
   "contract_version": "2.0.0-beta",
   "idl_version": "1.0.0",
   "instantiate": {
@@ -12,12 +12,6 @@
       "staking_contract"
     ],
     "properties": {
-      "manager": {
-        "type": [
-          "string",
-          "null"
-        ]
-      },
       "owner": {
         "type": [
           "string",
@@ -150,49 +144,71 @@
         "additionalProperties": false
       },
       {
+        "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.",
         "type": "object",
         "required": [
-          "update_owner"
+          "update_ownership"
         ],
         "properties": {
-          "update_owner": {
-            "type": "object",
-            "properties": {
-              "new_owner": {
-                "type": [
-                  "string",
-                  "null"
-                ]
-              }
-            },
-            "additionalProperties": false
+          "update_ownership": {
+            "$ref": "#/definitions/Action"
           }
         },
         "additionalProperties": false
-      },
-      {
-        "type": "object",
-        "required": [
-          "update_manager"
-        ],
-        "properties": {
-          "update_manager": {
+      }
+    ],
+    "definitions": {
+      "Action": {
+        "description": "Actions that can be taken to alter the contract's ownership",
+        "oneOf": [
+          {
+            "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.",
             "type": "object",
+            "required": [
+              "transfer_ownership"
+            ],
             "properties": {
-              "new_manager": {
-                "type": [
-                  "string",
-                  "null"
-                ]
+              "transfer_ownership": {
+                "type": "object",
+                "required": [
+                  "new_owner"
+                ],
+                "properties": {
+                  "expiry": {
+                    "anyOf": [
+                      {
+                        "$ref": "#/definitions/Expiration"
+                      },
+                      {
+                        "type": "null"
+                      }
+                    ]
+                  },
+                  "new_owner": {
+                    "type": "string"
+                  }
+                },
+                "additionalProperties": false
               }
             },
             "additionalProperties": false
+          },
+          {
+            "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.",
+            "type": "string",
+            "enum": [
+              "accept_ownership"
+            ]
+          },
+          {
+            "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.",
+            "type": "string",
+            "enum": [
+              "renounce_ownership"
+            ]
           }
-        },
-        "additionalProperties": false
-      }
-    ],
-    "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"
@@ -222,6 +238,53 @@
         },
         "additionalProperties": false
       },
+      "Expiration": {
+        "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+        "oneOf": [
+          {
+            "description": "AtHeight will expire when `env.block.height` >= height",
+            "type": "object",
+            "required": [
+              "at_height"
+            ],
+            "properties": {
+              "at_height": {
+                "type": "integer",
+                "format": "uint64",
+                "minimum": 0.0
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "AtTime will expire when `env.block.time` >= time",
+            "type": "object",
+            "required": [
+              "at_time"
+            ],
+            "properties": {
+              "at_time": {
+                "$ref": "#/definitions/Timestamp"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "Never will never expire. Used to express the empty variant",
+            "type": "object",
+            "required": [
+              "never"
+            ],
+            "properties": {
+              "never": {
+                "type": "object",
+                "additionalProperties": false
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
       "StakeChangedHookMsg": {
         "oneOf": [
           {
@@ -276,9 +339,21 @@
           }
         ]
       },
+      "Timestamp": {
+        "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+        "allOf": [
+          {
+            "$ref": "#/definitions/Uint64"
+          }
+        ]
+      },
       "Uint128": {
         "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
         "type": "string"
+      },
+      "Uint64": {
+        "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+        "type": "string"
       }
     }
   },
@@ -319,14 +394,41 @@
           }
         },
         "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "ownership"
+        ],
+        "properties": {
+          "ownership": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
       }
     ]
   },
   "migrate": {
     "$schema": "http://json-schema.org/draft-07/schema#",
     "title": "MigrateMsg",
-    "type": "object",
-    "additionalProperties": false
+    "oneOf": [
+      {
+        "description": "Migrates from version 0.2.6 to 2.0.0. The significant changes being the addition of a two-step ownership transfer using `cw_ownable` and the removal of the manager. Migrating will automatically remove the current manager.",
+        "type": "object",
+        "required": [
+          "from_v1"
+        ],
+        "properties": {
+          "from_v1": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
   },
   "sudo": null,
   "responses": {
@@ -425,26 +527,6 @@
             "staking_contract"
           ],
           "properties": {
-            "manager": {
-              "anyOf": [
-                {
-                  "$ref": "#/definitions/Addr"
-                },
-                {
-                  "type": "null"
-                }
-              ]
-            },
-            "owner": {
-              "anyOf": [
-                {
-                  "$ref": "#/definitions/Addr"
-                },
-                {
-                  "type": "null"
-                }
-              ]
-            },
             "reward_token": {
               "$ref": "#/definitions/Denom"
             },
@@ -511,6 +593,113 @@
           "type": "string"
         }
       }
+    },
+    "ownership": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Ownership_for_Addr",
+      "description": "The contract's ownership info",
+      "type": "object",
+      "properties": {
+        "owner": {
+          "description": "The contract's current owner. `None` if the ownership has been renounced.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Addr"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pending_expiry": {
+          "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Expiration"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pending_owner": {
+          "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Addr"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "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"
+        },
+        "Expiration": {
+          "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+          "oneOf": [
+            {
+              "description": "AtHeight will expire when `env.block.height` >= height",
+              "type": "object",
+              "required": [
+                "at_height"
+              ],
+              "properties": {
+                "at_height": {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "description": "AtTime will expire when `env.block.time` >= time",
+              "type": "object",
+              "required": [
+                "at_time"
+              ],
+              "properties": {
+                "at_time": {
+                  "$ref": "#/definitions/Timestamp"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "description": "Never will never expire. Used to express the empty variant",
+              "type": "object",
+              "required": [
+                "never"
+              ],
+              "properties": {
+                "never": {
+                  "type": "object",
+                  "additionalProperties": false
+                }
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "Timestamp": {
+          "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Uint64"
+            }
+          ]
+        },
+        "Uint64": {
+          "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+          "type": "string"
+        }
+      }
     }
   }
 }
diff --git a/contracts/staking/cw20-stake-external-rewards/src/contract.rs b/contracts/staking/cw20-stake-external-rewards/src/contract.rs
index 8a6eb2ac..a2a50412 100644
--- a/contracts/staking/cw20-stake-external-rewards/src/contract.rs
+++ b/contracts/staking/cw20-stake-external-rewards/src/contract.rs
@@ -8,7 +8,7 @@ use crate::state::{
 };
 use crate::ContractError;
 use crate::ContractError::{
-    InvalidCw20, InvalidFunds, NoRewardsClaimable, RewardPeriodNotFinished, Unauthorized,
+    InvalidCw20, InvalidFunds, NoRewardsClaimable, RewardPeriodNotFinished,
 };
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
@@ -17,7 +17,7 @@ use cosmwasm_std::{
     from_binary, to_binary, Addr, BankMsg, Binary, Coin, CosmosMsg, Deps, DepsMut, Empty, Env,
     MessageInfo, Response, StdError, StdResult, Uint128, Uint256, WasmMsg,
 };
-use cw2::set_contract_version;
+use cw2::{get_contract_version, set_contract_version, ContractVersion};
 use cw20::{Cw20ReceiveMsg, Denom};
 use cw20_stake::hooks::StakeChangedHookMsg;
 
@@ -37,21 +37,13 @@ pub fn instantiate(
 ) -> Result<Response<Empty>, ContractError> {
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
 
-    let owner = msg.owner.map(|a| deps.api.addr_validate(&a)).transpose()?;
-    let manager = msg
-        .manager
-        .map(|a| deps.api.addr_validate(&a))
-        .transpose()?;
+    cw_ownable::initialize_owner(deps.storage, deps.api, msg.owner.as_deref())?;
 
     let reward_token = match msg.reward_token {
         Denom::Native(denom) => Denom::Native(denom),
         Cw20(addr) => Cw20(deps.api.addr_validate(addr.as_ref())?),
     };
 
-    if msg.reward_duration == 0 {
-        return Err(ContractError::ZeroRewardDuration {});
-    }
-
     // Verify contract provided is a staking contract
     let _: cw20_stake::msg::TotalStakedAtHeightResponse = deps.querier.query_wasm_smart(
         &msg.staking_contract,
@@ -59,14 +51,15 @@ pub fn instantiate(
     )?;
 
     let config = Config {
-        owner,
-        manager,
         staking_contract: deps.api.addr_validate(&msg.staking_contract)?,
         reward_token,
     };
     CONFIG.save(deps.storage, &config)?;
 
-    // Non-zero rewards duration checked above.
+    if msg.reward_duration == 0 {
+        return Err(ContractError::ZeroRewardDuration {});
+    }
+
     let reward_config = RewardConfig {
         period_finish: 0,
         reward_rate: Uint128::zero(),
@@ -75,20 +68,7 @@ pub fn instantiate(
     REWARD_CONFIG.save(deps.storage, &reward_config)?;
 
     Ok(Response::new()
-        .add_attribute(
-            "owner",
-            config
-                .owner
-                .map(|a| a.into_string())
-                .unwrap_or_else(|| "None".to_string()),
-        )
-        .add_attribute(
-            "manager",
-            config
-                .manager
-                .map(|a| a.into_string())
-                .unwrap_or_else(|| "None".to_string()),
-        )
+        .add_attribute("owner", msg.owner.unwrap_or_else(|| "None".to_string()))
         .add_attribute("staking_contract", config.staking_contract)
         .add_attribute(
             "reward_token",
@@ -103,10 +83,39 @@ pub fn instantiate(
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
-pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
-    // Set contract to version to latest
+pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
+    use cw20_stake_external_rewards_v1 as v1;
+
+    let ContractVersion { version, .. } = get_contract_version(deps.storage)?;
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
-    Ok(Response::default())
+
+    match msg {
+        MigrateMsg::FromV1 {} => {
+            if version == CONTRACT_VERSION {
+                // You can not possibly be migrating from v1 to v2 and
+                // also not changing your contract version.
+                return Err(ContractError::AlreadyMigrated {});
+            }
+            // From v1 -> v2 we moved `owner` out of config and into
+            // the `cw_ownable` package.
+            let config = v1::state::CONFIG.load(deps.storage)?;
+            cw_ownable::initialize_owner(
+                deps.storage,
+                deps.api,
+                config.owner.map(|a| a.into_string()).as_deref(),
+            )?;
+            let config = Config {
+                staking_contract: config.staking_contract,
+                reward_token: match config.reward_token {
+                    cw20_013::Denom::Native(n) => Denom::Native(n),
+                    cw20_013::Denom::Cw20(a) => Denom::Cw20(a),
+                },
+            };
+            CONFIG.save(deps.storage, &config)?;
+
+            Ok(Response::default())
+        }
+    }
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
@@ -124,10 +133,7 @@ pub fn execute(
         ExecuteMsg::UpdateRewardDuration { new_duration } => {
             execute_update_reward_duration(deps, env, info, new_duration)
         }
-        ExecuteMsg::UpdateOwner { new_owner } => execute_update_owner(deps, env, info, new_owner),
-        ExecuteMsg::UpdateManager { new_manager } => {
-            execute_update_manager(deps, env, info, new_manager)
-        }
+        ExecuteMsg::UpdateOwnership(action) => execute_update_owner(deps, info, env, action),
     }
 }
 
@@ -170,10 +176,7 @@ pub fn execute_fund(
     sender: Addr,
     amount: Uint128,
 ) -> Result<Response<Empty>, ContractError> {
-    let config = CONFIG.load(deps.storage)?;
-    if config.owner != Some(sender.clone()) && config.manager != Some(sender.clone()) {
-        return Err(Unauthorized {});
-    };
+    cw_ownable::assert_owner(deps.storage, &sender)?;
 
     update_rewards(&mut deps, &env, &sender)?;
     let reward_config = REWARD_CONFIG.load(deps.storage)?;
@@ -212,7 +215,7 @@ pub fn execute_stake_changed(
 ) -> Result<Response<Empty>, ContractError> {
     let config = CONFIG.load(deps.storage)?;
     if info.sender != config.staking_contract {
-        return Err(ContractError::Unauthorized {});
+        return Err(ContractError::InvalidHookSender {});
     };
     match msg {
         StakeChangedHookMsg::Stake { addr, .. } => execute_stake(deps, env, addr),
@@ -259,6 +262,16 @@ pub fn execute_claim(
         .add_attribute("amount", rewards))
 }
 
+pub fn execute_update_owner(
+    deps: DepsMut,
+    info: MessageInfo,
+    env: Env,
+    action: cw_ownable::Action,
+) -> Result<Response, ContractError> {
+    let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?;
+    Ok(Response::default().add_attributes(ownership.into_attributes()))
+}
+
 pub fn get_transfer_msg(recipient: Addr, amount: Uint128, denom: Denom) -> StdResult<CosmosMsg> {
     match denom {
         Denom::Native(denom) => Ok(BankMsg::Send {
@@ -374,10 +387,7 @@ pub fn execute_update_reward_duration(
     info: MessageInfo,
     new_duration: u64,
 ) -> Result<Response<Empty>, ContractError> {
-    let config = CONFIG.load(deps.storage)?;
-    if Some(info.sender.clone()) != config.owner && Some(info.sender) != config.manager {
-        return Err(ContractError::Unauthorized {});
-    };
+    cw_ownable::assert_owner(deps.storage, &info.sender)?;
 
     let mut reward_config = REWARD_CONFIG.load(deps.storage)?;
     if reward_config.period_finish > env.block.height {
@@ -398,71 +408,6 @@ pub fn execute_update_reward_duration(
         .add_attribute("old_duration", old_duration.to_string()))
 }
 
-pub fn execute_update_owner(
-    deps: DepsMut,
-    _env: Env,
-    info: MessageInfo,
-    new_owner: Option<String>,
-) -> Result<Response<Empty>, ContractError> {
-    let new_owner = new_owner.map(|a| deps.api.addr_validate(&a)).transpose()?;
-
-    let mut config = CONFIG.load(deps.storage)?;
-    if Some(info.sender) != config.owner {
-        return Err(ContractError::Unauthorized {});
-    };
-    let old_owner = config.owner.clone();
-    config.owner = new_owner.clone();
-    CONFIG.save(deps.storage, &config)?;
-
-    Ok(Response::new()
-        .add_attribute("action", "update_owner")
-        .add_attribute(
-            "new_owner",
-            new_owner
-                .map(|a| a.into_string())
-                .unwrap_or_else(|| "None".to_string()),
-        )
-        .add_attribute(
-            "old_owner",
-            old_owner
-                .map(|a| a.into_string())
-                .unwrap_or_else(|| "None".to_string()),
-        ))
-}
-pub fn execute_update_manager(
-    deps: DepsMut,
-    _env: Env,
-    info: MessageInfo,
-    new_manager: Option<String>,
-) -> Result<Response<Empty>, ContractError> {
-    let new_manager = new_manager
-        .map(|a| deps.api.addr_validate(&a))
-        .transpose()?;
-
-    let mut config = CONFIG.load(deps.storage)?;
-    if Some(info.sender.clone()) != config.owner && Some(info.sender) != config.manager {
-        return Err(ContractError::Unauthorized {});
-    };
-    let old_manager = config.manager.clone();
-    config.manager = new_manager.clone();
-    CONFIG.save(deps.storage, &config)?;
-
-    Ok(Response::new()
-        .add_attribute("action", "update_manager")
-        .add_attribute(
-            "new_manager",
-            new_manager
-                .map(|a| a.into_string())
-                .unwrap_or_else(|| "None".to_string()),
-        )
-        .add_attribute(
-            "old_manager",
-            old_manager
-                .map(|a| a.into_string())
-                .unwrap_or_else(|| "None".to_string()),
-        ))
-}
-
 fn scale_factor() -> Uint256 {
     Uint256::from(10u8).pow(39)
 }
@@ -473,6 +418,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::GetPendingRewards { address } => {
             Ok(to_binary(&query_pending_rewards(deps, env, address)?)?)
         }
+        QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?),
     }
 }
 
@@ -514,26 +460,20 @@ pub fn query_pending_rewards(
 mod tests {
     use std::borrow::BorrowMut;
 
-    use crate::{
-        contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION},
-        msg::MigrateMsg,
-        ContractError,
-    };
+    use crate::{msg::MigrateMsg, ContractError};
 
-    use cosmwasm_std::{
-        coin,
-        testing::{mock_dependencies, mock_env},
-        to_binary, Addr, Empty, Uint128,
-    };
+    use cosmwasm_std::{coin, to_binary, Addr, Empty, Uint128, WasmMsg};
     use cw20::{Cw20Coin, Cw20ExecuteMsg, Denom};
+    use cw_ownable::{Action, Ownership, OwnershipError};
     use cw_utils::Duration;
 
     use cw_multi_test::{next_block, App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg};
 
+    use cw20_stake_external_rewards_v1 as v1;
+
     use crate::msg::{ExecuteMsg, InfoResponse, PendingRewardsResponse, QueryMsg, ReceiveMsg};
 
     const OWNER: &str = "owner";
-    const MANAGER: &str = "manager";
     const ADDR1: &str = "addr0001";
     const ADDR2: &str = "addr0002";
     const ADDR3: &str = "addr0003";
@@ -543,6 +483,16 @@ mod tests {
             crate::contract::execute,
             crate::contract::instantiate,
             crate::contract::query,
+        )
+        .with_migrate(crate::contract::migrate);
+        Box::new(contract)
+    }
+
+    pub fn contract_rewards_v1() -> Box<dyn Contract<Empty>> {
+        let contract = ContractWrapper::new(
+            v1::contract::execute,
+            v1::contract::instantiate,
+            v1::contract::query,
         );
         Box::new(contract)
     }
@@ -592,7 +542,6 @@ mod tests {
         let staking_code_id = app.store_code(contract_staking());
         let msg = cw20_stake::msg::InstantiateMsg {
             owner: Some(OWNER.to_string()),
-            manager: Some("manager".to_string()),
             token_address: cw20.to_string(),
             unstaking_duration,
         };
@@ -655,12 +604,10 @@ mod tests {
         staking_contract: Addr,
         reward_token: Denom,
         owner: Addr,
-        manager: Addr,
     ) -> Addr {
         let reward_code_id = app.store_code(contract_rewards());
         let msg = crate::msg::InstantiateMsg {
             owner: Some(owner.clone().into_string()),
-            manager: Some(manager.into_string()),
             staking_contract: staking_contract.clone().into_string(),
             reward_token,
             reward_duration: 100000,
@@ -698,6 +645,12 @@ mod tests {
         app.wrap().query_balance(address, denom).unwrap().amount
     }
 
+    fn get_ownership<T: Into<String>>(app: &App, address: T) -> Ownership<Addr> {
+        app.wrap()
+            .query_wasm_smart(address, &QueryMsg::Ownership {})
+            .unwrap()
+    }
+
     fn assert_pending_rewards(app: &mut App, reward_addr: &Addr, address: &str, expected: u128) {
         let res: PendingRewardsResponse = app
             .borrow_mut()
@@ -756,11 +709,9 @@ mod tests {
 
         let reward_token = Denom::Native(denom);
         let owner = admin;
-        let manager = Addr::unchecked(MANAGER);
         let reward_code_id = app.store_code(contract_rewards());
         let msg = crate::msg::InstantiateMsg {
             owner: Some(owner.clone().into_string()),
-            manager: Some(manager.into_string()),
             staking_contract: staking_addr.to_string(),
             reward_token,
             reward_duration: 0,
@@ -807,7 +758,6 @@ mod tests {
             staking_addr.clone(),
             Denom::Native(denom.clone()),
             admin.clone(),
-            Addr::unchecked(MANAGER),
         );
 
         app.borrow_mut().update_block(|b| b.height = 1000);
@@ -1043,7 +993,6 @@ mod tests {
             staking_addr.clone(),
             Denom::Cw20(reward_token.clone()),
             admin.clone(),
-            Addr::unchecked(MANAGER),
         );
 
         app.borrow_mut().update_block(|b| b.height = 1000);
@@ -1283,7 +1232,6 @@ mod tests {
             staking_addr,
             Denom::Native(denom.clone()),
             admin.clone(),
-            Addr::unchecked(MANAGER),
         );
 
         app.borrow_mut().update_block(|b| b.height = 1000);
@@ -1303,7 +1251,7 @@ mod tests {
             .downcast()
             .unwrap();
 
-        assert_eq!(err, ContractError::Unauthorized {});
+        assert_eq!(err, ContractError::Ownable(OwnershipError::NotOwner));
 
         let _res = app
             .borrow_mut()
@@ -1414,7 +1362,6 @@ mod tests {
             staking_addr,
             Denom::Native(denom.clone()),
             admin.clone(),
-            Addr::unchecked(MANAGER),
         );
 
         let res: InfoResponse = app
@@ -1461,7 +1408,7 @@ mod tests {
             .unwrap_err()
             .downcast()
             .unwrap();
-        assert_eq!(err, ContractError::Unauthorized {});
+        assert_eq!(err, ContractError::Ownable(OwnershipError::NotOwner));
 
         let reward_funding = vec![coin(1000, denom)];
         app.sudo(SudoMsg::Bank({
@@ -1537,8 +1484,7 @@ mod tests {
     #[test]
     fn test_update_owner() {
         let mut app = mock_app();
-        let owner = Addr::unchecked(OWNER);
-        let manager = Addr::unchecked(MANAGER);
+        let addr_owner = Addr::unchecked(OWNER);
         app.borrow_mut().update_block(|b| b.height = 0);
         let initial_balances = vec![
             Cw20Coin {
@@ -1561,262 +1507,83 @@ mod tests {
             &mut app,
             staking_addr,
             Denom::Native(denom),
-            owner.clone(),
-            manager.clone(),
+            addr_owner.clone(),
         );
 
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
-            .unwrap();
-
-        assert_eq!(res.config.owner, Some(owner.clone()));
-        assert_eq!(res.config.manager, Some(manager.clone()));
-
-        // manager cannot update owner
-        let msg = ExecuteMsg::UpdateOwner {
-            new_owner: Some(ADDR1.to_string()),
-        };
-        let err: ContractError = app
-            .borrow_mut()
-            .execute_contract(manager, reward_addr.clone(), &msg, &[])
-            .unwrap_err()
-            .downcast()
-            .unwrap();
-        assert_eq!(err, ContractError::Unauthorized {});
+        let owner = get_ownership(&app, &reward_addr).owner;
+        assert_eq!(owner, Some(addr_owner.clone()));
 
         // random addr cannot update owner
-        let msg = ExecuteMsg::UpdateOwner {
-            new_owner: Some(ADDR1.to_string()),
-        };
+        let msg = ExecuteMsg::UpdateOwnership(Action::TransferOwnership {
+            new_owner: ADDR1.to_string(),
+            expiry: None,
+        });
         let err: ContractError = app
             .borrow_mut()
             .execute_contract(Addr::unchecked(ADDR1), reward_addr.clone(), &msg, &[])
             .unwrap_err()
             .downcast()
             .unwrap();
-        assert_eq!(err, ContractError::Unauthorized {});
-
-        // Update owner
-        let msg = ExecuteMsg::UpdateOwner {
-            new_owner: Some(ADDR1.to_string()),
-        };
-        let _resp = app
-            .borrow_mut()
-            .execute_contract(owner, reward_addr.clone(), &msg, &[])
-            .unwrap();
-
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
-            .unwrap();
-
-        assert_eq!(res.config.owner, Some(Addr::unchecked(ADDR1)));
+        assert_eq!(err, ContractError::Ownable(OwnershipError::NotOwner));
 
-        // Remove owner
-        let msg = ExecuteMsg::UpdateOwner { new_owner: None };
-        let _resp = app
-            .borrow_mut()
-            .execute_contract(Addr::unchecked(ADDR1), reward_addr.clone(), &msg, &[])
-            .unwrap();
-
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
+        // owner nominates a new onwer.
+        app.borrow_mut()
+            .execute_contract(addr_owner.clone(), reward_addr.clone(), &msg, &[])
             .unwrap();
 
-        assert_eq!(res.config.owner, None);
-    }
-
-    #[test]
-    fn test_update_manager() {
-        let mut app = mock_app();
-        let owner = Addr::unchecked(OWNER);
-        let manager = Addr::unchecked(MANAGER);
-        app.borrow_mut().update_block(|b| b.height = 0);
-        let initial_balances = vec![
-            Cw20Coin {
-                address: ADDR1.to_string(),
-                amount: Uint128::new(100),
-            },
-            Cw20Coin {
-                address: ADDR2.to_string(),
-                amount: Uint128::new(50),
-            },
-            Cw20Coin {
-                address: ADDR3.to_string(),
-                amount: Uint128::new(50),
-            },
-        ];
-        let denom = "utest".to_string();
-        let (staking_addr, _cw20_addr) = setup_staking_contract(&mut app, initial_balances);
-
-        let reward_addr = setup_reward_contract(
-            &mut app,
-            staking_addr,
-            Denom::Native(denom),
-            owner.clone(),
-            manager.clone(),
+        let ownership = get_ownership(&app, &reward_addr);
+        assert_eq!(
+            ownership,
+            Ownership::<Addr> {
+                owner: Some(addr_owner),
+                pending_owner: Some(Addr::unchecked(ADDR1)),
+                pending_expiry: None,
+            }
         );
 
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
-            .unwrap();
-
-        assert_eq!(res.config.owner, Some(owner.clone()));
-        assert_eq!(res.config.manager, Some(manager.clone()));
-
-        // random addr cannot update manager
-        let msg = ExecuteMsg::UpdateManager {
-            new_manager: Some(ADDR2.to_string()),
-        };
-        let err: ContractError = app
-            .borrow_mut()
-            .execute_contract(Addr::unchecked(ADDR1), reward_addr.clone(), &msg, &[])
-            .unwrap_err()
-            .downcast()
-            .unwrap();
-        assert_eq!(err, ContractError::Unauthorized {});
-
-        // Owner can update manager
-        let msg = ExecuteMsg::UpdateManager {
-            new_manager: Some(ADDR1.to_string()),
-        };
-        let _resp = app
-            .borrow_mut()
-            .execute_contract(owner, reward_addr.clone(), &msg, &[])
-            .unwrap();
-
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
-            .unwrap();
-
-        assert_eq!(res.config.manager, Some(Addr::unchecked(ADDR1)));
-
-        // Manager can update manager
-        let msg = ExecuteMsg::UpdateManager {
-            new_manager: Some(manager.clone().into_string()),
-        };
-        let _resp = app
-            .borrow_mut()
-            .execute_contract(Addr::unchecked(ADDR1), reward_addr.clone(), &msg, &[])
-            .unwrap();
-
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
-            .unwrap();
-
-        assert_eq!(res.config.manager, Some(manager.clone()));
-
-        // Remove manager
-        let msg = ExecuteMsg::UpdateManager { new_manager: None };
-        let _resp = app
-            .borrow_mut()
-            .execute_contract(manager, reward_addr.clone(), &msg, &[])
-            .unwrap();
-
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
-            .unwrap();
-
-        assert_eq!(res.config.manager, None);
-    }
-
-    #[test]
-    fn test_manager_permissions() {
-        let mut app = mock_app();
-        let owner = Addr::unchecked(OWNER);
-        let manager = Addr::unchecked(MANAGER);
-        app.borrow_mut().update_block(|b| b.height = 0);
-        let initial_balances = vec![
-            Cw20Coin {
-                address: ADDR1.to_string(),
-                amount: Uint128::new(100),
-            },
-            Cw20Coin {
-                address: ADDR2.to_string(),
-                amount: Uint128::new(50),
-            },
-            Cw20Coin {
-                address: ADDR3.to_string(),
-                amount: Uint128::new(50),
-            },
-        ];
-        let denom = "utest".to_string();
-        let (staking_addr, _cw20_addr) = setup_staking_contract(&mut app, initial_balances);
+        // new owner accepts the nomination.
+        app.execute_contract(
+            Addr::unchecked(ADDR1),
+            reward_addr.clone(),
+            &ExecuteMsg::UpdateOwnership(Action::AcceptOwnership),
+            &[],
+        )
+        .unwrap();
 
-        let reward_addr = setup_reward_contract(
-            &mut app,
-            staking_addr,
-            Denom::Native(denom.clone()),
-            owner,
-            manager.clone(),
+        let ownership = get_ownership(&app, &reward_addr);
+        assert_eq!(
+            ownership,
+            Ownership::<Addr> {
+                owner: Some(Addr::unchecked(ADDR1)),
+                pending_owner: None,
+                pending_expiry: None,
+            }
         );
 
-        // Manager can update reward duration
-        let msg = ExecuteMsg::UpdateRewardDuration { new_duration: 10 };
-        let _resp = app
-            .borrow_mut()
-            .execute_contract(manager.clone(), reward_addr.clone(), &msg, &[])
-            .unwrap();
-
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
-            .unwrap();
-
-        assert_eq!(res.reward.reward_rate, Uint128::new(0));
-        assert_eq!(res.reward.period_finish, 0);
-        assert_eq!(res.reward.reward_duration, 10);
-
-        // Manager can fund contract
-
-        let reward_funding = vec![coin(100, denom)];
-        app.sudo(SudoMsg::Bank({
-            BankSudo::Mint {
-                to_address: manager.to_string(),
-                amount: reward_funding.clone(),
-            }
-        }))
+        // new owner renounces ownership.
+        app.execute_contract(
+            Addr::unchecked(ADDR1),
+            reward_addr.clone(),
+            &ExecuteMsg::UpdateOwnership(Action::RenounceOwnership),
+            &[],
+        )
         .unwrap();
 
-        app.borrow_mut().update_block(|b| b.height = 1000);
-
-        let fund_msg = ExecuteMsg::Fund {};
-
-        let _res = app
-            .borrow_mut()
-            .execute_contract(manager, reward_addr.clone(), &fund_msg, &reward_funding)
-            .unwrap();
-
-        let res: InfoResponse = app
-            .borrow_mut()
-            .wrap()
-            .query_wasm_smart(&reward_addr, &QueryMsg::Info {})
-            .unwrap();
-
-        assert_eq!(res.reward.reward_rate, Uint128::new(10));
-        assert_eq!(res.reward.period_finish, 1010);
-        assert_eq!(res.reward.reward_duration, 10);
+        let ownership = get_ownership(&app, &reward_addr);
+        assert_eq!(
+            ownership,
+            Ownership::<Addr> {
+                owner: None,
+                pending_owner: None,
+                pending_expiry: None,
+            }
+        );
     }
 
     #[test]
     fn test_cannot_fund_with_wrong_coin_native() {
         let mut app = mock_app();
         let owner = Addr::unchecked(OWNER);
-        let manager = Addr::unchecked(MANAGER);
         app.borrow_mut().update_block(|b| b.height = 0);
         let initial_balances = vec![
             Cw20Coin {
@@ -1840,7 +1607,6 @@ mod tests {
             staking_addr,
             Denom::Native(denom.clone()),
             owner.clone(),
-            manager,
         );
 
         app.borrow_mut().update_block(|b| b.height = 1000);
@@ -1962,7 +1728,6 @@ mod tests {
             staking_addr,
             Denom::Cw20(Addr::unchecked("dummy_cw20")),
             admin.clone(),
-            Addr::unchecked(MANAGER),
         );
 
         app.borrow_mut().update_block(|b| b.height = 1000);
@@ -2042,7 +1807,6 @@ mod tests {
             staking_addr.clone(),
             Denom::Native(denom),
             admin.clone(),
-            Addr::unchecked(MANAGER),
         );
 
         app.borrow_mut().update_block(|b| b.height = 1000);
@@ -2121,13 +1885,8 @@ mod tests {
             }
         }))
         .unwrap();
-        let reward_addr = setup_reward_contract(
-            &mut app,
-            staking_addr,
-            Denom::Native(denom),
-            admin.clone(),
-            Addr::unchecked(MANAGER),
-        );
+        let reward_addr =
+            setup_reward_contract(&mut app, staking_addr, Denom::Native(denom), admin.clone());
 
         app.borrow_mut().update_block(|b| b.height = 1000);
 
@@ -2185,13 +1944,8 @@ mod tests {
             }
         }))
         .unwrap();
-        let reward_addr = setup_reward_contract(
-            &mut app,
-            staking_addr,
-            Denom::Native(denom),
-            admin.clone(),
-            Addr::unchecked(MANAGER),
-        );
+        let reward_addr =
+            setup_reward_contract(&mut app, staking_addr, Denom::Native(denom), admin.clone());
 
         app.borrow_mut().update_block(|b| b.height = 1000);
 
@@ -2204,12 +1958,80 @@ mod tests {
     }
 
     #[test]
-    pub fn test_migrate_update_version() {
-        let mut deps = mock_dependencies();
-        cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap();
-        migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap();
-        let version = cw2::get_contract_version(&deps.storage).unwrap();
-        assert_eq!(version.version, CONTRACT_VERSION);
-        assert_eq!(version.contract, CONTRACT_NAME);
+    fn test_migrate_from_v1() {
+        let mut app = App::default();
+
+        let v1_code = app.store_code(contract_rewards_v1());
+        let v2_code = app.store_code(contract_rewards());
+
+        let initial_balances = vec![
+            Cw20Coin {
+                address: ADDR1.to_string(),
+                amount: Uint128::new(100),
+            },
+            Cw20Coin {
+                address: ADDR2.to_string(),
+                amount: Uint128::new(50),
+            },
+            Cw20Coin {
+                address: ADDR3.to_string(),
+                amount: Uint128::new(50),
+            },
+        ];
+        let denom = "utest".to_string();
+        let (staking_addr, _) = setup_staking_contract(&mut app, initial_balances);
+
+        let rewards_addr = app
+            .instantiate_contract(
+                v1_code,
+                Addr::unchecked(OWNER),
+                &v1::msg::InstantiateMsg {
+                    owner: Some(OWNER.to_string()),
+                    manager: Some(ADDR1.to_string()),
+                    staking_contract: staking_addr.into_string(),
+                    reward_token: cw20_013::Denom::Native(denom),
+                    reward_duration: 10000,
+                },
+                &[],
+                "rewards".to_string(),
+                Some(OWNER.to_string()),
+            )
+            .unwrap();
+
+        app.execute(
+            Addr::unchecked(OWNER),
+            WasmMsg::Migrate {
+                contract_addr: rewards_addr.to_string(),
+                new_code_id: v2_code,
+                msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(),
+            }
+            .into(),
+        )
+        .unwrap();
+
+        let ownership = get_ownership(&app, &rewards_addr);
+        assert_eq!(
+            ownership,
+            Ownership::<Addr> {
+                owner: Some(Addr::unchecked(OWNER)),
+                pending_owner: None,
+                pending_expiry: None,
+            }
+        );
+
+        let err: ContractError = app
+            .execute(
+                Addr::unchecked(OWNER),
+                WasmMsg::Migrate {
+                    contract_addr: rewards_addr.to_string(),
+                    new_code_id: v2_code,
+                    msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(),
+                }
+                .into(),
+            )
+            .unwrap_err()
+            .downcast()
+            .unwrap();
+        assert_eq!(err, ContractError::AlreadyMigrated {});
     }
 }
diff --git a/contracts/staking/cw20-stake-external-rewards/src/error.rs b/contracts/staking/cw20-stake-external-rewards/src/error.rs
index e2c51470..90725b4d 100644
--- a/contracts/staking/cw20-stake-external-rewards/src/error.rs
+++ b/contracts/staking/cw20-stake-external-rewards/src/error.rs
@@ -3,12 +3,14 @@ use thiserror::Error;
 
 #[derive(Error, Debug, PartialEq)]
 pub enum ContractError {
-    #[error("{0}")]
+    #[error(transparent)]
     Std(#[from] StdError),
-    #[error("{0}")]
+    #[error(transparent)]
+    Ownable(#[from] cw_ownable::OwnershipError),
+    #[error(transparent)]
     Cw20Error(#[from] cw20_base::ContractError),
-    #[error("Unauthorized")]
-    Unauthorized {},
+    #[error("Staking change hook sender is not staking contract")]
+    InvalidHookSender {},
     #[error("No rewards claimable")]
     NoRewardsClaimable {},
     #[error("Reward period not finished")]
@@ -21,4 +23,6 @@ pub enum ContractError {
     RewardRateLessThenOnePerBlock {},
     #[error("Reward duration can not be zero")]
     ZeroRewardDuration {},
+    #[error("can not migrate. current version is up to date")]
+    AlreadyMigrated {},
 }
diff --git a/contracts/staking/cw20-stake-external-rewards/src/msg.rs b/contracts/staking/cw20-stake-external-rewards/src/msg.rs
index 6db4d223..fc859d69 100644
--- a/contracts/staking/cw20-stake-external-rewards/src/msg.rs
+++ b/contracts/staking/cw20-stake-external-rewards/src/msg.rs
@@ -4,20 +4,23 @@ use cw20::{Cw20ReceiveMsg, Denom};
 use cw20_stake::hooks::StakeChangedHookMsg;
 
 use crate::state::{Config, RewardConfig};
+
 pub use cw_controllers::ClaimsResponse;
+// so that consumers don't need a cw_ownable dependency to consume
+// this contract's queries.
+pub use cw_ownable::Ownership;
+
+use cw_ownable::cw_ownable;
 
 #[cw_serde]
 pub struct InstantiateMsg {
     pub owner: Option<String>,
-    pub manager: Option<String>,
     pub staking_contract: String,
     pub reward_token: Denom,
     pub reward_duration: u64,
 }
 
-#[cw_serde]
-pub struct MigrateMsg {}
-
+#[cw_ownable]
 #[cw_serde]
 pub enum ExecuteMsg {
     StakeChangeHook(StakeChangedHookMsg),
@@ -25,8 +28,15 @@ pub enum ExecuteMsg {
     Receive(Cw20ReceiveMsg),
     Fund {},
     UpdateRewardDuration { new_duration: u64 },
-    UpdateOwner { new_owner: Option<String> },
-    UpdateManager { new_manager: Option<String> },
+}
+
+#[cw_serde]
+pub enum MigrateMsg {
+    /// Migrates from version 0.2.6 to 2.0.0. The significant changes
+    /// being the addition of a two-step ownership transfer using
+    /// `cw_ownable` and the removal of the manager. Migrating will
+    /// automatically remove the current manager.
+    FromV1 {},
 }
 
 #[cw_serde]
@@ -41,6 +51,8 @@ pub enum QueryMsg {
     Info {},
     #[returns(PendingRewardsResponse)]
     GetPendingRewards { address: String },
+    #[returns(::cw_ownable::Ownership<::cosmwasm_std::Addr>)]
+    Ownership {},
 }
 
 #[cw_serde]
diff --git a/contracts/staking/cw20-stake-external-rewards/src/state.rs b/contracts/staking/cw20-stake-external-rewards/src/state.rs
index 68d9ea52..0c9e98d3 100644
--- a/contracts/staking/cw20-stake-external-rewards/src/state.rs
+++ b/contracts/staking/cw20-stake-external-rewards/src/state.rs
@@ -6,12 +6,12 @@ use cw_storage_plus::{Item, Map};
 
 #[cw_serde]
 pub struct Config {
-    pub owner: Option<Addr>,
-    pub manager: Option<Addr>,
     pub staking_contract: Addr,
     pub reward_token: Denom,
 }
-pub const CONFIG: Item<Config> = Item::new("config");
+
+// `"config"` key stores v1 configuration.
+pub const CONFIG: Item<Config> = Item::new("config_v2");
 
 #[cw_serde]
 pub struct RewardConfig {
diff --git a/contracts/staking/cw20-stake-reward-distributor/Cargo.toml b/contracts/staking/cw20-stake-reward-distributor/Cargo.toml
index d40a9f05..3359f60c 100644
--- a/contracts/staking/cw20-stake-reward-distributor/Cargo.toml
+++ b/contracts/staking/cw20-stake-reward-distributor/Cargo.toml
@@ -1,16 +1,8 @@
 [package]
-name = "stake-cw20-reward-distributor"
+name = "cw20-stake-reward-distributor"
 version = "2.0.0-beta"
 edition = "2018"
-authors = ["Vernon Johnson <vtj2105@columbia.edu>"]
-
-exclude = [
-  # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication.
-  "contract.wasm",
-  "hash.txt",
-]
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+authors = ["Vernon Johnson <vtj2105@columbia.edu>, ekez <ekez@withoutdoing.com>"]
 
 [lib]
 crate-type = ["cdylib", "rlib"]
@@ -21,25 +13,18 @@ backtraces = ["cosmwasm-std/backtraces"]
 # use library feature to disable all instantiate/execute/query exports
 library = []
 
-[package.metadata.scripts]
-optimize = """docker run --rm -v "$(pwd)":/code \
-  --mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
-  --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
-  cosmwasm/workspace-optimizer:0.12.6
-"""
-
 [dependencies]
 cosmwasm-std = { workspace = true }
-cosmwasm-storage = { workspace = true }
 cosmwasm-schema = { workspace = true }
 cw-storage-plus = { workspace = true }
 cw2 = { workspace = true }
 cw20 = { workspace = true }
 cw-utils = { workspace = true }
-
 cw20-base = {  workspace = true, features = ["library"] }
 cw20-stake = { workspace = true, features = ["library"]}
 thiserror = { workspace = true }
+cw-ownable = { workspace = true }
+cw20-stake-reward-distributor-v1 = { workspace = true, features = ["library"] }
 
 [dev-dependencies]
 cw-multi-test = { workspace = true }
diff --git a/contracts/staking/cw20-stake-reward-distributor/examples/schema.rs b/contracts/staking/cw20-stake-reward-distributor/examples/schema.rs
index 77cdca6b..94121312 100644
--- a/contracts/staking/cw20-stake-reward-distributor/examples/schema.rs
+++ b/contracts/staking/cw20-stake-reward-distributor/examples/schema.rs
@@ -1,5 +1,5 @@
 use cosmwasm_schema::write_api;
-use stake_cw20_reward_distributor::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
+use cw20_stake_reward_distributor::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
 
 fn main() {
     write_api! {
diff --git a/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json b/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json
new file mode 100644
index 00000000..57c898c4
--- /dev/null
+++ b/contracts/staking/cw20-stake-reward-distributor/schema/cw20-stake-reward-distributor.json
@@ -0,0 +1,443 @@
+{
+  "contract_name": "cw20-stake-reward-distributor",
+  "contract_version": "2.0.0-beta",
+  "idl_version": "1.0.0",
+  "instantiate": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "InstantiateMsg",
+    "type": "object",
+    "required": [
+      "owner",
+      "reward_rate",
+      "reward_token",
+      "staking_addr"
+    ],
+    "properties": {
+      "owner": {
+        "type": "string"
+      },
+      "reward_rate": {
+        "$ref": "#/definitions/Uint128"
+      },
+      "reward_token": {
+        "type": "string"
+      },
+      "staking_addr": {
+        "type": "string"
+      }
+    },
+    "additionalProperties": false,
+    "definitions": {
+      "Uint128": {
+        "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+        "type": "string"
+      }
+    }
+  },
+  "execute": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "ExecuteMsg",
+    "oneOf": [
+      {
+        "type": "object",
+        "required": [
+          "update_config"
+        ],
+        "properties": {
+          "update_config": {
+            "type": "object",
+            "required": [
+              "reward_rate",
+              "reward_token",
+              "staking_addr"
+            ],
+            "properties": {
+              "reward_rate": {
+                "$ref": "#/definitions/Uint128"
+              },
+              "reward_token": {
+                "type": "string"
+              },
+              "staking_addr": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "distribute"
+        ],
+        "properties": {
+          "distribute": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "withdraw"
+        ],
+        "properties": {
+          "withdraw": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.",
+        "type": "object",
+        "required": [
+          "update_ownership"
+        ],
+        "properties": {
+          "update_ownership": {
+            "$ref": "#/definitions/Action"
+          }
+        },
+        "additionalProperties": false
+      }
+    ],
+    "definitions": {
+      "Action": {
+        "description": "Actions that can be taken to alter the contract's ownership",
+        "oneOf": [
+          {
+            "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.",
+            "type": "object",
+            "required": [
+              "transfer_ownership"
+            ],
+            "properties": {
+              "transfer_ownership": {
+                "type": "object",
+                "required": [
+                  "new_owner"
+                ],
+                "properties": {
+                  "expiry": {
+                    "anyOf": [
+                      {
+                        "$ref": "#/definitions/Expiration"
+                      },
+                      {
+                        "type": "null"
+                      }
+                    ]
+                  },
+                  "new_owner": {
+                    "type": "string"
+                  }
+                },
+                "additionalProperties": false
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.",
+            "type": "string",
+            "enum": [
+              "accept_ownership"
+            ]
+          },
+          {
+            "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.",
+            "type": "string",
+            "enum": [
+              "renounce_ownership"
+            ]
+          }
+        ]
+      },
+      "Expiration": {
+        "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+        "oneOf": [
+          {
+            "description": "AtHeight will expire when `env.block.height` >= height",
+            "type": "object",
+            "required": [
+              "at_height"
+            ],
+            "properties": {
+              "at_height": {
+                "type": "integer",
+                "format": "uint64",
+                "minimum": 0.0
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "AtTime will expire when `env.block.time` >= time",
+            "type": "object",
+            "required": [
+              "at_time"
+            ],
+            "properties": {
+              "at_time": {
+                "$ref": "#/definitions/Timestamp"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "Never will never expire. Used to express the empty variant",
+            "type": "object",
+            "required": [
+              "never"
+            ],
+            "properties": {
+              "never": {
+                "type": "object",
+                "additionalProperties": false
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
+      "Timestamp": {
+        "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+        "allOf": [
+          {
+            "$ref": "#/definitions/Uint64"
+          }
+        ]
+      },
+      "Uint128": {
+        "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+        "type": "string"
+      },
+      "Uint64": {
+        "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+        "type": "string"
+      }
+    }
+  },
+  "query": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "QueryMsg",
+    "oneOf": [
+      {
+        "type": "object",
+        "required": [
+          "info"
+        ],
+        "properties": {
+          "info": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "type": "object",
+        "required": [
+          "ownership"
+        ],
+        "properties": {
+          "ownership": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
+  },
+  "migrate": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "MigrateMsg",
+    "oneOf": [
+      {
+        "description": "Updates the contract from v1 -> v2. Version two implements a two step ownership transfer.",
+        "type": "object",
+        "required": [
+          "from_v1"
+        ],
+        "properties": {
+          "from_v1": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
+  },
+  "sudo": null,
+  "responses": {
+    "info": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "InfoResponse",
+      "type": "object",
+      "required": [
+        "balance",
+        "config",
+        "last_payment_block"
+      ],
+      "properties": {
+        "balance": {
+          "$ref": "#/definitions/Uint128"
+        },
+        "config": {
+          "$ref": "#/definitions/Config"
+        },
+        "last_payment_block": {
+          "type": "integer",
+          "format": "uint64",
+          "minimum": 0.0
+        }
+      },
+      "additionalProperties": false,
+      "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"
+        },
+        "Config": {
+          "type": "object",
+          "required": [
+            "reward_rate",
+            "reward_token",
+            "staking_addr"
+          ],
+          "properties": {
+            "reward_rate": {
+              "$ref": "#/definitions/Uint128"
+            },
+            "reward_token": {
+              "$ref": "#/definitions/Addr"
+            },
+            "staking_addr": {
+              "$ref": "#/definitions/Addr"
+            }
+          },
+          "additionalProperties": false
+        },
+        "Uint128": {
+          "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
+          "type": "string"
+        }
+      }
+    },
+    "ownership": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Ownership_for_Addr",
+      "description": "The contract's ownership info",
+      "type": "object",
+      "properties": {
+        "owner": {
+          "description": "The contract's current owner. `None` if the ownership has been renounced.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Addr"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pending_expiry": {
+          "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Expiration"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pending_owner": {
+          "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Addr"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "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"
+        },
+        "Expiration": {
+          "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+          "oneOf": [
+            {
+              "description": "AtHeight will expire when `env.block.height` >= height",
+              "type": "object",
+              "required": [
+                "at_height"
+              ],
+              "properties": {
+                "at_height": {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "description": "AtTime will expire when `env.block.time` >= time",
+              "type": "object",
+              "required": [
+                "at_time"
+              ],
+              "properties": {
+                "at_time": {
+                  "$ref": "#/definitions/Timestamp"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "description": "Never will never expire. Used to express the empty variant",
+              "type": "object",
+              "required": [
+                "never"
+              ],
+              "properties": {
+                "never": {
+                  "type": "object",
+                  "additionalProperties": false
+                }
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "Timestamp": {
+          "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Uint64"
+            }
+          ]
+        },
+        "Uint64": {
+          "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+          "type": "string"
+        }
+      }
+    }
+  }
+}
diff --git a/contracts/staking/cw20-stake-reward-distributor/schema/stake-cw20-reward-distributor.json b/contracts/staking/cw20-stake-reward-distributor/schema/stake-cw20-reward-distributor.json
deleted file mode 100644
index 4de1c46a..00000000
--- a/contracts/staking/cw20-stake-reward-distributor/schema/stake-cw20-reward-distributor.json
+++ /dev/null
@@ -1,194 +0,0 @@
-{
-  "contract_name": "stake-cw20-reward-distributor",
-  "contract_version": "2.0.0-beta",
-  "idl_version": "1.0.0",
-  "instantiate": {
-    "$schema": "http://json-schema.org/draft-07/schema#",
-    "title": "InstantiateMsg",
-    "type": "object",
-    "required": [
-      "owner",
-      "reward_rate",
-      "reward_token",
-      "staking_addr"
-    ],
-    "properties": {
-      "owner": {
-        "type": "string"
-      },
-      "reward_rate": {
-        "$ref": "#/definitions/Uint128"
-      },
-      "reward_token": {
-        "type": "string"
-      },
-      "staking_addr": {
-        "type": "string"
-      }
-    },
-    "additionalProperties": false,
-    "definitions": {
-      "Uint128": {
-        "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
-        "type": "string"
-      }
-    }
-  },
-  "execute": {
-    "$schema": "http://json-schema.org/draft-07/schema#",
-    "title": "ExecuteMsg",
-    "oneOf": [
-      {
-        "type": "object",
-        "required": [
-          "update_config"
-        ],
-        "properties": {
-          "update_config": {
-            "type": "object",
-            "required": [
-              "owner",
-              "reward_rate",
-              "reward_token",
-              "staking_addr"
-            ],
-            "properties": {
-              "owner": {
-                "type": "string"
-              },
-              "reward_rate": {
-                "$ref": "#/definitions/Uint128"
-              },
-              "reward_token": {
-                "type": "string"
-              },
-              "staking_addr": {
-                "type": "string"
-              }
-            },
-            "additionalProperties": false
-          }
-        },
-        "additionalProperties": false
-      },
-      {
-        "type": "object",
-        "required": [
-          "distribute"
-        ],
-        "properties": {
-          "distribute": {
-            "type": "object",
-            "additionalProperties": false
-          }
-        },
-        "additionalProperties": false
-      },
-      {
-        "type": "object",
-        "required": [
-          "withdraw"
-        ],
-        "properties": {
-          "withdraw": {
-            "type": "object",
-            "additionalProperties": false
-          }
-        },
-        "additionalProperties": false
-      }
-    ],
-    "definitions": {
-      "Uint128": {
-        "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
-        "type": "string"
-      }
-    }
-  },
-  "query": {
-    "$schema": "http://json-schema.org/draft-07/schema#",
-    "title": "QueryMsg",
-    "oneOf": [
-      {
-        "type": "object",
-        "required": [
-          "info"
-        ],
-        "properties": {
-          "info": {
-            "type": "object",
-            "additionalProperties": false
-          }
-        },
-        "additionalProperties": false
-      }
-    ]
-  },
-  "migrate": {
-    "$schema": "http://json-schema.org/draft-07/schema#",
-    "title": "MigrateMsg",
-    "type": "object",
-    "additionalProperties": false
-  },
-  "sudo": null,
-  "responses": {
-    "info": {
-      "$schema": "http://json-schema.org/draft-07/schema#",
-      "title": "InfoResponse",
-      "type": "object",
-      "required": [
-        "balance",
-        "config",
-        "last_payment_block"
-      ],
-      "properties": {
-        "balance": {
-          "$ref": "#/definitions/Uint128"
-        },
-        "config": {
-          "$ref": "#/definitions/Config"
-        },
-        "last_payment_block": {
-          "type": "integer",
-          "format": "uint64",
-          "minimum": 0.0
-        }
-      },
-      "additionalProperties": false,
-      "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"
-        },
-        "Config": {
-          "type": "object",
-          "required": [
-            "owner",
-            "reward_rate",
-            "reward_token",
-            "staking_addr"
-          ],
-          "properties": {
-            "owner": {
-              "$ref": "#/definitions/Addr"
-            },
-            "reward_rate": {
-              "$ref": "#/definitions/Uint128"
-            },
-            "reward_token": {
-              "$ref": "#/definitions/Addr"
-            },
-            "staking_addr": {
-              "$ref": "#/definitions/Addr"
-            }
-          },
-          "additionalProperties": false
-        },
-        "Uint128": {
-          "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
-          "type": "string"
-        }
-      }
-    }
-  }
-}
diff --git a/contracts/staking/cw20-stake-reward-distributor/src/contract.rs b/contracts/staking/cw20-stake-reward-distributor/src/contract.rs
index 70e31924..d4108d88 100644
--- a/contracts/staking/cw20-stake-reward-distributor/src/contract.rs
+++ b/contracts/staking/cw20-stake-reward-distributor/src/contract.rs
@@ -8,7 +8,7 @@ use crate::error::ContractError;
 use crate::msg::{ExecuteMsg, InfoResponse, InstantiateMsg, MigrateMsg, QueryMsg};
 use crate::state::{Config, CONFIG, LAST_PAYMENT_BLOCK};
 use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};
-use cw2::set_contract_version;
+use cw2::{get_contract_version, set_contract_version, ContractVersion};
 
 pub(crate) const CONTRACT_NAME: &str = "crates.io:cw20-stake-reward-distributor";
 pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -22,7 +22,6 @@ pub fn instantiate(
 ) -> Result<Response, ContractError> {
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
 
-    let owner = deps.api.addr_validate(&msg.owner)?;
     let staking_addr = deps.api.addr_validate(&msg.staking_addr)?;
     if !validate_staking(deps.as_ref(), staking_addr.clone()) {
         return Err(ContractError::InvalidStakingContract {});
@@ -34,19 +33,19 @@ pub fn instantiate(
     }
 
     let config = Config {
-        owner: owner.clone(),
         staking_addr: staking_addr.clone(),
         reward_token: reward_token.clone(),
         reward_rate: msg.reward_rate,
     };
     CONFIG.save(deps.storage, &config)?;
+    cw_ownable::initialize_owner(deps.storage, deps.api, Some(&msg.owner))?;
 
     // Initialize last payment block
     LAST_PAYMENT_BLOCK.save(deps.storage, &env.block.height)?;
 
     Ok(Response::new()
         .add_attribute("action", "instantiate")
-        .add_attribute("owner", owner.into_string())
+        .add_attribute("owner", msg.owner)
         .add_attribute("staking_addr", staking_addr.into_string())
         .add_attribute("reward_token", reward_token.into_string())
         .add_attribute("reward_rate", msg.reward_rate))
@@ -61,21 +60,13 @@ pub fn execute(
 ) -> Result<Response, ContractError> {
     match msg {
         ExecuteMsg::UpdateConfig {
-            owner,
             staking_addr,
             reward_rate,
             reward_token,
-        } => execute_update_config(
-            deps,
-            info,
-            env,
-            owner,
-            staking_addr,
-            reward_rate,
-            reward_token,
-        ),
+        } => execute_update_config(deps, info, env, staking_addr, reward_rate, reward_token),
         ExecuteMsg::Distribute {} => execute_distribute(deps, env),
         ExecuteMsg::Withdraw {} => execute_withdraw(deps, info, env),
+        ExecuteMsg::UpdateOwnership(action) => execute_update_owner(deps, info, env, action),
     }
 }
 
@@ -83,19 +74,14 @@ pub fn execute_update_config(
     deps: DepsMut,
     info: MessageInfo,
     env: Env,
-    owner: String,
     staking_addr: String,
     reward_rate: Uint128,
     reward_token: String,
 ) -> Result<Response, ContractError> {
-    let config = CONFIG.load(deps.storage)?;
-    if config.owner != info.sender {
-        return Err(ContractError::Unauthorized {});
-    }
+    cw_ownable::assert_owner(deps.storage, &info.sender)?;
 
     LAST_PAYMENT_BLOCK.save(deps.storage, &env.block.height)?;
 
-    let owner = deps.api.addr_validate(&owner)?;
     let staking_addr = deps.api.addr_validate(&staking_addr)?;
     if !validate_staking(deps.as_ref(), staking_addr.clone()) {
         return Err(ContractError::InvalidStakingContract {});
@@ -107,7 +93,6 @@ pub fn execute_update_config(
     }
 
     let config = Config {
-        owner: owner.clone(),
         staking_addr: staking_addr.clone(),
         reward_token: reward_token.clone(),
         reward_rate,
@@ -123,12 +108,21 @@ pub fn execute_update_config(
 
     Ok(resp
         .add_attribute("action", "update_config")
-        .add_attribute("owner", owner.into_string())
         .add_attribute("staking_addr", staking_addr.into_string())
         .add_attribute("reward_token", reward_token.into_string())
         .add_attribute("reward_rate", reward_rate))
 }
 
+pub fn execute_update_owner(
+    deps: DepsMut,
+    info: MessageInfo,
+    env: Env,
+    action: cw_ownable::Action,
+) -> Result<Response, ContractError> {
+    let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?;
+    Ok(Response::default().add_attributes(ownership.into_attributes()))
+}
+
 pub fn validate_cw20(deps: Deps, cw20_addr: Addr) -> bool {
     let response: Result<cw20::TokenInfoResponse, StdError> = deps
         .querier
@@ -196,10 +190,8 @@ pub fn execute_withdraw(
     info: MessageInfo,
     env: Env,
 ) -> Result<Response, ContractError> {
+    cw_ownable::assert_owner(deps.storage, &info.sender)?;
     let config = CONFIG.load(deps.storage)?;
-    if config.owner != info.sender {
-        return Err(ContractError::Unauthorized {});
-    }
 
     let balance_info: cw20::BalanceResponse = deps.querier.query_wasm_smart(
         config.reward_token.clone(),
@@ -209,7 +201,9 @@ pub fn execute_withdraw(
     )?;
 
     let msg = to_binary(&cw20::Cw20ExecuteMsg::Transfer {
-        recipient: config.owner.clone().into(),
+        // `assert_owner` call above validates that the sender is the
+        // owner.
+        recipient: info.sender.to_string(),
         amount: balance_info.balance,
     })?;
     let send_msg: CosmosMsg = WasmMsg::Execute {
@@ -223,21 +217,45 @@ pub fn execute_withdraw(
         .add_message(send_msg)
         .add_attribute("action", "withdraw")
         .add_attribute("amount", balance_info.balance)
-        .add_attribute("recipient", config.owner.into_string()))
+        .add_attribute("recipient", &info.sender))
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
         QueryMsg::Info {} => to_binary(&query_info(deps, env)?),
+        QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?),
     }
 }
 
 #[cfg_attr(not(feature = "library"), entry_point)]
-pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
-    // Set contract to version to latest
+pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
+    use cw20_stake_reward_distributor_v1 as v1;
+
+    let ContractVersion { version, .. } = get_contract_version(deps.storage)?;
     set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
-    Ok(Response::default())
+
+    match msg {
+        MigrateMsg::FromV1 {} => {
+            if version == CONTRACT_VERSION {
+                // You can not possibly be migrating from v1 to v2 and
+                // also not changing your contract version.
+                return Err(ContractError::AlreadyMigrated {});
+            }
+            // From v1 -> v2 we moved `owner` out of config and into
+            // the `cw_ownable` package.
+            let config = v1::state::CONFIG.load(deps.storage)?;
+            cw_ownable::initialize_owner(deps.storage, deps.api, Some(config.owner.as_str()))?;
+            let config = Config {
+                staking_addr: config.staking_addr,
+                reward_rate: config.reward_rate,
+                reward_token: config.reward_token,
+            };
+            CONFIG.save(deps.storage, &config)?;
+
+            Ok(Response::default())
+        }
+    }
 }
 
 fn query_info(deps: Deps, env: Env) -> StdResult<InfoResponse> {
diff --git a/contracts/staking/cw20-stake-reward-distributor/src/error.rs b/contracts/staking/cw20-stake-reward-distributor/src/error.rs
index 6c0bb37e..05a4a96e 100644
--- a/contracts/staking/cw20-stake-reward-distributor/src/error.rs
+++ b/contracts/staking/cw20-stake-reward-distributor/src/error.rs
@@ -3,11 +3,11 @@ use thiserror::Error;
 
 #[derive(Error, Debug, PartialEq)]
 pub enum ContractError {
-    #[error("{0}")]
+    #[error(transparent)]
     Std(#[from] StdError),
 
-    #[error("Unauthorized")]
-    Unauthorized {},
+    #[error(transparent)]
+    Ownership(#[from] cw_ownable::OwnershipError),
 
     #[error("Invalid Cw20")]
     InvalidCw20 {},
@@ -20,4 +20,7 @@ pub enum ContractError {
 
     #[error("Rewards have already been distributed for this block")]
     RewardsDistributedForBlock {},
+
+    #[error("can not migrate. current version is up to date")]
+    AlreadyMigrated {},
 }
diff --git a/contracts/staking/cw20-stake-reward-distributor/src/msg.rs b/contracts/staking/cw20-stake-reward-distributor/src/msg.rs
index c2b675d2..5aa45ea5 100644
--- a/contracts/staking/cw20-stake-reward-distributor/src/msg.rs
+++ b/contracts/staking/cw20-stake-reward-distributor/src/msg.rs
@@ -2,6 +2,12 @@ use crate::state::Config;
 use cosmwasm_schema::{cw_serde, QueryResponses};
 use cosmwasm_std::Uint128;
 
+use cw_ownable::cw_ownable;
+
+// so that consumers don't need a cw_ownable dependency to consume
+// this contract's queries.
+pub use cw_ownable::Ownership;
+
 #[cw_serde]
 pub struct InstantiateMsg {
     pub owner: String,
@@ -10,10 +16,10 @@ pub struct InstantiateMsg {
     pub reward_token: String,
 }
 
+#[cw_ownable]
 #[cw_serde]
 pub enum ExecuteMsg {
     UpdateConfig {
-        owner: String,
         staking_addr: String,
         reward_rate: Uint128,
         reward_token: String,
@@ -27,6 +33,9 @@ pub enum ExecuteMsg {
 pub enum QueryMsg {
     #[returns(InfoResponse)]
     Info {},
+
+    #[returns(::cw_ownable::Ownership<::cosmwasm_std::Addr>)]
+    Ownership {},
 }
 
 #[cw_serde]
@@ -37,4 +46,8 @@ pub struct InfoResponse {
 }
 
 #[cw_serde]
-pub struct MigrateMsg {}
+pub enum MigrateMsg {
+    /// Updates the contract from v1 -> v2. Version two implements a
+    /// two step ownership transfer.
+    FromV1 {},
+}
diff --git a/contracts/staking/cw20-stake-reward-distributor/src/state.rs b/contracts/staking/cw20-stake-reward-distributor/src/state.rs
index ebcce131..0546c39a 100644
--- a/contracts/staking/cw20-stake-reward-distributor/src/state.rs
+++ b/contracts/staking/cw20-stake-reward-distributor/src/state.rs
@@ -4,12 +4,12 @@ use cw_storage_plus::Item;
 
 #[cw_serde]
 pub struct Config {
-    pub owner: Addr,
     pub staking_addr: Addr,
     pub reward_rate: Uint128,
     pub reward_token: Addr,
 }
 
-pub const CONFIG: Item<Config> = Item::new("config");
+// `"config"` key stores v1 configuration.
+pub const CONFIG: Item<Config> = Item::new("config_v2");
 
 pub const LAST_PAYMENT_BLOCK: Item<u64> = Item::new("last_payment_block");
diff --git a/contracts/staking/cw20-stake/Cargo.toml b/contracts/staking/cw20-stake/Cargo.toml
index 442f840a..7e24b916 100644
--- a/contracts/staking/cw20-stake/Cargo.toml
+++ b/contracts/staking/cw20-stake/Cargo.toml
@@ -27,6 +27,10 @@ cw20-base = {  workspace = true, features = ["library"] }
 cw2 = { workspace = true }
 thiserror = { workspace = true }
 cw-paginate = { workspace = true }
+cw-ownable = { workspace = true }
+
+cw20-stake-v1 = { workspace = true, features = ["library"] }
+cw-utils-v1 = { workspace = true }
 
 [dev-dependencies]
 cw-multi-test = { workspace = true }
diff --git a/contracts/staking/cw20-stake/schema/cw20-stake.json b/contracts/staking/cw20-stake/schema/cw20-stake.json
index 8d6f132c..57d5602b 100644
--- a/contracts/staking/cw20-stake/schema/cw20-stake.json
+++ b/contracts/staking/cw20-stake/schema/cw20-stake.json
@@ -10,12 +10,6 @@
       "token_address"
     ],
     "properties": {
-      "manager": {
-        "type": [
-          "string",
-          "null"
-        ]
-      },
       "owner": {
         "type": [
           "string",
@@ -142,18 +136,6 @@
                     "type": "null"
                   }
                 ]
-              },
-              "manager": {
-                "type": [
-                  "string",
-                  "null"
-                ]
-              },
-              "owner": {
-                "type": [
-                  "string",
-                  "null"
-                ]
               }
             },
             "additionalProperties": false
@@ -202,9 +184,73 @@
           }
         },
         "additionalProperties": false
+      },
+      {
+        "description": "Update the contract's ownership. The `action` to be provided can be either to propose transferring ownership to an account, accept a pending ownership transfer, or renounce the ownership permanently.",
+        "type": "object",
+        "required": [
+          "update_ownership"
+        ],
+        "properties": {
+          "update_ownership": {
+            "$ref": "#/definitions/Action"
+          }
+        },
+        "additionalProperties": false
       }
     ],
     "definitions": {
+      "Action": {
+        "description": "Actions that can be taken to alter the contract's ownership",
+        "oneOf": [
+          {
+            "description": "Propose to transfer the contract's ownership to another account, optionally with an expiry time.\n\nCan only be called by the contract's current owner.\n\nAny existing pending ownership transfer is overwritten.",
+            "type": "object",
+            "required": [
+              "transfer_ownership"
+            ],
+            "properties": {
+              "transfer_ownership": {
+                "type": "object",
+                "required": [
+                  "new_owner"
+                ],
+                "properties": {
+                  "expiry": {
+                    "anyOf": [
+                      {
+                        "$ref": "#/definitions/Expiration"
+                      },
+                      {
+                        "type": "null"
+                      }
+                    ]
+                  },
+                  "new_owner": {
+                    "type": "string"
+                  }
+                },
+                "additionalProperties": false
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "Accept the pending ownership transfer.\n\nCan only be called by the pending owner.",
+            "type": "string",
+            "enum": [
+              "accept_ownership"
+            ]
+          },
+          {
+            "description": "Give up the contract's ownership and the possibility of appointing a new owner.\n\nCan only be invoked by the contract's current owner.\n\nAny existing pending ownership transfer is canceled.",
+            "type": "string",
+            "enum": [
+              "renounce_ownership"
+            ]
+          }
+        ]
+      },
       "Binary": {
         "description": "Binary is a wrapper around Vec<u8> to add base64 de/serialization with serde. It also adds some helper methods to help encode inline.\n\nThis is only needed as serde-json-{core,wasm} has a horrible encoding for Vec<u8>. See also <https://github.com/CosmWasm/cosmwasm/blob/main/docs/MESSAGE_TYPES.md>.",
         "type": "string"
@@ -264,9 +310,68 @@
           }
         ]
       },
+      "Expiration": {
+        "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+        "oneOf": [
+          {
+            "description": "AtHeight will expire when `env.block.height` >= height",
+            "type": "object",
+            "required": [
+              "at_height"
+            ],
+            "properties": {
+              "at_height": {
+                "type": "integer",
+                "format": "uint64",
+                "minimum": 0.0
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "AtTime will expire when `env.block.time` >= time",
+            "type": "object",
+            "required": [
+              "at_time"
+            ],
+            "properties": {
+              "at_time": {
+                "$ref": "#/definitions/Timestamp"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "Never will never expire. Used to express the empty variant",
+            "type": "object",
+            "required": [
+              "never"
+            ],
+            "properties": {
+              "never": {
+                "type": "object",
+                "additionalProperties": false
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
+      "Timestamp": {
+        "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+        "allOf": [
+          {
+            "$ref": "#/definitions/Uint64"
+          }
+        ]
+      },
       "Uint128": {
         "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```",
         "type": "string"
+      },
+      "Uint64": {
+        "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+        "type": "string"
       }
     }
   },
@@ -435,41 +540,34 @@
           }
         },
         "additionalProperties": false
-      }
-    ]
-  },
-  "migrate": {
-    "$schema": "http://json-schema.org/draft-07/schema#",
-    "title": "MigrateMsg",
-    "oneOf": [
+      },
       {
         "type": "object",
         "required": [
-          "from_beta"
+          "ownership"
         ],
         "properties": {
-          "from_beta": {
+          "ownership": {
             "type": "object",
-            "properties": {
-              "manager": {
-                "type": [
-                  "string",
-                  "null"
-                ]
-              }
-            },
             "additionalProperties": false
           }
         },
         "additionalProperties": false
-      },
+      }
+    ]
+  },
+  "migrate": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "MigrateMsg",
+    "oneOf": [
       {
+        "description": "Migrates the contract from version one to version two. This will remove the contract's current manager, and require a nomination -> acceptance flow for future ownership transfers.",
         "type": "object",
         "required": [
-          "from_compatible"
+          "from_v1"
         ],
         "properties": {
-          "from_compatible": {
+          "from_v1": {
             "type": "object",
             "additionalProperties": false
           }
@@ -586,26 +684,6 @@
         "token_address"
       ],
       "properties": {
-        "manager": {
-          "anyOf": [
-            {
-              "$ref": "#/definitions/Addr"
-            },
-            {
-              "type": "null"
-            }
-          ]
-        },
-        "owner": {
-          "anyOf": [
-            {
-              "$ref": "#/definitions/Addr"
-            },
-            {
-              "type": "null"
-            }
-          ]
-        },
         "token_address": {
           "$ref": "#/definitions/Addr"
         },
@@ -718,6 +796,113 @@
         }
       }
     },
+    "ownership": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Ownership_for_Addr",
+      "description": "The contract's ownership info",
+      "type": "object",
+      "properties": {
+        "owner": {
+          "description": "The contract's current owner. `None` if the ownership has been renounced.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Addr"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pending_expiry": {
+          "description": "The deadline for the pending owner to accept the ownership. `None` if there isn't a pending ownership transfer, or if a transfer exists and it doesn't have a deadline.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Expiration"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        },
+        "pending_owner": {
+          "description": "The account who has been proposed to take over the ownership. `None` if there isn't a pending ownership transfer.",
+          "anyOf": [
+            {
+              "$ref": "#/definitions/Addr"
+            },
+            {
+              "type": "null"
+            }
+          ]
+        }
+      },
+      "additionalProperties": false,
+      "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"
+        },
+        "Expiration": {
+          "description": "Expiration represents a point in time when some event happens. It can compare with a BlockInfo and will return is_expired() == true once the condition is hit (and for every block in the future)",
+          "oneOf": [
+            {
+              "description": "AtHeight will expire when `env.block.height` >= height",
+              "type": "object",
+              "required": [
+                "at_height"
+              ],
+              "properties": {
+                "at_height": {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "description": "AtTime will expire when `env.block.time` >= time",
+              "type": "object",
+              "required": [
+                "at_time"
+              ],
+              "properties": {
+                "at_time": {
+                  "$ref": "#/definitions/Timestamp"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "description": "Never will never expire. Used to express the empty variant",
+              "type": "object",
+              "required": [
+                "never"
+              ],
+              "properties": {
+                "never": {
+                  "type": "object",
+                  "additionalProperties": false
+                }
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "Timestamp": {
+          "description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Uint64"
+            }
+          ]
+        },
+        "Uint64": {
+          "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
+          "type": "string"
+        }
+      }
+    },
     "staked_balance_at_height": {
       "$schema": "http://json-schema.org/draft-07/schema#",
       "title": "StakedBalanceAtHeightResponse",
diff --git a/contracts/staking/cw20-stake/src/contract.rs b/contracts/staking/cw20-stake/src/contract.rs
index 46f975cc..ad4aea12 100644
--- a/contracts/staking/cw20-stake/src/contract.rs
+++ b/contracts/staking/cw20-stake/src/contract.rs
@@ -1,10 +1,9 @@
-use cosmwasm_schema::cw_serde;
 #[cfg(not(feature = "library"))]
 use cosmwasm_std::entry_point;
 
 use cosmwasm_std::{
-    from_binary, from_slice, to_binary, to_vec, Addr, Binary, Deps, DepsMut, Empty, Env,
-    MessageInfo, Response, StdError, StdResult, Uint128,
+    from_binary, to_binary, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Response,
+    StdError, StdResult, Uint128,
 };
 
 use cw20::{Cw20ReceiveMsg, TokenInfoResponse};
@@ -20,7 +19,7 @@ use crate::state::{
     Config, BALANCE, CLAIMS, CONFIG, HOOKS, MAX_CLAIMS, STAKED_BALANCES, STAKED_TOTAL,
 };
 use crate::ContractError;
-use cw2::set_contract_version;
+use cw2::{get_contract_version, set_contract_version, ContractVersion};
 pub use cw20_base::allowances::{
     execute_burn_from, execute_decrease_allowance, execute_increase_allowance, execute_send_from,
     execute_transfer_from, query_allowance,
@@ -62,16 +61,7 @@ pub fn instantiate(
     _info: MessageInfo,
     msg: InstantiateMsg,
 ) -> Result<Response<Empty>, ContractError> {
-    let owner = match msg.owner {
-        Some(owner) => Some(deps.api.addr_validate(owner.as_str())?),
-        None => None,
-    };
-
-    let manager = match msg.manager {
-        Some(manager) => Some(deps.api.addr_validate(manager.as_str())?),
-        None => None,
-    };
-
+    cw_ownable::initialize_owner(deps.storage, deps.api, msg.owner.as_deref())?;
     // Smoke test that the provided cw20 contract responds to a
     // token_info query. It is not possible to determine if the
     // contract implements the entire cw20 standard and runtime,
@@ -85,8 +75,6 @@ pub fn instantiate(
 
     validate_duration(msg.unstaking_duration)?;
     let config = Config {
-        owner,
-        manager,
         token_address,
         unstaking_duration: msg.unstaking_duration,
     };
@@ -115,60 +103,34 @@ pub fn execute(
         ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg),
         ExecuteMsg::Unstake { amount } => execute_unstake(deps, env, info, amount),
         ExecuteMsg::Claim {} => execute_claim(deps, env, info),
-        ExecuteMsg::UpdateConfig {
-            owner,
-            manager,
-            duration,
-        } => execute_update_config(info, deps, owner, manager, duration),
+        ExecuteMsg::UpdateConfig { duration } => execute_update_config(info, deps, duration),
         ExecuteMsg::AddHook { addr } => execute_add_hook(deps, env, info, addr),
         ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, env, info, addr),
+        ExecuteMsg::UpdateOwnership(action) => execute_update_owner(deps, info, env, action),
     }
 }
 
 pub fn execute_update_config(
     info: MessageInfo,
     deps: DepsMut,
-    new_owner: Option<String>,
-    new_manager: Option<String>,
     duration: Option<Duration>,
 ) -> Result<Response, ContractError> {
-    let new_owner = new_owner
-        .map(|new_owner| deps.api.addr_validate(&new_owner))
-        .transpose()?;
-    let new_manager = new_manager
-        .map(|new_manager| deps.api.addr_validate(&new_manager))
-        .transpose()?;
-    let mut config: Config = CONFIG.load(deps.storage)?;
-    if Some(info.sender.clone()) != config.owner && Some(info.sender.clone()) != config.manager {
-        return Err(ContractError::Unauthorized {});
-    };
-    if Some(info.sender) != config.owner && new_owner != config.owner {
-        return Err(ContractError::OnlyOwnerCanChangeOwner {});
-    };
+    cw_ownable::assert_owner(deps.storage, &info.sender)?;
 
     validate_duration(duration)?;
 
-    config.owner = new_owner;
-    config.manager = new_manager;
-
-    config.unstaking_duration = duration;
+    CONFIG.update(deps.storage, |mut config| -> Result<Config, StdError> {
+        config.unstaking_duration = duration;
+        Ok(config)
+    })?;
 
-    CONFIG.save(deps.storage, &config)?;
     Ok(Response::new()
         .add_attribute("action", "update_config")
         .add_attribute(
-            "owner",
-            config
-                .owner
-                .map(|a| a.to_string())
-                .unwrap_or_else(|| "None".to_string()),
-        )
-        .add_attribute(
-            "manager",
-            config
-                .manager
-                .map(|a| a.to_string())
-                .unwrap_or_else(|| "None".to_string()),
+            "unstaking_duration",
+            duration
+                .map(|d| format!("{d}"))
+                .unwrap_or_else(|| "none".to_string()),
         ))
 }
 
@@ -290,7 +252,7 @@ pub fn execute_unstake(
         }
         Some(duration) => {
             let outstanding_claims = CLAIMS.query_claims(deps.as_ref(), &info.sender)?.claims;
-            if outstanding_claims.len() >= MAX_CLAIMS as usize {
+            if outstanding_claims.len() + 1 > MAX_CLAIMS as usize {
                 return Err(ContractError::TooManyClaims {});
             }
 
@@ -357,12 +319,9 @@ pub fn execute_add_hook(
     info: MessageInfo,
     addr: String,
 ) -> Result<Response, ContractError> {
-    let addr = deps.api.addr_validate(&addr)?;
-    let config: Config = CONFIG.load(deps.storage)?;
-    if config.owner != Some(info.sender.clone()) && config.manager != Some(info.sender) {
-        return Err(ContractError::Unauthorized {});
-    };
-    HOOKS.add_hook(deps.storage, addr.clone())?;
+    cw_ownable::assert_owner(deps.storage, &info.sender)?;
+    let hook = deps.api.addr_validate(&addr)?;
+    HOOKS.add_hook(deps.storage, hook)?;
     Ok(Response::new()
         .add_attribute("action", "add_hook")
         .add_attribute("hook", addr))
@@ -374,17 +333,24 @@ pub fn execute_remove_hook(
     info: MessageInfo,
     addr: String,
 ) -> Result<Response, ContractError> {
-    let addr = deps.api.addr_validate(&addr)?;
-    let config: Config = CONFIG.load(deps.storage)?;
-    if config.owner != Some(info.sender.clone()) && config.manager != Some(info.sender) {
-        return Err(ContractError::Unauthorized {});
-    };
-    HOOKS.remove_hook(deps.storage, addr.clone())?;
+    cw_ownable::assert_owner(deps.storage, &info.sender)?;
+    let hook = deps.api.addr_validate(&addr)?;
+    HOOKS.remove_hook(deps.storage, hook)?;
     Ok(Response::new()
         .add_attribute("action", "remove_hook")
         .add_attribute("hook", addr))
 }
 
+pub fn execute_update_owner(
+    deps: DepsMut,
+    info: MessageInfo,
+    env: Env,
+    action: cw_ownable::Action,
+) -> Result<Response, ContractError> {
+    let ownership = cw_ownable::update_ownership(deps, &env.block, &info.sender, action)?;
+    Ok(Response::default().add_attributes(ownership.into_attributes()))
+}
+
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
     match msg {
@@ -402,6 +368,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
         QueryMsg::ListStakers { start_after, limit } => {
             query_list_stakers(deps, start_after, limit)
         }
+        QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?),
     }
 }
 
@@ -506,34 +473,33 @@ pub fn query_list_stakers(
 
 #[cfg_attr(not(feature = "library"), entry_point)]
 pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
-    // Set contract to version to latest
-    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
-
-    #[cw_serde]
-    struct BetaConfig {
-        pub admin: Addr,
-        pub token_address: Addr,
-        pub unstaking_duration: Option<Duration>,
-    }
+    use cw20_stake_v1 as v1;
 
+    let ContractVersion { version, .. } = get_contract_version(deps.storage)?;
+    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
     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,
+        MigrateMsg::FromV1 {} => {
+            if version == CONTRACT_VERSION {
+                // Migrating from a version to a new one implies that
+                // the new version must be different.
+                return Err(ContractError::AlreadyMigrated {});
+            }
+            let config = v1::state::CONFIG.load(deps.storage)?;
+            cw_ownable::initialize_owner(
+                deps.storage,
+                deps.api,
+                config.owner.map(|a| a.into_string()).as_deref(),
+            )?;
+            let config = Config {
+                token_address: config.token_address,
+                unstaking_duration: config.unstaking_duration.map(|duration| match duration {
+                    cw_utils_v1::Duration::Time(t) => Duration::Time(t),
+                    cw_utils_v1::Duration::Height(h) => Duration::Height(h),
+                }),
             };
-            deps.storage.set(b"config", &to_vec(&new_config)?);
+            CONFIG.save(deps.storage, &config)?;
+
             Ok(Response::default())
         }
-        MigrateMsg::FromCompatible {} => Ok(Response::default()),
     }
 }
diff --git a/contracts/staking/cw20-stake/src/error.rs b/contracts/staking/cw20-stake/src/error.rs
index 64a0fc3e..cd7140a2 100644
--- a/contracts/staking/cw20-stake/src/error.rs
+++ b/contracts/staking/cw20-stake/src/error.rs
@@ -3,10 +3,15 @@ use thiserror::Error;
 
 #[derive(Error, Debug, PartialEq)]
 pub enum ContractError {
-    #[error("{0}")]
+    #[error(transparent)]
     Std(#[from] StdError),
-    #[error("{0}")]
+    #[error(transparent)]
     Cw20Error(#[from] cw20_base::ContractError),
+    #[error(transparent)]
+    Ownership(#[from] cw_ownable::OwnershipError),
+    #[error(transparent)]
+    HookError(#[from] cw_controllers::HookError),
+
     #[error("Provided cw20 errored in response to TokenInfo query")]
     InvalidCw20 {},
     #[error("Nothing to claim")]
@@ -19,16 +24,10 @@ pub enum ContractError {
     ImpossibleUnstake {},
     #[error("Invalid token")]
     InvalidToken { received: Addr, expected: Addr },
-    #[error("Unauthorized")]
-    Unauthorized {},
     #[error("Too many outstanding claims. Claim some tokens before unstaking more.")]
     TooManyClaims {},
-    #[error("No admin configured")]
-    NoAdminConfigured {},
-    #[error("{0}")]
-    HookError(#[from] cw_controllers::HookError),
-    #[error("Only owner can change owner")]
-    OnlyOwnerCanChangeOwner {},
     #[error("Invalid unstaking duration, unstaking duration cannot be 0")]
     InvalidUnstakingDuration {},
+    #[error("can not migrate. current version is up to date")]
+    AlreadyMigrated {},
 }
diff --git a/contracts/staking/cw20-stake/src/msg.rs b/contracts/staking/cw20-stake/src/msg.rs
index f952f6c8..bd7a8ce8 100644
--- a/contracts/staking/cw20-stake/src/msg.rs
+++ b/contracts/staking/cw20-stake/src/msg.rs
@@ -1,37 +1,33 @@
 use cosmwasm_schema::{cw_serde, QueryResponses};
 use cosmwasm_std::Uint128;
 use cw20::Cw20ReceiveMsg;
-pub use cw_controllers::ClaimsResponse;
+
 use cw_utils::Duration;
 
+use cw_ownable::cw_ownable;
+
+pub use cw_controllers::ClaimsResponse;
+// so that consumers don't need a cw_ownable dependency to consume
+// this contract's queries.
+pub use cw_ownable::Ownership;
+
 #[cw_serde]
 pub struct InstantiateMsg {
     // Owner can update all configs including changing the owner. This will generally be a DAO.
     pub owner: Option<String>,
-    // Manager can update all configs except changing the owner. This will generally be an operations multisig for a DAO.
-    pub manager: Option<String>,
     pub token_address: String,
     pub unstaking_duration: Option<Duration>,
 }
 
+#[cw_ownable]
 #[cw_serde]
 pub enum ExecuteMsg {
     Receive(Cw20ReceiveMsg),
-    Unstake {
-        amount: Uint128,
-    },
+    Unstake { amount: Uint128 },
     Claim {},
-    UpdateConfig {
-        owner: Option<String>,
-        manager: Option<String>,
-        duration: Option<Duration>,
-    },
-    AddHook {
-        addr: String,
-    },
-    RemoveHook {
-        addr: String,
-    },
+    UpdateConfig { duration: Option<Duration> },
+    AddHook { addr: String },
+    RemoveHook { addr: String },
 }
 
 #[cw_serde]
@@ -65,12 +61,16 @@ pub enum QueryMsg {
         start_after: Option<String>,
         limit: Option<u32>,
     },
+    #[returns(::cw_ownable::Ownership::<::cosmwasm_std::Addr>)]
+    Ownership {},
 }
 
 #[cw_serde]
 pub enum MigrateMsg {
-    FromBeta { manager: Option<String> },
-    FromCompatible {},
+    /// Migrates the contract from version one to version two. This
+    /// will remove the contract's current manager, and require a
+    /// nomination -> acceptance flow for future ownership transfers.
+    FromV1 {},
 }
 
 #[cw_serde]
diff --git a/contracts/staking/cw20-stake/src/state.rs b/contracts/staking/cw20-stake/src/state.rs
index 94ec4acc..2ebb1c88 100644
--- a/contracts/staking/cw20-stake/src/state.rs
+++ b/contracts/staking/cw20-stake/src/state.rs
@@ -7,13 +7,12 @@ use cw_utils::Duration;
 
 #[cw_serde]
 pub struct Config {
-    pub owner: Option<Addr>,
-    pub manager: Option<Addr>,
     pub token_address: Addr,
     pub unstaking_duration: Option<Duration>,
 }
 
-pub const CONFIG: Item<Config> = Item::new("config");
+// `"config"` key stores v1 configuration.
+pub const CONFIG: Item<Config> = Item::new("config_v2");
 
 pub const STAKED_BALANCES: SnapshotMap<&Addr, Uint128> = SnapshotMap::new(
     "staked_balances",
diff --git a/contracts/voting/dao-voting-cw20-staked/src/contract.rs b/contracts/voting/dao-voting-cw20-staked/src/contract.rs
index c6590d7b..4ad835db 100644
--- a/contracts/voting/dao-voting-cw20-staked/src/contract.rs
+++ b/contracts/voting/dao-voting-cw20-staked/src/contract.rs
@@ -95,7 +95,6 @@ pub fn instantiate(
                             owner: Some(info.sender.to_string()),
                             unstaking_duration,
                             token_address: address.to_string(),
-                            manager: None,
                         })?,
                     };
                     let msg = SubMsg::reply_on_success(msg, INSTANTIATE_STAKING_REPLY_ID);
@@ -413,7 +412,6 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result<Response, ContractEr
                             owner: Some(dao.to_string()),
                             unstaking_duration,
                             token_address: token.to_string(),
-                            manager: None,
                         })?,
                     };
                     let msg = SubMsg::reply_on_success(msg, INSTANTIATE_STAKING_REPLY_ID);
diff --git a/typescript/contracts/cw20-stake-external-rewards/Cw20StakeExternalRewards.client.ts b/typescript/contracts/cw20-stake-external-rewards/Cw20StakeExternalRewards.client.ts
index 28fe9fa8..3966d25c 100644
--- a/typescript/contracts/cw20-stake-external-rewards/Cw20StakeExternalRewards.client.ts
+++ b/typescript/contracts/cw20-stake-external-rewards/Cw20StakeExternalRewards.client.ts
@@ -6,7 +6,7 @@
 
 import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
 import { Coin, StdFee } from "@cosmjs/amino";
-import { Denom, Addr, InstantiateMsg, ExecuteMsg, StakeChangedHookMsg, Uint128, Binary, Cw20ReceiveMsg, QueryMsg, MigrateMsg, PendingRewardsResponse, InfoResponse, Config, RewardConfig } from "./Cw20StakeExternalRewards.types";
+import { Denom, Addr, InstantiateMsg, ExecuteMsg, StakeChangedHookMsg, Uint128, Binary, Action, Expiration, Timestamp, Uint64, Cw20ReceiveMsg, QueryMsg, MigrateMsg, PendingRewardsResponse, InfoResponse, Config, RewardConfig, OwnershipForAddr } from "./Cw20StakeExternalRewards.types";
 export interface Cw20StakeExternalRewardsReadOnlyInterface {
   contractAddress: string;
   info: () => Promise<InfoResponse>;
@@ -15,6 +15,7 @@ export interface Cw20StakeExternalRewardsReadOnlyInterface {
   }: {
     address: string;
   }) => Promise<PendingRewardsResponse>;
+  ownership: () => Promise<OwnershipForAddr>;
 }
 export class Cw20StakeExternalRewardsQueryClient implements Cw20StakeExternalRewardsReadOnlyInterface {
   client: CosmWasmClient;
@@ -25,6 +26,7 @@ export class Cw20StakeExternalRewardsQueryClient implements Cw20StakeExternalRew
     this.contractAddress = contractAddress;
     this.info = this.info.bind(this);
     this.getPendingRewards = this.getPendingRewards.bind(this);
+    this.ownership = this.ownership.bind(this);
   }
 
   info = async (): Promise<InfoResponse> => {
@@ -43,6 +45,11 @@ export class Cw20StakeExternalRewardsQueryClient implements Cw20StakeExternalRew
       }
     });
   };
+  ownership = async (): Promise<OwnershipForAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      ownership: {}
+    });
+  };
 }
 export interface Cw20StakeExternalRewardsInterface extends Cw20StakeExternalRewardsReadOnlyInterface {
   contractAddress: string;
@@ -64,16 +71,7 @@ export interface Cw20StakeExternalRewardsInterface extends Cw20StakeExternalRewa
   }: {
     newDuration: number;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
-  updateOwner: ({
-    newOwner
-  }: {
-    newOwner?: string;
-  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
-  updateManager: ({
-    newManager
-  }: {
-    newManager?: string;
-  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  updateOwnership: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
 }
 export class Cw20StakeExternalRewardsClient extends Cw20StakeExternalRewardsQueryClient implements Cw20StakeExternalRewardsInterface {
   client: SigningCosmWasmClient;
@@ -90,8 +88,7 @@ export class Cw20StakeExternalRewardsClient extends Cw20StakeExternalRewardsQuer
     this.receive = this.receive.bind(this);
     this.fund = this.fund.bind(this);
     this.updateRewardDuration = this.updateRewardDuration.bind(this);
-    this.updateOwner = this.updateOwner.bind(this);
-    this.updateManager = this.updateManager.bind(this);
+    this.updateOwnership = this.updateOwnership.bind(this);
   }
 
   stakeChangeHook = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
@@ -137,26 +134,9 @@ export class Cw20StakeExternalRewardsClient extends Cw20StakeExternalRewardsQuer
       }
     }, fee, memo, funds);
   };
-  updateOwner = async ({
-    newOwner
-  }: {
-    newOwner?: string;
-  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
-    return await this.client.execute(this.sender, this.contractAddress, {
-      update_owner: {
-        new_owner: newOwner
-      }
-    }, fee, memo, funds);
-  };
-  updateManager = async ({
-    newManager
-  }: {
-    newManager?: string;
-  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+  updateOwnership = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
     return await this.client.execute(this.sender, this.contractAddress, {
-      update_manager: {
-        new_manager: newManager
-      }
+      update_ownership: {}
     }, fee, memo, funds);
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/cw20-stake-external-rewards/Cw20StakeExternalRewards.types.ts b/typescript/contracts/cw20-stake-external-rewards/Cw20StakeExternalRewards.types.ts
index 54026120..dabcbbe9 100644
--- a/typescript/contracts/cw20-stake-external-rewards/Cw20StakeExternalRewards.types.ts
+++ b/typescript/contracts/cw20-stake-external-rewards/Cw20StakeExternalRewards.types.ts
@@ -11,7 +11,6 @@ export type Denom = {
 };
 export type Addr = string;
 export interface InstantiateMsg {
-  manager?: string | null;
   owner?: string | null;
   reward_duration: number;
   reward_token: Denom;
@@ -30,13 +29,7 @@ export type ExecuteMsg = {
     new_duration: number;
   };
 } | {
-  update_owner: {
-    new_owner?: string | null;
-  };
-} | {
-  update_manager: {
-    new_manager?: string | null;
-  };
+  update_ownership: Action;
 };
 export type StakeChangedHookMsg = {
   stake: {
@@ -51,6 +44,21 @@ export type StakeChangedHookMsg = {
 };
 export type Uint128 = string;
 export type Binary = string;
+export type Action = {
+  transfer_ownership: {
+    expiry?: Expiration | null;
+    new_owner: string;
+  };
+} | "accept_ownership" | "renounce_ownership";
+export type Expiration = {
+  at_height: number;
+} | {
+  at_time: Timestamp;
+} | {
+  never: {};
+};
+export type Timestamp = Uint64;
+export type Uint64 = string;
 export interface Cw20ReceiveMsg {
   amount: Uint128;
   msg: Binary;
@@ -62,8 +70,12 @@ export type QueryMsg = {
   get_pending_rewards: {
     address: string;
   };
+} | {
+  ownership: {};
+};
+export type MigrateMsg = {
+  from_v1: {};
 };
-export interface MigrateMsg {}
 export interface PendingRewardsResponse {
   address: string;
   denom: Denom;
@@ -75,8 +87,6 @@ export interface InfoResponse {
   reward: RewardConfig;
 }
 export interface Config {
-  manager?: Addr | null;
-  owner?: Addr | null;
   reward_token: Denom;
   staking_contract: Addr;
 }
@@ -84,4 +94,9 @@ export interface RewardConfig {
   period_finish: number;
   reward_duration: number;
   reward_rate: Uint128;
+}
+export interface OwnershipForAddr {
+  owner?: Addr | null;
+  pending_expiry?: Expiration | null;
+  pending_owner?: Addr | null;
 }
\ No newline at end of file
diff --git a/typescript/contracts/cw20-stake-reward-distributor/Cw20StakeRewardDistributor.client.ts b/typescript/contracts/cw20-stake-reward-distributor/Cw20StakeRewardDistributor.client.ts
index ba49f31f..ef0183d9 100644
--- a/typescript/contracts/cw20-stake-reward-distributor/Cw20StakeRewardDistributor.client.ts
+++ b/typescript/contracts/cw20-stake-reward-distributor/Cw20StakeRewardDistributor.client.ts
@@ -6,10 +6,11 @@
 
 import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
 import { Coin, StdFee } from "@cosmjs/amino";
-import { Uint128, InstantiateMsg, ExecuteMsg, QueryMsg, MigrateMsg, Addr, InfoResponse, Config } from "./Cw20StakeRewardDistributor.types";
+import { Uint128, InstantiateMsg, ExecuteMsg, Action, Expiration, Timestamp, Uint64, QueryMsg, MigrateMsg, Addr, InfoResponse, Config, OwnershipForAddr } from "./Cw20StakeRewardDistributor.types";
 export interface Cw20StakeRewardDistributorReadOnlyInterface {
   contractAddress: string;
   info: () => Promise<InfoResponse>;
+  ownership: () => Promise<OwnershipForAddr>;
 }
 export class Cw20StakeRewardDistributorQueryClient implements Cw20StakeRewardDistributorReadOnlyInterface {
   client: CosmWasmClient;
@@ -19,6 +20,7 @@ export class Cw20StakeRewardDistributorQueryClient implements Cw20StakeRewardDis
     this.client = client;
     this.contractAddress = contractAddress;
     this.info = this.info.bind(this);
+    this.ownership = this.ownership.bind(this);
   }
 
   info = async (): Promise<InfoResponse> => {
@@ -26,23 +28,27 @@ export class Cw20StakeRewardDistributorQueryClient implements Cw20StakeRewardDis
       info: {}
     });
   };
+  ownership = async (): Promise<OwnershipForAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      ownership: {}
+    });
+  };
 }
 export interface Cw20StakeRewardDistributorInterface extends Cw20StakeRewardDistributorReadOnlyInterface {
   contractAddress: string;
   sender: string;
   updateConfig: ({
-    owner,
     rewardRate,
     rewardToken,
     stakingAddr
   }: {
-    owner: string;
     rewardRate: Uint128;
     rewardToken: string;
     stakingAddr: string;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   distribute: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   withdraw: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  updateOwnership: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
 }
 export class Cw20StakeRewardDistributorClient extends Cw20StakeRewardDistributorQueryClient implements Cw20StakeRewardDistributorInterface {
   client: SigningCosmWasmClient;
@@ -57,22 +63,20 @@ export class Cw20StakeRewardDistributorClient extends Cw20StakeRewardDistributor
     this.updateConfig = this.updateConfig.bind(this);
     this.distribute = this.distribute.bind(this);
     this.withdraw = this.withdraw.bind(this);
+    this.updateOwnership = this.updateOwnership.bind(this);
   }
 
   updateConfig = async ({
-    owner,
     rewardRate,
     rewardToken,
     stakingAddr
   }: {
-    owner: string;
     rewardRate: Uint128;
     rewardToken: string;
     stakingAddr: string;
   }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
     return await this.client.execute(this.sender, this.contractAddress, {
       update_config: {
-        owner,
         reward_rate: rewardRate,
         reward_token: rewardToken,
         staking_addr: stakingAddr
@@ -89,4 +93,9 @@ export class Cw20StakeRewardDistributorClient extends Cw20StakeRewardDistributor
       withdraw: {}
     }, fee, memo, funds);
   };
+  updateOwnership = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      update_ownership: {}
+    }, fee, memo, funds);
+  };
 }
\ No newline at end of file
diff --git a/typescript/contracts/cw20-stake-reward-distributor/Cw20StakeRewardDistributor.types.ts b/typescript/contracts/cw20-stake-reward-distributor/Cw20StakeRewardDistributor.types.ts
index eea54558..62da8b2a 100644
--- a/typescript/contracts/cw20-stake-reward-distributor/Cw20StakeRewardDistributor.types.ts
+++ b/typescript/contracts/cw20-stake-reward-distributor/Cw20StakeRewardDistributor.types.ts
@@ -13,7 +13,6 @@ export interface InstantiateMsg {
 }
 export type ExecuteMsg = {
   update_config: {
-    owner: string;
     reward_rate: Uint128;
     reward_token: string;
     staking_addr: string;
@@ -22,11 +21,32 @@ export type ExecuteMsg = {
   distribute: {};
 } | {
   withdraw: {};
+} | {
+  update_ownership: Action;
+};
+export type Action = {
+  transfer_ownership: {
+    expiry?: Expiration | null;
+    new_owner: string;
+  };
+} | "accept_ownership" | "renounce_ownership";
+export type Expiration = {
+  at_height: number;
+} | {
+  at_time: Timestamp;
+} | {
+  never: {};
 };
+export type Timestamp = Uint64;
+export type Uint64 = string;
 export type QueryMsg = {
   info: {};
+} | {
+  ownership: {};
+};
+export type MigrateMsg = {
+  from_v1: {};
 };
-export interface MigrateMsg {}
 export type Addr = string;
 export interface InfoResponse {
   balance: Uint128;
@@ -34,8 +54,12 @@ export interface InfoResponse {
   last_payment_block: number;
 }
 export interface Config {
-  owner: Addr;
   reward_rate: Uint128;
   reward_token: Addr;
   staking_addr: Addr;
+}
+export interface OwnershipForAddr {
+  owner?: Addr | null;
+  pending_expiry?: Expiration | null;
+  pending_owner?: Addr | null;
 }
\ No newline at end of file
diff --git a/typescript/contracts/cw20-stake/Cw20Stake.client.ts b/typescript/contracts/cw20-stake/Cw20Stake.client.ts
index fb78458a..9240cadd 100644
--- a/typescript/contracts/cw20-stake/Cw20Stake.client.ts
+++ b/typescript/contracts/cw20-stake/Cw20Stake.client.ts
@@ -6,7 +6,7 @@
 
 import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
 import { Coin, StdFee } from "@cosmjs/amino";
-import { Duration, InstantiateMsg, ExecuteMsg, Uint128, Binary, Cw20ReceiveMsg, QueryMsg, MigrateMsg, Expiration, Timestamp, Uint64, ClaimsResponse, Claim, Addr, Config, GetHooksResponse, ListStakersResponse, StakerBalanceResponse, StakedBalanceAtHeightResponse, StakedValueResponse, TotalStakedAtHeightResponse, TotalValueResponse } from "./Cw20Stake.types";
+import { Duration, InstantiateMsg, ExecuteMsg, Uint128, Binary, Action, Expiration, Timestamp, Uint64, Cw20ReceiveMsg, QueryMsg, MigrateMsg, ClaimsResponse, Claim, Addr, Config, GetHooksResponse, ListStakersResponse, StakerBalanceResponse, OwnershipForAddr, StakedBalanceAtHeightResponse, StakedValueResponse, TotalStakedAtHeightResponse, TotalValueResponse } from "./Cw20Stake.types";
 export interface Cw20StakeReadOnlyInterface {
   contractAddress: string;
   stakedBalanceAtHeight: ({
@@ -41,6 +41,7 @@ export interface Cw20StakeReadOnlyInterface {
     limit?: number;
     startAfter?: string;
   }) => Promise<ListStakersResponse>;
+  ownership: () => Promise<OwnershipForAddr>;
 }
 export class Cw20StakeQueryClient implements Cw20StakeReadOnlyInterface {
   client: CosmWasmClient;
@@ -57,6 +58,7 @@ export class Cw20StakeQueryClient implements Cw20StakeReadOnlyInterface {
     this.claims = this.claims.bind(this);
     this.getHooks = this.getHooks.bind(this);
     this.listStakers = this.listStakers.bind(this);
+    this.ownership = this.ownership.bind(this);
   }
 
   stakedBalanceAtHeight = async ({
@@ -135,6 +137,11 @@ export class Cw20StakeQueryClient implements Cw20StakeReadOnlyInterface {
       }
     });
   };
+  ownership = async (): Promise<OwnershipForAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      ownership: {}
+    });
+  };
 }
 export interface Cw20StakeInterface extends Cw20StakeReadOnlyInterface {
   contractAddress: string;
@@ -155,13 +162,9 @@ export interface Cw20StakeInterface extends Cw20StakeReadOnlyInterface {
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   claim: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   updateConfig: ({
-    duration,
-    manager,
-    owner
+    duration
   }: {
     duration?: Duration;
-    manager?: string;
-    owner?: string;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
   addHook: ({
     addr
@@ -173,6 +176,7 @@ export interface Cw20StakeInterface extends Cw20StakeReadOnlyInterface {
   }: {
     addr: string;
   }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  updateOwnership: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
 }
 export class Cw20StakeClient extends Cw20StakeQueryClient implements Cw20StakeInterface {
   client: SigningCosmWasmClient;
@@ -190,6 +194,7 @@ export class Cw20StakeClient extends Cw20StakeQueryClient implements Cw20StakeIn
     this.updateConfig = this.updateConfig.bind(this);
     this.addHook = this.addHook.bind(this);
     this.removeHook = this.removeHook.bind(this);
+    this.updateOwnership = this.updateOwnership.bind(this);
   }
 
   receive = async ({
@@ -226,19 +231,13 @@ export class Cw20StakeClient extends Cw20StakeQueryClient implements Cw20StakeIn
     }, fee, memo, funds);
   };
   updateConfig = async ({
-    duration,
-    manager,
-    owner
+    duration
   }: {
     duration?: Duration;
-    manager?: string;
-    owner?: string;
   }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
     return await this.client.execute(this.sender, this.contractAddress, {
       update_config: {
-        duration,
-        manager,
-        owner
+        duration
       }
     }, fee, memo, funds);
   };
@@ -264,4 +263,9 @@ export class Cw20StakeClient extends Cw20StakeQueryClient implements Cw20StakeIn
       }
     }, fee, memo, funds);
   };
+  updateOwnership = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      update_ownership: {}
+    }, fee, memo, funds);
+  };
 }
\ No newline at end of file
diff --git a/typescript/contracts/cw20-stake/Cw20Stake.types.ts b/typescript/contracts/cw20-stake/Cw20Stake.types.ts
index 4cf8fc17..ff601b01 100644
--- a/typescript/contracts/cw20-stake/Cw20Stake.types.ts
+++ b/typescript/contracts/cw20-stake/Cw20Stake.types.ts
@@ -10,7 +10,6 @@ export type Duration = {
   time: number;
 };
 export interface InstantiateMsg {
-  manager?: string | null;
   owner?: string | null;
   token_address: string;
   unstaking_duration?: Duration | null;
@@ -26,8 +25,6 @@ export type ExecuteMsg = {
 } | {
   update_config: {
     duration?: Duration | null;
-    manager?: string | null;
-    owner?: string | null;
   };
 } | {
   add_hook: {
@@ -37,9 +34,26 @@ export type ExecuteMsg = {
   remove_hook: {
     addr: string;
   };
+} | {
+  update_ownership: Action;
 };
 export type Uint128 = string;
 export type Binary = string;
+export type Action = {
+  transfer_ownership: {
+    expiry?: Expiration | null;
+    new_owner: string;
+  };
+} | "accept_ownership" | "renounce_ownership";
+export type Expiration = {
+  at_height: number;
+} | {
+  at_time: Timestamp;
+} | {
+  never: {};
+};
+export type Timestamp = Uint64;
+export type Uint64 = string;
 export interface Cw20ReceiveMsg {
   amount: Uint128;
   msg: Binary;
@@ -73,23 +87,12 @@ export type QueryMsg = {
     limit?: number | null;
     start_after?: string | null;
   };
-};
-export type MigrateMsg = {
-  from_beta: {
-    manager?: string | null;
-  };
 } | {
-  from_compatible: {};
+  ownership: {};
 };
-export type Expiration = {
-  at_height: number;
-} | {
-  at_time: Timestamp;
-} | {
-  never: {};
+export type MigrateMsg = {
+  from_v1: {};
 };
-export type Timestamp = Uint64;
-export type Uint64 = string;
 export interface ClaimsResponse {
   claims: Claim[];
 }
@@ -99,8 +102,6 @@ export interface Claim {
 }
 export type Addr = string;
 export interface Config {
-  manager?: Addr | null;
-  owner?: Addr | null;
   token_address: Addr;
   unstaking_duration?: Duration | null;
 }
@@ -114,6 +115,11 @@ export interface StakerBalanceResponse {
   address: string;
   balance: Uint128;
 }
+export interface OwnershipForAddr {
+  owner?: Addr | null;
+  pending_expiry?: Expiration | null;
+  pending_owner?: Addr | null;
+}
 export interface StakedBalanceAtHeightResponse {
   balance: Uint128;
   height: number;
diff --git a/typescript/contracts/cw20-stake/bundle.ts b/typescript/contracts/cw20-stake/bundle.ts
index 2f175cdf..71a400ed 100644
--- a/typescript/contracts/cw20-stake/bundle.ts
+++ b/typescript/contracts/cw20-stake/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _22 from "./Cw20Stake.types";
-import * as _23 from "./Cw20Stake.client";
+import * as _24 from "./Cw20Stake.types";
+import * as _25 from "./Cw20Stake.client";
 export namespace contracts {
-  export const Cw20Stake = { ..._22,
-    ..._23
+  export const Cw20Stake = { ..._24,
+    ..._25
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-voting-cw20-staked/bundle.ts b/typescript/contracts/dao-voting-cw20-staked/bundle.ts
index 57d8db01..aa94fc3b 100644
--- a/typescript/contracts/dao-voting-cw20-staked/bundle.ts
+++ b/typescript/contracts/dao-voting-cw20-staked/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _24 from "./DaoVotingCw20Staked.types";
-import * as _25 from "./DaoVotingCw20Staked.client";
+import * as _22 from "./DaoVotingCw20Staked.types";
+import * as _23 from "./DaoVotingCw20Staked.client";
 export namespace contracts {
-  export const DaoVotingCw20Staked = { ..._24,
-    ..._25
+  export const DaoVotingCw20Staked = { ..._22,
+    ..._23
   };
 }
\ No newline at end of file


Test patch:
diff --git a/ci/integration-tests/src/tests/cw_core_test.rs b/ci/integration-tests/src/tests/cw_core_test.rs
index 662d5793..92db6353 100644
--- a/ci/integration-tests/src/tests/cw_core_test.rs
+++ b/ci/integration-tests/src/tests/cw_core_test.rs
@@ -299,13 +299,23 @@ fn instantiate_with_admin(chain: &mut Chain) {
         .query("cw20_stake", &cw20_stake::msg::QueryMsg::GetConfig {})
         .unwrap();
     let config_res: cw20_stake::state::Config = res.data().unwrap();
+    assert_eq!(config_res.unstaking_duration, Some(Duration::Time(1209600)));
+
+    let res = chain
+        .orc
+        .query("cw20_stake", &cw20_stake::msg::QueryMsg::Ownership {})
+        .unwrap();
+    let ownership: cw20_stake::msg::Ownership<Addr> = res.data().unwrap();
     assert_eq!(
-        config_res.owner,
-        Some(Addr::unchecked(
-            chain.orc.contract_map.address("dao_core").unwrap()
-        ))
+        ownership,
+        cw20_stake::msg::Ownership::<Addr> {
+            owner: Some(Addr::unchecked(
+                chain.orc.contract_map.address("dao_core").unwrap()
+            )),
+            pending_owner: None,
+            pending_expiry: None
+        }
     );
-    assert_eq!(config_res.manager, None);
 
     let res = &chain
         .orc
diff --git a/contracts/staking/cw20-stake-reward-distributor/src/tests.rs b/contracts/staking/cw20-stake-reward-distributor/src/tests.rs
index ab1d11cd..61056d39 100644
--- a/contracts/staking/cw20-stake-reward-distributor/src/tests.rs
+++ b/contracts/staking/cw20-stake-reward-distributor/src/tests.rs
@@ -1,19 +1,18 @@
 use crate::{
-    contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION},
     msg::{ExecuteMsg, InfoResponse, InstantiateMsg, MigrateMsg, QueryMsg},
     state::Config,
     ContractError,
 };
-use cosmwasm_std::{
-    testing::{mock_dependencies, mock_env},
-    Addr, Empty, Uint128,
-};
+
+use cw20_stake_reward_distributor_v1 as v1;
+
+use cosmwasm_std::{to_binary, Addr, Empty, Uint128, WasmMsg};
 use cw20::Cw20Coin;
-use cw_multi_test::{App, Contract, ContractWrapper, Executor};
+use cw_multi_test::{next_block, App, Contract, ContractWrapper, Executor};
+use cw_ownable::{Action, Expiration, Ownership, OwnershipError};
 
 const OWNER: &str = "owner";
 const OWNER2: &str = "owner2";
-const MANAGER: &str = "manager";
 
 pub fn cw20_contract() -> Box<dyn Contract<Empty>> {
     let contract = ContractWrapper::new(
@@ -38,6 +37,16 @@ fn distributor_contract() -> Box<dyn Contract<Empty>> {
         crate::contract::execute,
         crate::contract::instantiate,
         crate::contract::query,
+    )
+    .with_migrate(crate::contract::migrate);
+    Box::new(contract)
+}
+
+fn distributor_contract_v1() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        v1::contract::execute,
+        v1::contract::instantiate,
+        v1::contract::query,
     );
     Box::new(contract)
 }
@@ -61,7 +70,6 @@ fn instantiate_staking(app: &mut App, cw20_addr: Addr) -> Addr {
     let staking_id = app.store_code(staking_contract());
     let msg = cw20_stake::msg::InstantiateMsg {
         owner: Some(OWNER.to_string()),
-        manager: Some(MANAGER.to_string()),
         token_address: cw20_addr.to_string(),
         unstaking_duration: None,
     };
@@ -80,7 +88,7 @@ fn instantiate_distributor(app: &mut App, msg: InstantiateMsg) -> Addr {
     let code_id = app.store_code(distributor_contract());
     app.instantiate_contract(
         code_id,
-        Addr::unchecked(OWNER), // TODO: Remove this?
+        Addr::unchecked(OWNER),
         &msg,
         &[],
         "distributor",
@@ -109,6 +117,12 @@ fn get_info<T: Into<String>>(app: &App, distributor_addr: T) -> InfoResponse {
     result
 }
 
+fn get_owner(app: &App, contract: &Addr) -> Ownership<Addr> {
+    app.wrap()
+        .query_wasm_smart(contract, &QueryMsg::Ownership {})
+        .unwrap()
+}
+
 #[test]
 fn test_instantiate() {
     let mut app = App::default();
@@ -126,19 +140,28 @@ fn test_instantiate() {
     let distributor_addr = instantiate_distributor(&mut app, msg);
     let response: InfoResponse = app
         .wrap()
-        .query_wasm_smart(distributor_addr, &QueryMsg::Info {})
+        .query_wasm_smart(&distributor_addr, &QueryMsg::Info {})
         .unwrap();
 
     assert_eq!(
         response.config,
         Config {
-            owner: Addr::unchecked(OWNER),
             staking_addr,
             reward_rate: Uint128::new(1),
             reward_token: cw20_addr,
         }
     );
     assert_eq!(response.last_payment_block, app.block_info().height);
+
+    let ownership = get_owner(&app, &distributor_addr);
+    assert_eq!(
+        ownership,
+        Ownership::<Addr> {
+            owner: Some(Addr::unchecked(OWNER)),
+            pending_owner: None,
+            pending_expiry: None
+        }
+    );
 }
 
 #[test]
@@ -157,7 +180,6 @@ fn test_update_config() {
     let distributor_addr = instantiate_distributor(&mut app, msg);
 
     let msg = ExecuteMsg::UpdateConfig {
-        owner: OWNER2.to_string(),
         staking_addr: staking_addr.to_string(),
         reward_rate: Uint128::new(5),
         reward_token: cw20_addr.to_string(),
@@ -174,7 +196,6 @@ fn test_update_config() {
     assert_eq!(
         response.config,
         Config {
-            owner: Addr::unchecked(OWNER2),
             staking_addr: staking_addr.clone(),
             reward_rate: Uint128::new(5),
             reward_token: cw20_addr.clone(),
@@ -182,19 +203,19 @@ fn test_update_config() {
     );
 
     let msg = ExecuteMsg::UpdateConfig {
-        owner: OWNER2.to_string(),
         staking_addr: staking_addr.to_string(),
         reward_rate: Uint128::new(7),
         reward_token: cw20_addr.to_string(),
     };
 
+    // non-owner may not update config.
     let err: ContractError = app
-        .execute_contract(Addr::unchecked(OWNER), distributor_addr, &msg, &[])
+        .execute_contract(Addr::unchecked("notowner"), distributor_addr, &msg, &[])
         .unwrap_err()
         .downcast()
         .unwrap();
 
-    assert_eq!(err, ContractError::Unauthorized {});
+    assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner));
 }
 
 #[test]
@@ -383,7 +404,6 @@ fn test_update_config_invalid_addrs() {
     let distributor_addr = instantiate_distributor(&mut app, msg);
 
     let msg = ExecuteMsg::UpdateConfig {
-        owner: OWNER.to_string(),
         staking_addr: staking_addr.to_string(),
         reward_rate: Uint128::new(5),
         reward_token: "invalid_cw20".to_string(),
@@ -397,7 +417,6 @@ fn test_update_config_invalid_addrs() {
     assert_eq!(err, ContractError::InvalidCw20 {});
 
     let msg = ExecuteMsg::UpdateConfig {
-        owner: OWNER.to_string(),
         staking_addr: "invalid_staking".to_string(),
         reward_rate: Uint128::new(5),
         reward_token: staking_addr.to_string(),
@@ -458,14 +477,17 @@ fn test_withdraw() {
     // Unauthorized user cannot withdraw funds
     let err = app
         .execute_contract(
-            Addr::unchecked(MANAGER),
+            Addr::unchecked("notowner"),
             distributor_addr.clone(),
             &ExecuteMsg::Withdraw {},
             &[],
         )
         .unwrap_err();
 
-    assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap());
+    assert_eq!(
+        ContractError::Ownership(OwnershipError::NotOwner),
+        err.downcast().unwrap()
+    );
 
     // Withdraw funds
     app.execute_contract(
@@ -504,10 +526,7 @@ fn test_dao_deploy() {
     };
     let distributor_addr = instantiate_distributor(&mut app, msg);
 
-    app.update_block(|mut block| block.height += 1000);
-
     let msg = ExecuteMsg::UpdateConfig {
-        owner: OWNER.to_string(),
         staking_addr: staking_addr.to_string(),
         reward_rate: Uint128::new(1),
         reward_token: cw20_addr.to_string(),
@@ -540,11 +559,200 @@ fn test_dao_deploy() {
 }
 
 #[test]
-pub fn test_migrate_update_version() {
-    let mut deps = mock_dependencies();
-    cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap();
-    migrate(deps.as_mut(), mock_env(), MigrateMsg {}).unwrap();
-    let version = cw2::get_contract_version(&deps.storage).unwrap();
-    assert_eq!(version.version, CONTRACT_VERSION);
-    assert_eq!(version.contract, CONTRACT_NAME);
+fn test_ownership() {
+    let mut app = App::default();
+    let cw20_addr = instantiate_cw20(
+        &mut app,
+        vec![cw20::Cw20Coin {
+            address: OWNER.to_string(),
+            amount: Uint128::from(1000u64),
+        }],
+    );
+    let staking_addr = instantiate_staking(&mut app, cw20_addr.clone());
+    let msg = InstantiateMsg {
+        owner: OWNER.to_string(),
+        staking_addr: staking_addr.to_string(),
+        reward_rate: Uint128::new(0),
+        reward_token: cw20_addr.to_string(),
+    };
+    let distributor_addr = instantiate_distributor(&mut app, msg);
+
+    app.execute_contract(
+        Addr::unchecked(OWNER),
+        distributor_addr.clone(),
+        &ExecuteMsg::UpdateOwnership(Action::TransferOwnership {
+            new_owner: OWNER2.to_string(),
+            expiry: None,
+        }),
+        &[],
+    )
+    .unwrap();
+
+    let ownership = get_owner(&app, &distributor_addr);
+    assert_eq!(
+        ownership,
+        Ownership::<Addr> {
+            owner: Some(Addr::unchecked(OWNER)),
+            pending_owner: Some(Addr::unchecked(OWNER2)),
+            pending_expiry: None
+        }
+    );
+
+    app.execute_contract(
+        Addr::unchecked(OWNER2),
+        distributor_addr.clone(),
+        &ExecuteMsg::UpdateOwnership(Action::AcceptOwnership),
+        &[],
+    )
+    .unwrap();
+
+    let ownership = get_owner(&app, &distributor_addr);
+    assert_eq!(
+        ownership,
+        Ownership::<Addr> {
+            owner: Some(Addr::unchecked(OWNER2)),
+            pending_owner: None,
+            pending_expiry: None
+        }
+    );
+}
+
+#[test]
+fn test_ownership_expiry() {
+    let mut app = App::default();
+    let cw20_addr = instantiate_cw20(
+        &mut app,
+        vec![cw20::Cw20Coin {
+            address: OWNER.to_string(),
+            amount: Uint128::from(1000u64),
+        }],
+    );
+    let staking_addr = instantiate_staking(&mut app, cw20_addr.clone());
+    let msg = InstantiateMsg {
+        owner: OWNER.to_string(),
+        staking_addr: staking_addr.to_string(),
+        reward_rate: Uint128::new(0),
+        reward_token: cw20_addr.to_string(),
+    };
+    let distributor_addr = instantiate_distributor(&mut app, msg);
+
+    app.execute_contract(
+        Addr::unchecked(OWNER),
+        distributor_addr.clone(),
+        &ExecuteMsg::UpdateOwnership(Action::TransferOwnership {
+            new_owner: OWNER2.to_string(),
+            expiry: Some(Expiration::AtHeight(app.block_info().height + 1)),
+        }),
+        &[],
+    )
+    .unwrap();
+
+    let ownership = get_owner(&app, &distributor_addr);
+    assert_eq!(
+        ownership,
+        Ownership::<Addr> {
+            owner: Some(Addr::unchecked(OWNER)),
+            pending_owner: Some(Addr::unchecked(OWNER2)),
+            pending_expiry: Some(Expiration::AtHeight(app.block_info().height + 1)),
+        }
+    );
+
+    app.update_block(next_block);
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked(OWNER2),
+            distributor_addr,
+            &ExecuteMsg::UpdateOwnership(Action::AcceptOwnership),
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(
+        err,
+        ContractError::Ownership(OwnershipError::TransferExpired)
+    )
+}
+
+#[test]
+fn test_migrate_from_v1() {
+    let mut app = App::default();
+    let sender = Addr::unchecked("sender");
+
+    let cw20_addr = instantiate_cw20(
+        &mut app,
+        vec![cw20::Cw20Coin {
+            address: sender.to_string(),
+            amount: Uint128::from(1000u64),
+        }],
+    );
+    let staking_addr = instantiate_staking(&mut app, cw20_addr.clone());
+
+    let v1_code = app.store_code(distributor_contract_v1());
+    let v2_code = app.store_code(distributor_contract());
+    let distributor = app
+        .instantiate_contract(
+            v1_code,
+            sender.clone(),
+            &v1::msg::InstantiateMsg {
+                owner: sender.to_string(),
+                staking_addr: staking_addr.to_string(),
+                reward_rate: Uint128::new(1),
+                reward_token: cw20_addr.to_string(),
+            },
+            &[],
+            "distributor",
+            Some(sender.to_string()),
+        )
+        .unwrap();
+    app.execute(
+        sender.clone(),
+        WasmMsg::Migrate {
+            contract_addr: distributor.to_string(),
+            new_code_id: v2_code,
+            msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(),
+        }
+        .into(),
+    )
+    .unwrap();
+
+    let ownership = get_owner(&app, &distributor);
+    assert_eq!(
+        ownership,
+        Ownership::<Addr> {
+            owner: Some(sender.clone()),
+            pending_owner: None,
+            pending_expiry: None,
+        }
+    );
+
+    let info = get_info(&app, &distributor);
+    assert_eq!(
+        info,
+        InfoResponse {
+            config: Config {
+                staking_addr,
+                reward_rate: Uint128::new(1),
+                reward_token: cw20_addr
+            },
+            last_payment_block: app.block_info().height,
+            balance: Uint128::zero()
+        }
+    );
+
+    let err: ContractError = app
+        .execute(
+            sender,
+            WasmMsg::Migrate {
+                contract_addr: distributor.to_string(),
+                new_code_id: v2_code,
+                msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(),
+            }
+            .into(),
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::AlreadyMigrated {});
 }
diff --git a/contracts/staking/cw20-stake/src/tests.rs b/contracts/staking/cw20-stake/src/tests.rs
index 69371af5..81ae03b1 100644
--- a/contracts/staking/cw20-stake/src/tests.rs
+++ b/contracts/staking/cw20-stake/src/tests.rs
@@ -1,6 +1,5 @@
 use std::borrow::BorrowMut;
 
-use crate::contract::{migrate, CONTRACT_NAME, CONTRACT_VERSION};
 use crate::msg::{
     ExecuteMsg, ListStakersResponse, MigrateMsg, QueryMsg, ReceiveMsg,
     StakedBalanceAtHeightResponse, StakedValueResponse, StakerBalanceResponse,
@@ -8,15 +7,16 @@ use crate::msg::{
 };
 use crate::state::{Config, MAX_CLAIMS};
 use crate::ContractError;
-use cosmwasm_schema::cw_serde;
 use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
-use cosmwasm_std::{from_slice, to_binary, Addr, Empty, MessageInfo, Storage, Uint128};
+use cosmwasm_std::{to_binary, Addr, Empty, MessageInfo, Uint128, WasmMsg};
 use cw20::Cw20Coin;
+use cw_ownable::{Action, Ownership, OwnershipError};
 use cw_utils::Duration;
 
 use cw_multi_test::{next_block, App, AppResponse, Contract, ContractWrapper, Executor};
 
 use anyhow::Result as AnyResult;
+use cw20_stake_v1 as v1;
 
 use cw_controllers::{Claim, ClaimsResponse};
 use cw_utils::Expiration::AtHeight;
@@ -25,8 +25,9 @@ const ADDR1: &str = "addr0001";
 const ADDR2: &str = "addr0002";
 const ADDR3: &str = "addr0003";
 const ADDR4: &str = "addr0004";
+const OWNER: &str = "owner";
 
-pub fn contract_staking() -> Box<dyn Contract<Empty>> {
+fn contract_staking() -> Box<dyn Contract<Empty>> {
     let contract = ContractWrapper::new(
         crate::contract::execute,
         crate::contract::instantiate,
@@ -36,7 +37,17 @@ pub fn contract_staking() -> Box<dyn Contract<Empty>> {
     Box::new(contract)
 }
 
-pub fn contract_cw20() -> Box<dyn Contract<Empty>> {
+fn contract_staking_v1() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        v1::contract::execute,
+        v1::contract::instantiate,
+        v1::contract::query,
+    )
+    .with_migrate(v1::contract::migrate);
+    Box::new(contract)
+}
+
+fn contract_cw20() -> Box<dyn Contract<Empty>> {
     let contract = ContractWrapper::new(
         cw20_base::contract::execute,
         cw20_base::contract::instantiate,
@@ -79,8 +90,7 @@ fn instantiate_cw20(app: &mut App, initial_balances: Vec<Cw20Coin>) -> Addr {
 fn instantiate_staking(app: &mut App, cw20: Addr, unstaking_duration: Option<Duration>) -> Addr {
     let staking_code_id = app.store_code(contract_staking());
     let msg = crate::msg::InstantiateMsg {
-        owner: Some("owner".to_string()),
-        manager: Some("manager".to_string()),
+        owner: Some(OWNER.to_string()),
         token_address: cw20.to_string(),
         unstaking_duration,
     };
@@ -128,6 +138,12 @@ fn query_config<T: Into<String>>(app: &App, contract_addr: T) -> Config {
     app.wrap().query_wasm_smart(contract_addr, &msg).unwrap()
 }
 
+fn query_owner<T: Into<String>>(app: &App, contract: T) -> Ownership<Addr> {
+    app.wrap()
+        .query_wasm_smart(contract, &QueryMsg::Ownership {})
+        .unwrap()
+}
+
 fn query_total_staked<T: Into<String>>(app: &App, contract_addr: T) -> Uint128 {
     let msg = QueryMsg::TotalStakedAtHeight { height: None };
     let result: TotalStakedAtHeightResponse =
@@ -184,15 +200,9 @@ fn update_config(
     app: &mut App,
     staking_addr: &Addr,
     info: MessageInfo,
-    owner: Option<Addr>,
-    manager: Option<Addr>,
     duration: Option<Duration>,
 ) -> AnyResult<AppResponse> {
-    let msg = ExecuteMsg::UpdateConfig {
-        owner: owner.map(|a| a.to_string()),
-        manager: manager.map(|a| a.to_string()),
-        duration,
-    };
+    let msg = ExecuteMsg::UpdateConfig { duration };
     app.execute_contract(info.sender, staking_addr.clone(), &msg, &[])
 }
 
@@ -234,204 +244,43 @@ fn test_instantiate_with_non_cw20_token() {
 
 #[test]
 fn test_update_config() {
-    let _deps = mock_dependencies();
-
     let mut app = mock_app();
     let amount1 = Uint128::from(100u128);
-    let _token_address = Addr::unchecked("token_address");
     let initial_balances = vec![Cw20Coin {
         address: ADDR1.to_string(),
         amount: amount1,
     }];
     let (staking_addr, _cw20_addr) = setup_test_case(&mut app, initial_balances, None);
 
-    let info = mock_info("owner", &[]);
-    let _env = mock_env();
-    // Test update admin
-    update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        Some(Addr::unchecked("owner2")),
-        None,
-        Some(Duration::Height(100)),
-    )
-    .unwrap();
-
-    let config = query_config(&app, &staking_addr);
-    assert_eq!(config.owner, Some(Addr::unchecked("owner2")));
-    assert_eq!(config.unstaking_duration, Some(Duration::Height(100)));
-
-    // Try updating owner with original owner, which is now invalid
-    let info = mock_info("owner", &[]);
-    let _err = update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        Some(Addr::unchecked("owner3")),
-        None,
-        Some(Duration::Height(100)),
-    )
-    .unwrap_err();
-
-    // Add manager
-    let info = mock_info("owner2", &[]);
-    let _env = mock_env();
-    update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        Some(Addr::unchecked("owner2")),
-        Some(Addr::unchecked("manager")),
-        Some(Duration::Height(100)),
-    )
-    .unwrap();
-
-    let config = query_config(&app, &staking_addr);
-    assert_eq!(config.owner, Some(Addr::unchecked("owner2")));
-    assert_eq!(config.manager, Some(Addr::unchecked("manager")));
-
-    // Manager can update unstaking duration
-    let info = mock_info("manager", &[]);
-    let _env = mock_env();
-    update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        Some(Addr::unchecked("owner2")),
-        Some(Addr::unchecked("manager")),
-        Some(Duration::Height(50)),
-    )
-    .unwrap();
-    let config = query_config(&app, &staking_addr);
-    assert_eq!(config.owner, Some(Addr::unchecked("owner2")));
-    assert_eq!(config.unstaking_duration, Some(Duration::Height(50)));
-
-    // Manager cannot update owner
-    let info = mock_info("manager", &[]);
-    let _env = mock_env();
-    update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        Some(Addr::unchecked("manager")),
-        Some(Addr::unchecked("manager")),
-        Some(Duration::Height(50)),
-    )
-    .unwrap_err();
-
-    // Manager can update manager
-    let info = mock_info("owner2", &[]);
-    let _env = mock_env();
-    update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        Some(Addr::unchecked("owner2")),
-        None,
-        Some(Duration::Height(50)),
-    )
-    .unwrap();
-
+    // Owner can update configuration.
+    let info = mock_info(OWNER, &[]);
+    update_config(&mut app, &staking_addr, info, Some(Duration::Height(1234))).unwrap();
     let config = query_config(&app, &staking_addr);
-    assert_eq!(config.owner, Some(Addr::unchecked("owner2")));
-    assert_eq!(config.manager, None);
+    assert_eq!(config.unstaking_duration, Some(Duration::Height(1234)));
 
-    // Invalid duration
-    let info = mock_info("owner2", &[]);
-    let _env = mock_env();
-    let err: ContractError = update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        Some(Addr::unchecked("owner2")),
-        None,
-        Some(Duration::Height(0)),
-    )
-    .unwrap_err()
-    .downcast()
-    .unwrap();
+    // Non owner may not update configuration.
+    let info = mock_info(ADDR1, &[]);
+    let err: ContractError = update_config(&mut app, &staking_addr, info, None)
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::Ownership(OwnershipError::NotOwner));
+
+    // Zero durations not allowed.
+    let info = mock_info(OWNER, &[]);
+    let err: ContractError =
+        update_config(&mut app, &staking_addr, info, Some(Duration::Height(0)))
+            .unwrap_err()
+            .downcast()
+            .unwrap();
     assert_eq!(err, ContractError::InvalidUnstakingDuration {});
 
-    // Remove owner
-    let info = mock_info("owner2", &[]);
-    let _env = mock_env();
-    update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        None,
-        None,
-        Some(Duration::Height(100)),
-    )
-    .unwrap();
-
-    // Assert no further updates can be made
-    let info = mock_info("owner2", &[]);
-    let _env = mock_env();
-    let err: ContractError = update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        None,
-        None,
-        Some(Duration::Height(100)),
-    )
-    .unwrap_err()
-    .downcast()
-    .unwrap();
-    assert_eq!(err, ContractError::Unauthorized {});
-
-    let info = mock_info("manager", &[]);
-    let _env = mock_env();
-    let err: ContractError = update_config(
-        &mut app,
-        &staking_addr,
-        info,
-        None,
-        None,
-        Some(Duration::Height(100)),
-    )
-    .unwrap_err()
-    .downcast()
-    .unwrap();
-    assert_eq!(err, ContractError::Unauthorized {})
-}
-
-#[test]
-fn test_migrate_from_beta() {
-    let mut deps = mock_dependencies();
-    let env = mock_env();
-    let token_address = Addr::unchecked("token_address");
-
-    // Write to storage in old config format
-    let key = b"config";
-    #[cw_serde]
-    struct BetaConfig {
-        pub admin: Addr,
-        pub token_address: Addr,
-        pub unstaking_duration: Option<Duration>,
-    }
-    let beta_config = BetaConfig {
-        admin: Addr::unchecked("beta_admin"),
-        token_address: token_address.clone(),
-        unstaking_duration: None,
-    };
-
-    deps.storage.set(key, &to_binary(&beta_config).unwrap());
-
-    let migrate_msg = MigrateMsg::FromBeta {
-        manager: Some("new_manager".to_string()),
-    };
-
-    migrate(deps.as_mut(), env, migrate_msg).unwrap();
-
-    let config_bytes = deps.storage.get(key).unwrap();
-    let config: Config = from_slice(&config_bytes).unwrap();
-    assert_eq!(config.owner, Some(Addr::unchecked("beta_admin")));
-    assert_eq!(config.manager, Some(Addr::unchecked("new_manager")));
-    assert_eq!(config.unstaking_duration, None);
-    assert_eq!(config.token_address, token_address)
+    let info = mock_info(OWNER, &[]);
+    let err: ContractError = update_config(&mut app, &staking_addr, info, Some(Duration::Time(0)))
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::InvalidUnstakingDuration {});
 }
 
 #[test]
@@ -1211,11 +1060,133 @@ fn test_query_list_stakers() {
 }
 
 #[test]
-pub fn test_migrate_update_version() {
-    let mut deps = mock_dependencies();
-    cw2::set_contract_version(&mut deps.storage, "my-contract", "old-version").unwrap();
-    migrate(deps.as_mut(), mock_env(), MigrateMsg::FromCompatible {}).unwrap();
-    let version = cw2::get_contract_version(&deps.storage).unwrap();
-    assert_eq!(version.version, CONTRACT_VERSION);
-    assert_eq!(version.contract, CONTRACT_NAME);
+fn test_ownership_transfer() {
+    let mut app = App::default();
+    let cw20_addr = instantiate_cw20(
+        &mut app,
+        vec![cw20::Cw20Coin {
+            address: OWNER.to_string(),
+            amount: Uint128::from(1000u64),
+        }],
+    );
+    let staking_addr = instantiate_staking(&mut app, cw20_addr, None);
+
+    app.execute_contract(
+        Addr::unchecked(OWNER),
+        staking_addr.clone(),
+        &ExecuteMsg::UpdateOwnership(Action::TransferOwnership {
+            new_owner: ADDR1.to_string(),
+            expiry: None,
+        }),
+        &[],
+    )
+    .unwrap();
+
+    let ownership = query_owner(&app, &staking_addr);
+    assert_eq!(
+        ownership,
+        Ownership::<Addr> {
+            owner: Some(Addr::unchecked(OWNER)),
+            pending_owner: Some(Addr::unchecked(ADDR1)),
+            pending_expiry: None
+        }
+    );
+
+    app.execute_contract(
+        Addr::unchecked(ADDR1),
+        staking_addr.clone(),
+        &ExecuteMsg::UpdateOwnership(Action::AcceptOwnership),
+        &[],
+    )
+    .unwrap();
+
+    let ownership = query_owner(&app, &staking_addr);
+    assert_eq!(
+        ownership,
+        Ownership::<Addr> {
+            owner: Some(Addr::unchecked(ADDR1)),
+            pending_owner: None,
+            pending_expiry: None
+        }
+    );
+}
+
+#[test]
+fn test_migrate_from_v1() {
+    let mut app = App::default();
+    let cw20_addr = instantiate_cw20(
+        &mut app,
+        vec![cw20::Cw20Coin {
+            address: OWNER.to_string(),
+            amount: Uint128::from(1000u64),
+        }],
+    );
+
+    let v1_code = app.store_code(contract_staking_v1());
+    let v2_code = app.store_code(contract_staking());
+
+    let staking = app
+        .instantiate_contract(
+            v1_code,
+            Addr::unchecked(OWNER),
+            &v1::msg::InstantiateMsg {
+                owner: Some(OWNER.to_string()),
+                manager: Some(OWNER.to_string()),
+                token_address: cw20_addr.to_string(),
+                unstaking_duration: None,
+            },
+            &[],
+            "staking".to_string(),
+            Some(OWNER.to_string()),
+        )
+        .unwrap();
+
+    app.execute(
+        Addr::unchecked(OWNER),
+        WasmMsg::Migrate {
+            contract_addr: staking.to_string(),
+            new_code_id: v2_code,
+            msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(),
+        }
+        .into(),
+    )
+    .unwrap();
+
+    // can not migrate more than once.
+    let err: ContractError = app
+        .execute(
+            Addr::unchecked(OWNER),
+            WasmMsg::Migrate {
+                contract_addr: staking.to_string(),
+                new_code_id: v2_code,
+                msg: to_binary(&MigrateMsg::FromV1 {}).unwrap(),
+            }
+            .into(),
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::AlreadyMigrated {});
+
+    // owner is moved into cw_ownable.
+    let ownership = query_owner(&app, &staking);
+    assert_eq!(
+        ownership,
+        Ownership::<Addr> {
+            owner: Some(Addr::unchecked(OWNER)),
+            pending_owner: None,
+            pending_expiry: None
+        }
+    );
+
+    // config is loadable and has no manager, but is otherwise
+    // unchanged.
+    let config = query_config(&app, &staking);
+    assert_eq!(
+        config,
+        Config {
+            token_address: cw20_addr,
+            unstaking_duration: None,
+        }
+    );
 }