Repository: DA0-DA0/dao-contracts
Pull request #608 (2023-01-08 04:28:15)

cw-vesting with payroll factory
Building off of #602. This simplifies things and minimizes risk by using the factory pattern.

Supports staking, used some code inspired by what we did for mesh security recently, so curious what folks think of rewards calculation, etc.

TODO:
- [x] Factory contract
- [x] Redelegate
- [x] Query list of payroll contracts by instantiator
- [x] Query list of payroll contracts by recipient
- [x] Query vested amount
- [x] MORE TESTS

codecov-commenter: # [Codecov](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/608?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) Report
Base: **95.34**% // Head: **94.40**% // Decreases project coverage by **`-0.94%`** :warning:
> Coverage data is based on head [(`1471862`)](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/608?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) compared to base [(`1673241`)](https://codecov.io/gh/DA0-DA0/dao-contracts/commit/1673241909dccc57a561f5be9c06e5e21757be09?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0).
> Patch coverage: 83.22% of modified lines in pull request are covered.

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


```diff
@@            Coverage Diff             @@
##             main     #608      +/-   ##
==========================================
- Coverage   95.34%   94.40%   -0.95%     
==========================================
  Files          41       44       +3     
  Lines        3738     4054     +316     
==========================================
+ Hits         3564     3827     +263     
- Misses        174      227      +53     
```


| [Impacted Files](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0) | Coverage Δ | |
|---|---|---|
| [...tracts/external/cw-payroll-factory/src/contract.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2V4dGVybmFsL2N3LXBheXJvbGwtZmFjdG9yeS9zcmMvY29udHJhY3QucnM=) | `28.00% <28.00%> (ø)` | |
| [contracts/external/cw-payroll/src/contract.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2V4dGVybmFsL2N3LXBheXJvbGwvc3JjL2NvbnRyYWN0LnJz) | `87.61% <87.61%> (ø)` | |
| [contracts/external/cw-payroll/src/state.rs](https://codecov.io/gh/DA0-DA0/dao-contracts/pull/608?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=DA0-DA0#diff-Y29udHJhY3RzL2V4dGVybmFsL2N3LXBheXJvbGwvc3JjL3N0YXRlLnJz) | `89.23% <89.23%> (ø)` | |

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/608?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> 
 
 No gas diff larger than 0.5% 
 
 <details><summary>Raw Report for 147186231567f80bbb4eb11c79671f4296cb762b </summary><br/> 

| Contract | Op Name | Gas Used | Gas Wanted | File | 
 | --- | --- | --- | --- | --- |
| dao_core | Instantiate__inst_admin_create_dao | 1277715 | 1893773 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_stake_create_dao | 1276641 | 1892162 | 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 | 1277715 | 1893773 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__inst_dao_no_admin | 1276665 | 1892198 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Store__Store | 5668751 | 8480337 | ci/integration-tests/src/helpers/chain.rs:97 |
| 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 | 1276665 | 1892198 | ci/integration-tests/src/helpers/helper.rs:98 |
| dao_core | Instantiate__exc_admin_msgs_create_dao_with_admin | 1277715 | 1893773 | ci/integration-tests/src/helpers/helper.rs:98 |
| cw20_base | Execute__exc_stake_stake_tokens | 234536 | 320952 | ci/integration-tests/src/tests/cw20_stake_test.rs:76 |
| cw20_base | Store__Store | 4180849 | 6248484 | 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 |
| 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 |
| dao_voting_cw721_staked | Execute__unstake_nfts | 230319 | 322760 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:105 |
| dao_voting_cw721_staked | Instantiate__instantiate_dao_voting_cw721_staked | 179007 | 245795 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:49 |
| 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 | Store__Store | 3327711 | 4968777 | ci/integration-tests/src/helpers/chain.rs:97 |
| multiple_contracts | Execute__batch_cw721_stake_max_claims | 4196439 | 6254235 | ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs:251 |
| cw20_stake | Store__Store | 3918184 | 5854487 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw20_stake_external_rewards | Store__Store | 3332066 | 4975310 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw20_stake_reward_distributor | Store__Store | 2816213 | 4201530 | 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_payroll | Store__Store | 3916364 | 5851757 | ci/integration-tests/src/helpers/chain.rs:97 |
| cw_payroll_factory | Store__Store | 2003219 | 2982039 | 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 | 6661184 | 9968987 | ci/integration-tests/src/helpers/chain.rs:97 |
| dao_voting_cw20_staked | Store__Store | 3608407 | 5389821 | 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 |
</details>
blue-note: I think the design is looking good!! I did a first pass, going to do a more detailed look on the next pass 
JakeHartnell: > this looks great architecturally. as discussed in Discord, i think we can remove all the complexity of calculating and storing rewards by making the vest receiver the [withdraw address](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L29-L37) for staked tokens. frontends can then handle providing an easy way to withdraw those funds.
> 
> once that's done i will do a detailed pass over the math. think this is close :)

This was a great suggestion! Much cleaner now, and can probably be even cleaner still. Only challenge is test coverage... `cw-multi-test` does not support the `SetWithdrawAddress` distribution message: https://github.com/CosmWasm/cw-multi-test/issues/20

I tried to cover things as best as possible with unit tests, we should figure out what to do until this gets added upstream.
mikedotexe: Super psyched. No joke, this is among a handful of smart contracts that can change all of web3 in 2023 💪🏼 
0xekez: really obscure observation about this contract below. i don't think we need to change anything before merging, saying in the hope it is useful / reveals something i don't understand.

---

let $curve(t)$ be the value of the vesting curve at, and $claimed(t)$ be total claimed up to time $t$.

in this contract, we construct the curve such that $claimable(t) := total - curve(t) - claimed(t)$.

in state, we store:

```rust
#[cw_serde]
pub struct VestingPayment {
    pub amount: Uint128,
    pub claimed: Uint128,
    pub curve: Curve,
}
```

and we maintain that $amount(t) := total - claimed(t)$.

$$
claimable(t) := total - claimed(t) - curve(t)
= amount(t) - curve(t)
$$

this is fine, and works and is readable once your head it around it. though i wonder if we stored:

```rust
pub struct VestingPayment {
    pub total: Uint128,
    pub claimed: Uint128,
    pub curve: Curve,
}
```

this might simplify things a little, as the total is more visible and only the `claimed` field is mutable?
JakeHartnell: > really obscure observation about this contract below. i don't think we need to change anything before merging, saying in the hope it is useful / reveals something i don't understand.
> 
> let curve(t) be the value of the vesting curve at, and claimed(t) be total claimed up to time t.
> 
> in this contract, we construct the curve such that claimable(t):=total−curve(t)−claimed(t).
> 
> in state, we store:
> 
> ```rust
> #[cw_serde]
> pub struct VestingPayment {
>     pub amount: Uint128,
>     pub claimed: Uint128,
>     pub curve: Curve,
> }
> ```
> 
> and we maintain that amount(t):=total−claimed(t).
> 
> claimable(t):=total−claimed(t)−curve(t)=amount(t)−curve(t)
> 
> this is fine, and works and is readable once your head it around it. though i wonder if we stored:
> 
> ```rust
> pub struct VestingPayment {
>     pub total: Uint128,
>     pub claimed: Uint128,
>     pub curve: Curve,
> }
> ```
> 
> this might simplify things a little, as the total is more visible and only the `claimed` field is mutable?

Love this!

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 0c0d9e52..addc0582 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -667,6 +667,25 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "cw-payroll-factory"
+version = "2.0.0-beta"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cw-denom",
+ "cw-multi-test",
+ "cw-ownable",
+ "cw-storage-plus 0.16.0",
+ "cw-utils 0.16.0",
+ "cw-vesting",
+ "cw2 0.16.0",
+ "cw20 0.16.0",
+ "cw20-base 0.16.0",
+ "thiserror",
+ "wynd-utils",
+]
+
 [[package]]
 name = "cw-proposal-single"
 version = "0.1.0"
@@ -781,6 +800,27 @@ dependencies = [
  "thiserror",
 ]
 
+[[package]]
+name = "cw-vesting"
+version = "2.0.1"
+dependencies = [
+ "cosmwasm-schema",
+ "cosmwasm-std",
+ "cw-denom",
+ "cw-multi-test",
+ "cw-ownable",
+ "cw-paginate 2.0.1",
+ "cw-storage-plus 0.16.0",
+ "cw-utils 0.16.0",
+ "cw2 0.16.0",
+ "cw20 0.16.0",
+ "cw20-base 0.16.0",
+ "dao-testing",
+ "serde",
+ "thiserror",
+ "wynd-utils",
+]
+
 [[package]]
 name = "cw2"
 version = "0.13.4"
@@ -3778,6 +3818,18 @@ version = "0.42.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
 
+[[package]]
+name = "wynd-utils"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daa37b3fba808df599acc6f0d7523b465baf47a0b0361867c4f1635eb53f72aa"
+dependencies = [
+ "cosmwasm-std",
+ "schemars",
+ "serde",
+ "thiserror",
+]
+
 [[package]]
 name = "yaml-rust"
 version = "0.4.5"
diff --git a/Cargo.toml b/Cargo.toml
index 3e9f3ffa..9af953c4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -55,6 +55,7 @@ serde = "1.0"
 proc-macro2 = "1.0"
 quote = "1.0"
 syn = { version = "1.0", features = ["derive"] }
+wynd-utils = "0.4.1"
 
 # One commit ahead of version 0.3.0. Allows initialization with an
 # optional owner.
@@ -62,6 +63,8 @@ cw-ownable = { git = "https://github.com/steak-enjoyers/cw-plus-plus", rev = "50
 
 
 cw-admin-factory = { path = "./contracts/external/cw-admin-factory" }
+cw-payroll-factory = { path = "./contracts/external/cw-payroll-factory" }
+cw-vesting = { path = "./contracts/external/cw-vesting" }
 dao-core = { path = "./contracts/dao-core" }
 dao-proposal-single = { path = "./contracts/proposal/dao-proposal-single", version = "*" }
 dao-proposal-multiple = { path = "./contracts/proposal/dao-proposal-multiple", version = "*" }
diff --git a/contracts/external/cw-admin-factory/.gitignore b/contracts/external/cw-admin-factory/.gitignore
deleted file mode 100644
index 5a6557d7..00000000
--- a/contracts/external/cw-admin-factory/.gitignore
+++ /dev/null
@@ -1,16 +0,0 @@
-# Build results
-/target
-
-# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327)
-.cargo-ok
-
-# Text file backups
-**/*.rs.bk
-
-# macOS
-.DS_Store
-
-# IDEs
-*.iml
-.editorconfig
-.idea
diff --git a/contracts/external/cw-admin-factory/Cargo.toml b/contracts/external/cw-admin-factory/Cargo.toml
index 8519e37b..e7cf5d38 100644
--- a/contracts/external/cw-admin-factory/Cargo.toml
+++ b/contracts/external/cw-admin-factory/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name ="cw-admin-factory"
 version = "2.0.1"
-authors = ["blue-note"]
+authors = ["Jake Hartnell", "blue-note", "ekez <ekez@withoutdoing.com>"]
 edition = "2021"
 repository = "https://github.com/DA0-DA0/dao-contracts"
 description = "A CosmWasm factory contract for instantiating a contract as its own admin."
diff --git a/contracts/external/cw-admin-factory/src/lib.rs b/contracts/external/cw-admin-factory/src/lib.rs
index ea90e3aa..6902586b 100644
--- a/contracts/external/cw-admin-factory/src/lib.rs
+++ b/contracts/external/cw-admin-factory/src/lib.rs
@@ -3,6 +3,7 @@
 pub mod contract;
 mod error;
 pub mod msg;
+
 #[cfg(test)]
 mod tests;
 
diff --git a/contracts/external/cw-payroll-factory/.cargo/config b/contracts/external/cw-payroll-factory/.cargo/config
new file mode 100644
index 00000000..336b618a
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/.cargo/config
@@ -0,0 +1,4 @@
+[alias]
+wasm = "build --release --target wasm32-unknown-unknown"
+unit-test = "test --lib"
+schema = "run --example schema"
diff --git a/contracts/external/cw-payroll-factory/Cargo.toml b/contracts/external/cw-payroll-factory/Cargo.toml
new file mode 100644
index 00000000..8925a861
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name ="cw-payroll-factory"
+version = "2.0.0-beta"
+authors = ["Jake Hartnell"]
+edition = "2021"
+repository = "https://github.com/DA0-DA0/dao-contracts"
+description = "A CosmWasm factory contract for instantiating a payroll contract."
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+# for more explicit tests, cargo test --features=backtraces
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all instantiate/execute/query exports
+library = []
+
+[dependencies]
+cosmwasm-std = { workspace = true }
+cosmwasm-schema = { workspace = true }
+cw-denom = { workspace = true }
+cw-ownable = { workspace = true }
+cw-storage-plus = { workspace = true }
+cw2 = { workspace = true }
+cw20 = { workspace = true }
+thiserror = { workspace = true }
+cw-vesting = { workspace = true, features = ["library"] }
+cw-utils = { workspace = true }
+
+[dev-dependencies]
+cw-multi-test = { workspace = true }
+cw20-base = { workspace = true, features = ["library"] }
+wynd-utils = { workspace = true }
diff --git a/contracts/external/cw-payroll-factory/README.md b/contracts/external/cw-payroll-factory/README.md
new file mode 100644
index 00000000..553134a9
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/README.md
@@ -0,0 +1,5 @@
+# cw-payroll-factory
+
+Serves as a factory that instantiates [cw-vesting](../cw-vesting) contracts and stores them in an indexed maps for easy querying by recipient or the instantiator (i.e. give me all of my vesting payment contracts or give me all of a DAO's vesting payment contracts).
+
+An optional `owner` can be specified when instantiating `cw-payroll-factory` that limits contract instantiation to a single account.
diff --git a/contracts/external/cw-payroll-factory/examples/schema.rs b/contracts/external/cw-payroll-factory/examples/schema.rs
new file mode 100644
index 00000000..bd3ccb53
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/examples/schema.rs
@@ -0,0 +1,10 @@
+use cosmwasm_schema::write_api;
+use cw_payroll_factory::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+
+fn main() {
+    write_api! {
+        instantiate: InstantiateMsg,
+        query: QueryMsg,
+        execute: ExecuteMsg,
+    }
+}
diff --git a/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json b/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json
new file mode 100644
index 00000000..e71e8bcc
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/schema/cw-payroll-factory.json
@@ -0,0 +1,878 @@
+{
+  "contract_name": "cw-payroll-factory",
+  "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": [
+      "vesting_code_id"
+    ],
+    "properties": {
+      "owner": {
+        "type": [
+          "string",
+          "null"
+        ]
+      },
+      "vesting_code_id": {
+        "type": "integer",
+        "format": "uint64",
+        "minimum": 0.0
+      }
+    },
+    "additionalProperties": false
+  },
+  "execute": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "ExecuteMsg",
+    "oneOf": [
+      {
+        "description": "Instantiates a new vesting contract that is funded by a cw20 token.",
+        "type": "object",
+        "required": [
+          "receive"
+        ],
+        "properties": {
+          "receive": {
+            "$ref": "#/definitions/Cw20ReceiveMsg"
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Instantiates a new vesting contract that is funded by a native token.",
+        "type": "object",
+        "required": [
+          "instantiate_native_payroll_contract"
+        ],
+        "properties": {
+          "instantiate_native_payroll_contract": {
+            "type": "object",
+            "required": [
+              "instantiate_msg",
+              "label"
+            ],
+            "properties": {
+              "instantiate_msg": {
+                "$ref": "#/definitions/InstantiateMsg"
+              },
+              "label": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Callable only by the current owner. Updates the code ID used while instantiating vesting contracts.",
+        "type": "object",
+        "required": [
+          "update_code_id"
+        ],
+        "properties": {
+          "update_code_id": {
+            "type": "object",
+            "required": [
+              "vesting_code_id"
+            ],
+            "properties": {
+              "vesting_code_id": {
+                "type": "integer",
+                "format": "uint64",
+                "minimum": 0.0
+              }
+            },
+            "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"
+            ]
+          }
+        ]
+      },
+      "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"
+      },
+      "Curve": {
+        "oneOf": [
+          {
+            "type": "object",
+            "required": [
+              "constant"
+            ],
+            "properties": {
+              "constant": {
+                "type": "object",
+                "required": [
+                  "y"
+                ],
+                "properties": {
+                  "y": {
+                    "$ref": "#/definitions/Uint128"
+                  }
+                }
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "required": [
+              "saturating_linear"
+            ],
+            "properties": {
+              "saturating_linear": {
+                "$ref": "#/definitions/SaturatingLinear"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "required": [
+              "piecewise_linear"
+            ],
+            "properties": {
+              "piecewise_linear": {
+                "$ref": "#/definitions/PiecewiseLinear"
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
+      "Cw20ReceiveMsg": {
+        "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg",
+        "type": "object",
+        "required": [
+          "amount",
+          "msg",
+          "sender"
+        ],
+        "properties": {
+          "amount": {
+            "$ref": "#/definitions/Uint128"
+          },
+          "msg": {
+            "$ref": "#/definitions/Binary"
+          },
+          "sender": {
+            "type": "string"
+          }
+        },
+        "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
+          }
+        ]
+      },
+      "InstantiateMsg": {
+        "type": "object",
+        "required": [
+          "params"
+        ],
+        "properties": {
+          "owner": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "params": {
+            "$ref": "#/definitions/UncheckedVestingParams"
+          }
+        },
+        "additionalProperties": false
+      },
+      "PiecewiseLinear": {
+        "description": "This is a generalization of SaturatingLinear, steps must be arranged with increasing time (u64). Any point before first step gets the first value, after last step the last value. Otherwise, it is a linear interpolation between the two closest points. Vec of length 1 -> Constant Vec of length 2 -> SaturatingLinear",
+        "type": "object",
+        "required": [
+          "steps"
+        ],
+        "properties": {
+          "steps": {
+            "type": "array",
+            "items": {
+              "type": "array",
+              "items": [
+                {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                },
+                {
+                  "$ref": "#/definitions/Uint128"
+                }
+              ],
+              "maxItems": 2,
+              "minItems": 2
+            }
+          }
+        }
+      },
+      "SaturatingLinear": {
+        "description": "min_y for all x <= min_x, max_y for all x >= max_x, linear in between",
+        "type": "object",
+        "required": [
+          "max_x",
+          "max_y",
+          "min_x",
+          "min_y"
+        ],
+        "properties": {
+          "max_x": {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          },
+          "max_y": {
+            "$ref": "#/definitions/Uint128"
+          },
+          "min_x": {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          },
+          "min_y": {
+            "$ref": "#/definitions/Uint128"
+          }
+        }
+      },
+      "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"
+      },
+      "UncheckedDenom": {
+        "description": "A denom that has not been checked to confirm it points to a valid asset.",
+        "oneOf": [
+          {
+            "description": "A native (bank module) asset.",
+            "type": "object",
+            "required": [
+              "native"
+            ],
+            "properties": {
+              "native": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "A cw20 asset.",
+            "type": "object",
+            "required": [
+              "cw20"
+            ],
+            "properties": {
+              "cw20": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
+      "UncheckedVestingParams": {
+        "type": "object",
+        "required": [
+          "amount",
+          "denom",
+          "recipient",
+          "vesting_schedule"
+        ],
+        "properties": {
+          "amount": {
+            "$ref": "#/definitions/Uint128"
+          },
+          "denom": {
+            "$ref": "#/definitions/UncheckedDenom"
+          },
+          "description": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "recipient": {
+            "type": "string"
+          },
+          "title": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "vesting_schedule": {
+            "$ref": "#/definitions/Curve"
+          }
+        },
+        "additionalProperties": false
+      }
+    }
+  },
+  "query": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "QueryMsg",
+    "oneOf": [
+      {
+        "description": "Returns list of all vesting payment contracts",
+        "type": "object",
+        "required": [
+          "list_vesting_contracts"
+        ],
+        "properties": {
+          "list_vesting_contracts": {
+            "type": "object",
+            "properties": {
+              "limit": {
+                "type": [
+                  "integer",
+                  "null"
+                ],
+                "format": "uint32",
+                "minimum": 0.0
+              },
+              "start_after": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns list of all vesting payment contracts in reverse",
+        "type": "object",
+        "required": [
+          "list_vesting_contracts_reverse"
+        ],
+        "properties": {
+          "list_vesting_contracts_reverse": {
+            "type": "object",
+            "properties": {
+              "limit": {
+                "type": [
+                  "integer",
+                  "null"
+                ],
+                "format": "uint32",
+                "minimum": 0.0
+              },
+              "start_before": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns list of all vesting payment contracts by who instantiated them",
+        "type": "object",
+        "required": [
+          "list_vesting_contracts_by_instantiator"
+        ],
+        "properties": {
+          "list_vesting_contracts_by_instantiator": {
+            "type": "object",
+            "required": [
+              "instantiator"
+            ],
+            "properties": {
+              "instantiator": {
+                "type": "string"
+              },
+              "limit": {
+                "type": [
+                  "integer",
+                  "null"
+                ],
+                "format": "uint32",
+                "minimum": 0.0
+              },
+              "start_after": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns list of all vesting payment contracts by who instantiated them in reverse",
+        "type": "object",
+        "required": [
+          "list_vesting_contracts_by_instantiator_reverse"
+        ],
+        "properties": {
+          "list_vesting_contracts_by_instantiator_reverse": {
+            "type": "object",
+            "required": [
+              "instantiator"
+            ],
+            "properties": {
+              "instantiator": {
+                "type": "string"
+              },
+              "limit": {
+                "type": [
+                  "integer",
+                  "null"
+                ],
+                "format": "uint32",
+                "minimum": 0.0
+              },
+              "start_before": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns list of all vesting payment contracts by recipient",
+        "type": "object",
+        "required": [
+          "list_vesting_contracts_by_recipient"
+        ],
+        "properties": {
+          "list_vesting_contracts_by_recipient": {
+            "type": "object",
+            "required": [
+              "recipient"
+            ],
+            "properties": {
+              "limit": {
+                "type": [
+                  "integer",
+                  "null"
+                ],
+                "format": "uint32",
+                "minimum": 0.0
+              },
+              "recipient": {
+                "type": "string"
+              },
+              "start_after": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns list of all vesting payment contracts by recipient in reverse",
+        "type": "object",
+        "required": [
+          "list_vesting_contracts_by_recipient_reverse"
+        ],
+        "properties": {
+          "list_vesting_contracts_by_recipient_reverse": {
+            "type": "object",
+            "required": [
+              "recipient"
+            ],
+            "properties": {
+              "limit": {
+                "type": [
+                  "integer",
+                  "null"
+                ],
+                "format": "uint32",
+                "minimum": 0.0
+              },
+              "recipient": {
+                "type": "string"
+              },
+              "start_before": {
+                "type": [
+                  "string",
+                  "null"
+                ]
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns info about the contract ownership, if set",
+        "type": "object",
+        "required": [
+          "ownership"
+        ],
+        "properties": {
+          "ownership": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns the code ID currently being used to instantiate vesting contracts.",
+        "type": "object",
+        "required": [
+          "code_id"
+        ],
+        "properties": {
+          "code_id": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
+  },
+  "migrate": null,
+  "sudo": null,
+  "responses": {
+    "code_id": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "uint64",
+      "type": "integer",
+      "format": "uint64",
+      "minimum": 0.0
+    },
+    "list_vesting_contracts": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Array_of_Addr",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Addr"
+      },
+      "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"
+        }
+      }
+    },
+    "list_vesting_contracts_by_instantiator": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Array_of_Addr",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Addr"
+      },
+      "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"
+        }
+      }
+    },
+    "list_vesting_contracts_by_instantiator_reverse": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Array_of_Addr",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Addr"
+      },
+      "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"
+        }
+      }
+    },
+    "list_vesting_contracts_by_recipient": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Array_of_Addr",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Addr"
+      },
+      "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"
+        }
+      }
+    },
+    "list_vesting_contracts_by_recipient_reverse": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Array_of_Addr",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Addr"
+      },
+      "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"
+        }
+      }
+    },
+    "list_vesting_contracts_reverse": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "Array_of_Addr",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Addr"
+      },
+      "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"
+        }
+      }
+    },
+    "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/external/cw-payroll-factory/src/contract.rs b/contracts/external/cw-payroll-factory/src/contract.rs
new file mode 100644
index 00000000..b601e0e7
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/src/contract.rs
@@ -0,0 +1,348 @@
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cosmwasm_std::{
+    from_binary, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Order, Reply,
+    Response, StdResult, SubMsg, WasmMsg,
+};
+use cosmwasm_std::{Addr, Coin};
+
+use cw2::set_contract_version;
+use cw20::Cw20ExecuteMsg;
+use cw20::Cw20ReceiveMsg;
+use cw_denom::CheckedDenom;
+use cw_storage_plus::Bound;
+use cw_utils::{nonpayable, parse_reply_instantiate_data};
+use cw_vesting::{
+    msg::{
+        InstantiateMsg as PayrollInstantiateMsg, QueryMsg as PayrollQueryMsg,
+        ReceiveMsg as PayrollReceiveMsg,
+    },
+    state::VestingPayment,
+};
+
+use crate::error::ContractError;
+use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg};
+use crate::state::{vesting_contracts, VestingContract, TMP_INSTANTIATOR_INFO, VESTING_CODE_ID};
+
+pub(crate) const CONTRACT_NAME: &str = "crates.io:cw-payroll-factory";
+pub(crate) const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+pub const INSTANTIATE_CONTRACT_REPLY_ID: u64 = 0;
+pub const DEFAULT_LIMIT: u32 = 10;
+pub const MAX_LIMIT: u32 = 50;
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn instantiate(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    msg: InstantiateMsg,
+) -> Result<Response, ContractError> {
+    cw_ownable::initialize_owner(deps.storage, deps.api, msg.owner.as_deref())?;
+    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
+    VESTING_CODE_ID.save(deps.storage, &msg.vesting_code_id)?;
+    Ok(Response::new()
+        .add_attribute("method", "instantiate")
+        .add_attribute("creator", info.sender))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<Response, ContractError> {
+    match msg {
+        ExecuteMsg::Receive(msg) => execute_receive_cw20(env, deps, info, msg),
+        ExecuteMsg::InstantiateNativePayrollContract {
+            instantiate_msg,
+            label,
+        } => execute_instantiate_native_payroll_contract(deps, info, instantiate_msg, label),
+        ExecuteMsg::UpdateOwnership(action) => execute_update_owner(deps, info, env, action),
+        ExecuteMsg::UpdateCodeId { vesting_code_id } => {
+            execute_update_code_id(deps, info, vesting_code_id)
+        }
+    }
+}
+
+pub fn execute_receive_cw20(
+    _env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    receive_msg: Cw20ReceiveMsg,
+) -> Result<Response, ContractError> {
+    // Only accepts cw20 tokens
+    nonpayable(&info)?;
+
+    let msg: ReceiveMsg = from_binary(&receive_msg.msg)?;
+
+    // Save instantiator info for use in reply (cw20 sender in this case)
+    let sender = deps.api.addr_validate(&receive_msg.sender)?;
+    TMP_INSTANTIATOR_INFO.save(deps.storage, &sender)?;
+
+    match msg {
+        ReceiveMsg::InstantiatePayrollContract {
+            instantiate_msg,
+            label,
+        } => instantiate_contract(deps, sender, None, instantiate_msg, label),
+    }
+}
+
+pub fn execute_instantiate_native_payroll_contract(
+    deps: DepsMut,
+    info: MessageInfo,
+    instantiate_msg: PayrollInstantiateMsg,
+    label: String,
+) -> Result<Response, ContractError> {
+    // Save instantiator info for use in reply
+    TMP_INSTANTIATOR_INFO.save(deps.storage, &info.sender)?;
+
+    instantiate_contract(deps, info.sender, Some(info.funds), instantiate_msg, label)
+}
+
+/// `sender` here refers to the initiator of the vesting, not the
+/// literal sender of the message. Practically speaking, this means
+/// that it should be set to the sender of the cw20's being vested,
+/// and not the cw20 contract when dealing with non-native vesting.
+pub fn instantiate_contract(
+    deps: DepsMut,
+    sender: Addr,
+    funds: Option<Vec<Coin>>,
+    instantiate_msg: PayrollInstantiateMsg,
+    label: String,
+) -> Result<Response, ContractError> {
+    // Check sender is contract owner if set
+    let ownership = cw_ownable::get_ownership(deps.storage)?;
+    if ownership
+        .owner
+        .as_ref()
+        .map_or(false, |owner| *owner != sender)
+    {
+        return Err(ContractError::Unauthorized {});
+    }
+    // No owner is always allowed. If an owner is specified, it must
+    // exactly match the owner of the contract.
+    if instantiate_msg.owner.as_deref().map_or(false, |i| {
+        ownership.owner.as_ref().map_or(true, |o| o.as_str() != i)
+    }) {
+        return Err(ContractError::OwnerMissmatch {
+            actual: instantiate_msg.owner,
+            expected: ownership.owner.map(|a| a.into_string()),
+        });
+    }
+
+    let code_id = VESTING_CODE_ID.load(deps.storage)?;
+
+    // Instantiate the specified contract with owner as the admin.
+    let instantiate = WasmMsg::Instantiate {
+        admin: instantiate_msg.owner.clone(),
+        code_id,
+        msg: to_binary(&instantiate_msg)?,
+        funds: funds.unwrap_or_default(),
+        label,
+    };
+
+    let msg = SubMsg::reply_on_success(instantiate, INSTANTIATE_CONTRACT_REPLY_ID);
+
+    Ok(Response::default()
+        .add_attribute("action", "instantiate_cw_vesting")
+        .add_submessage(msg))
+}
+
+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 execute_update_code_id(
+    deps: DepsMut,
+    info: MessageInfo,
+    vesting_code_id: u64,
+) -> Result<Response, ContractError> {
+    cw_ownable::assert_owner(deps.storage, &info.sender)?;
+    VESTING_CODE_ID.save(deps.storage, &vesting_code_id)?;
+    Ok(Response::default()
+        .add_attribute("action", "update_code_id")
+        .add_attribute("vesting_code_id", vesting_code_id.to_string()))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::ListVestingContracts { start_after, limit } => {
+            let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
+            let start = start_after.as_deref().map(Bound::exclusive);
+
+            let res: Vec<VestingContract> = vesting_contracts()
+                .range(deps.storage, start, None, Order::Ascending)
+                .take(limit)
+                .flat_map(|vc| Ok::<VestingContract, ContractError>(vc?.1))
+                .collect();
+
+            Ok(to_binary(&res)?)
+        }
+        QueryMsg::ListVestingContractsReverse {
+            start_before,
+            limit,
+        } => {
+            let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
+            let start = start_before.as_deref().map(Bound::exclusive);
+
+            let res: Vec<VestingContract> = vesting_contracts()
+                .range(deps.storage, None, start, Order::Descending)
+                .take(limit)
+                .flat_map(|vc| Ok::<VestingContract, ContractError>(vc?.1))
+                .collect();
+
+            Ok(to_binary(&res)?)
+        }
+        QueryMsg::ListVestingContractsByInstantiator {
+            instantiator,
+            start_after,
+            limit,
+        } => {
+            let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
+            let start = start_after.map(Bound::<String>::exclusive);
+
+            // Validate owner address
+            deps.api.addr_validate(&instantiator)?;
+
+            let res: Vec<VestingContract> = vesting_contracts()
+                .idx
+                .instantiator
+                .prefix(instantiator)
+                .range(deps.storage, start, None, Order::Ascending)
+                .take(limit)
+                .flat_map(|vc| Ok::<VestingContract, ContractError>(vc?.1))
+                .collect();
+
+            Ok(to_binary(&res)?)
+        }
+        QueryMsg::ListVestingContractsByInstantiatorReverse {
+            instantiator,
+            start_before,
+            limit,
+        } => {
+            let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
+            let start = start_before.map(Bound::<String>::exclusive);
+
+            // Validate owner address
+            deps.api.addr_validate(&instantiator)?;
+
+            let res: Vec<VestingContract> = vesting_contracts()
+                .idx
+                .instantiator
+                .prefix(instantiator)
+                .range(deps.storage, None, start, Order::Descending)
+                .take(limit)
+                .flat_map(|vc| Ok::<VestingContract, ContractError>(vc?.1))
+                .collect();
+
+            Ok(to_binary(&res)?)
+        }
+        QueryMsg::ListVestingContractsByRecipient {
+            recipient,
+            start_after,
+            limit,
+        } => {
+            let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
+            let start = start_after.map(Bound::<String>::exclusive);
+
+            // Validate recipient address
+            deps.api.addr_validate(&recipient)?;
+
+            let res: Vec<VestingContract> = vesting_contracts()
+                .idx
+                .recipient
+                .prefix(recipient)
+                .range(deps.storage, start, None, Order::Ascending)
+                .take(limit)
+                .flat_map(|vc| Ok::<VestingContract, ContractError>(vc?.1))
+                .collect();
+
+            Ok(to_binary(&res)?)
+        }
+        QueryMsg::ListVestingContractsByRecipientReverse {
+            recipient,
+            start_before,
+            limit,
+        } => {
+            let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
+            let start = start_before.map(Bound::<String>::exclusive);
+
+            // Validate recipient address
+            deps.api.addr_validate(&recipient)?;
+
+            let res: Vec<VestingContract> = vesting_contracts()
+                .idx
+                .recipient
+                .prefix(recipient)
+                .range(deps.storage, None, start, Order::Descending)
+                .take(limit)
+                .flat_map(|vc| Ok::<VestingContract, ContractError>(vc?.1))
+                .collect();
+
+            Ok(to_binary(&res)?)
+        }
+        QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?),
+        QueryMsg::CodeId {} => to_binary(&VESTING_CODE_ID.load(deps.storage)?),
+    }
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn reply(deps: DepsMut, _env: Env, msg: Reply) -> Result<Response, ContractError> {
+    match msg.id {
+        INSTANTIATE_CONTRACT_REPLY_ID => {
+            let res = parse_reply_instantiate_data(msg)?;
+            let contract_addr = deps.api.addr_validate(&res.contract_address)?;
+
+            // Query new vesting payment contract for info
+            let vesting_payment: VestingPayment = deps
+                .querier
+                .query_wasm_smart(contract_addr.clone(), &PayrollQueryMsg::Info {})?;
+
+            let instantiator = TMP_INSTANTIATOR_INFO.load(deps.storage)?;
+
+            // Save vesting contract payment info
+            vesting_contracts().save(
+                deps.storage,
+                contract_addr.as_ref(),
+                &VestingContract {
+                    instantiator: instantiator.to_string(),
+                    recipient: vesting_payment.recipient.to_string(),
+                    contract: contract_addr.to_string(),
+                },
+            )?;
+
+            // Clear tmp instatiator info
+            TMP_INSTANTIATOR_INFO.remove(deps.storage);
+
+            // If cw20, fire off fund message!
+            let msgs: Vec<CosmosMsg> = match vesting_payment.denom {
+                CheckedDenom::Native(_) => vec![],
+                CheckedDenom::Cw20(denom) => {
+                    // Send transaction to fund contract
+                    vec![CosmosMsg::Wasm(WasmMsg::Execute {
+                        contract_addr: denom.to_string(),
+                        msg: to_binary(&Cw20ExecuteMsg::Send {
+                            contract: contract_addr.to_string(),
+                            amount: vesting_payment.amount,
+                            msg: to_binary(&PayrollReceiveMsg::Fund {})?,
+                        })?,
+                        funds: vec![],
+                    })]
+                }
+            };
+
+            Ok(Response::default()
+                .add_attribute("new_payroll_contract", contract_addr)
+                .add_messages(msgs))
+        }
+        _ => Err(ContractError::UnknownReplyId { id: msg.id }),
+    }
+}
diff --git a/contracts/external/cw-payroll-factory/src/error.rs b/contracts/external/cw-payroll-factory/src/error.rs
new file mode 100644
index 00000000..4deb87d3
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/src/error.rs
@@ -0,0 +1,31 @@
+use cosmwasm_std::StdError;
+use cw_ownable::OwnershipError;
+use cw_utils::{ParseReplyError, PaymentError};
+use thiserror::Error;
+
+#[derive(Error, Debug, PartialEq)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error(transparent)]
+    Ownable(#[from] OwnershipError),
+
+    #[error("{0}")]
+    PaymentError(#[from] PaymentError),
+
+    #[error("Unauthorized")]
+    Unauthorized {},
+
+    #[error("instantiate message owner does not match factory owner. got ({actual:?}) expected ({expected:?})")]
+    OwnerMissmatch {
+        actual: Option<String>,
+        expected: Option<String>,
+    },
+
+    #[error("{0}")]
+    ParseReplyError(#[from] ParseReplyError),
+
+    #[error("Got a submessage reply with unknown id: {id}")]
+    UnknownReplyId { id: u64 },
+}
diff --git a/contracts/external/cw-payroll-factory/src/lib.rs b/contracts/external/cw-payroll-factory/src/lib.rs
new file mode 100644
index 00000000..254bec4f
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/src/lib.rs
@@ -0,0 +1,14 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
+pub mod contract;
+mod error;
+pub mod msg;
+pub mod state;
+
+#[cfg(test)]
+mod tests;
+
+// so that consumers don't need a cw_ownable dependency to consume this contract's queries.
+pub use cw_ownable::Ownership;
+
+pub use crate::error::ContractError;
diff --git a/contracts/external/cw-payroll-factory/src/msg.rs b/contracts/external/cw-payroll-factory/src/msg.rs
new file mode 100644
index 00000000..a393e542
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/src/msg.rs
@@ -0,0 +1,88 @@
+use cosmwasm_schema::{cw_serde, QueryResponses};
+use cw20::Cw20ReceiveMsg;
+use cw_ownable::cw_ownable;
+use cw_vesting::msg::InstantiateMsg as PayrollInstantiateMsg;
+
+#[cw_serde]
+pub struct InstantiateMsg {
+    pub owner: Option<String>,
+    pub vesting_code_id: u64,
+}
+
+#[cw_ownable]
+#[cw_serde]
+pub enum ExecuteMsg {
+    /// Instantiates a new vesting contract that is funded by a cw20 token.
+    Receive(Cw20ReceiveMsg),
+    /// Instantiates a new vesting contract that is funded by a native token.
+    InstantiateNativePayrollContract {
+        instantiate_msg: PayrollInstantiateMsg,
+        label: String,
+    },
+
+    /// Callable only by the current owner. Updates the code ID used
+    /// while instantiating vesting contracts.
+    UpdateCodeId { vesting_code_id: u64 },
+}
+
+// Receiver setup
+#[cw_serde]
+pub enum ReceiveMsg {
+    /// Funds a vesting contract with a cw20 token
+    InstantiatePayrollContract {
+        instantiate_msg: PayrollInstantiateMsg,
+        label: String,
+    },
+}
+
+#[cw_serde]
+#[derive(QueryResponses)]
+pub enum QueryMsg {
+    /// Returns list of all vesting payment contracts
+    #[returns(Vec<::cosmwasm_std::Addr>)]
+    ListVestingContracts {
+        start_after: Option<String>,
+        limit: Option<u32>,
+    },
+    /// Returns list of all vesting payment contracts in reverse
+    #[returns(Vec<::cosmwasm_std::Addr>)]
+    ListVestingContractsReverse {
+        start_before: Option<String>,
+        limit: Option<u32>,
+    },
+    /// Returns list of all vesting payment contracts by who instantiated them
+    #[returns(Vec<::cosmwasm_std::Addr>)]
+    ListVestingContractsByInstantiator {
+        instantiator: String,
+        start_after: Option<String>,
+        limit: Option<u32>,
+    },
+    /// Returns list of all vesting payment contracts by who instantiated them in reverse
+    #[returns(Vec<::cosmwasm_std::Addr>)]
+    ListVestingContractsByInstantiatorReverse {
+        instantiator: String,
+        start_before: Option<String>,
+        limit: Option<u32>,
+    },
+    /// Returns list of all vesting payment contracts by recipient
+    #[returns(Vec<::cosmwasm_std::Addr>)]
+    ListVestingContractsByRecipient {
+        recipient: String,
+        start_after: Option<String>,
+        limit: Option<u32>,
+    },
+    /// Returns list of all vesting payment contracts by recipient in reverse
+    #[returns(Vec<::cosmwasm_std::Addr>)]
+    ListVestingContractsByRecipientReverse {
+        recipient: String,
+        start_before: Option<String>,
+        limit: Option<u32>,
+    },
+    /// Returns info about the contract ownership, if set
+    #[returns(::cw_ownable::Ownership<::cosmwasm_std::Addr>)]
+    Ownership {},
+
+    /// Returns the code ID currently being used to instantiate vesting contracts.
+    #[returns(::std::primitive::u64)]
+    CodeId {},
+}
diff --git a/contracts/external/cw-payroll-factory/src/state.rs b/contracts/external/cw-payroll-factory/src/state.rs
new file mode 100644
index 00000000..c65514fa
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/src/state.rs
@@ -0,0 +1,42 @@
+use cosmwasm_schema::cw_serde;
+use cosmwasm_std::Addr;
+use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex};
+
+/// Temporarily holds the address of the instantiator for use in submessages
+pub const TMP_INSTANTIATOR_INFO: Item<Addr> = Item::new("tmp_instantiator_info");
+pub const VESTING_CODE_ID: Item<u64> = Item::new("pci");
+
+#[cw_serde]
+pub struct VestingContract {
+    pub contract: String,
+    pub instantiator: String,
+    pub recipient: String,
+}
+
+pub struct TokenIndexes<'a> {
+    pub instantiator: MultiIndex<'a, String, VestingContract, String>,
+    pub recipient: MultiIndex<'a, String, VestingContract, String>,
+}
+
+impl<'a> IndexList<VestingContract> for TokenIndexes<'a> {
+    fn get_indexes(&'_ self) -> Box<dyn Iterator<Item = &'_ dyn Index<VestingContract>> + '_> {
+        let v: Vec<&dyn Index<VestingContract>> = vec![&self.instantiator, &self.recipient];
+        Box::new(v.into_iter())
+    }
+}
+
+pub fn vesting_contracts<'a>() -> IndexedMap<'a, &'a str, VestingContract, TokenIndexes<'a>> {
+    let indexes = TokenIndexes {
+        instantiator: MultiIndex::new(
+            |_pk: &[u8], d: &VestingContract| d.instantiator.clone(),
+            "vesting_contracts",
+            "vesting_contracts__instantiator",
+        ),
+        recipient: MultiIndex::new(
+            |_pk: &[u8], d: &VestingContract| d.recipient.clone(),
+            "vesting_contracts",
+            "vesting_contracts__recipient",
+        ),
+    };
+    IndexedMap::new("vesting_contracts", indexes)
+}
diff --git a/contracts/external/cw-vesting/.cargo/config b/contracts/external/cw-vesting/.cargo/config
new file mode 100644
index 00000000..8d4bc738
--- /dev/null
+++ b/contracts/external/cw-vesting/.cargo/config
@@ -0,0 +1,6 @@
+[alias]
+wasm = "build --release --target wasm32-unknown-unknown"
+wasm-debug = "build --target wasm32-unknown-unknown"
+unit-test = "test --lib"
+integration-test = "test --test integration"
+schema = "run --example schema"
diff --git a/contracts/external/cw-vesting/Cargo.toml b/contracts/external/cw-vesting/Cargo.toml
new file mode 100644
index 00000000..d2dc2f4c
--- /dev/null
+++ b/contracts/external/cw-vesting/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "cw-vesting"
+version = "2.0.1"
+authors = ["Jake Hartnell", "ekez <ekez@withoutdoing.com>", "blue-note"]
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+# for more explicit tests, cargo test --features=backtraces
+backtraces = ["cosmwasm-std/backtraces"]
+# use library feature to disable all instantiate/execute/query exports
+library = []
+
+[dependencies]
+cosmwasm-std = { workspace = true, features = ["staking"] }
+cosmwasm-schema = { workspace = true }
+cw-denom = { workspace = true }
+cw-ownable = { workspace = true }
+cw-storage-plus = { workspace = true }
+cw-paginate = { workspace = true }
+cw-utils = { workspace = true }
+cw2 = { workspace = true }
+cw20 = { workspace = true }
+thiserror = { workspace = true }
+serde = { workspace = true }
+wynd-utils = { workspace = true }
+
+[dev-dependencies]
+cw-multi-test = { workspace = true }
+cw20-base = { workspace = true }
+dao-testing = { workspace = true }
diff --git a/contracts/external/cw-vesting/README.md b/contracts/external/cw-vesting/README.md
new file mode 100644
index 00000000..79a6ece3
--- /dev/null
+++ b/contracts/external/cw-vesting/README.md
@@ -0,0 +1,39 @@
+# CW Payroll
+
+This contract enables the creation of native && cw20 token streams, which allows a payment to be vested continuously over time. 
+
+Key features include: 
+- Optional contract owner, with ability to cancel payments
+- Support for native and cw20 tokens
+- Allows for automated distribution via external parties
+- For payments in a chain governance token, the ability to stake and claim staking rewards
+- Complex configuration for vesting schedules powered by [wynd-utils](https://github.com/cosmorama/wynddao/tree/main/packages/utils)
+
+## Instantiation
+
+To instantiate a new instance of this contract you may specify a contract owner, as well as payment parameters.
+
+`cw-payroll-factory` can be used if wish to instantiate many `cw-vesting` contracts and query them.
+
+## Creating a CW20 Vesting
+A cw20 vesting payment can be created using the cw20 [Send / Receive](https://github.com/CosmWasm/cw-plus/blob/main/packages/cw20/README.md#receiver) flow. This involves triggering a Send message from the cw20 token contract, with a Receive callback that's sent to the vesting contract.
+
+## Distribute payments
+Vesting payments can be claimed continously at any point after the start time by triggering a Distribute message.
+
+Anyone can call the distribute message, allowing for agents such as [Croncat](https://cron.cat/) to automatically trigger payouts.
+
+## Staking native tokens
+This contract allows for underlying native tokens to be staked.
+
+### Limitations
+While this contract allows for delegating native tokens, it does not allow for voting. As such, be sure to pick validators you delegate to wisely when using this contract.
+
+## Cancelation
+This vesting contract supports optional cancelation. This is only possible if an `owner` is set upon contract instantiation, otherwise the vesting contract cannot be altered by either party.
+
+For example, if an employee has to leave a company for whatever reason, the company can vote to have the employee salary canceled.
+
+When a contract is canceled, funds that have vested up until that moment are paid out to the `recipient` and the rest are refunded to the contract `owner`.
+
+If funds are delegated when a contract is canceled, the delegated funds are immediately unbonded. After newly undelegated funds have finished the unbonding period, they can be withdraw by calling the `distribute` method to resolve.
diff --git a/contracts/external/cw-vesting/examples/schema.rs b/contracts/external/cw-vesting/examples/schema.rs
new file mode 100644
index 00000000..31a0509d
--- /dev/null
+++ b/contracts/external/cw-vesting/examples/schema.rs
@@ -0,0 +1,10 @@
+use cosmwasm_schema::write_api;
+use cw_vesting::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
+
+fn main() {
+    write_api! {
+        instantiate: InstantiateMsg,
+        query: QueryMsg,
+        execute: ExecuteMsg,
+    }
+}
diff --git a/contracts/external/cw-vesting/schema/cw-vesting.json b/contracts/external/cw-vesting/schema/cw-vesting.json
new file mode 100644
index 00000000..acf73f43
--- /dev/null
+++ b/contracts/external/cw-vesting/schema/cw-vesting.json
@@ -0,0 +1,956 @@
+{
+  "contract_name": "cw-vesting",
+  "contract_version": "2.0.1",
+  "idl_version": "1.0.0",
+  "instantiate": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "InstantiateMsg",
+    "type": "object",
+    "required": [
+      "params"
+    ],
+    "properties": {
+      "owner": {
+        "type": [
+          "string",
+          "null"
+        ]
+      },
+      "params": {
+        "$ref": "#/definitions/UncheckedVestingParams"
+      }
+    },
+    "additionalProperties": false,
+    "definitions": {
+      "Curve": {
+        "oneOf": [
+          {
+            "type": "object",
+            "required": [
+              "constant"
+            ],
+            "properties": {
+              "constant": {
+                "type": "object",
+                "required": [
+                  "y"
+                ],
+                "properties": {
+                  "y": {
+                    "$ref": "#/definitions/Uint128"
+                  }
+                }
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "required": [
+              "saturating_linear"
+            ],
+            "properties": {
+              "saturating_linear": {
+                "$ref": "#/definitions/SaturatingLinear"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "type": "object",
+            "required": [
+              "piecewise_linear"
+            ],
+            "properties": {
+              "piecewise_linear": {
+                "$ref": "#/definitions/PiecewiseLinear"
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
+      "PiecewiseLinear": {
+        "description": "This is a generalization of SaturatingLinear, steps must be arranged with increasing time (u64). Any point before first step gets the first value, after last step the last value. Otherwise, it is a linear interpolation between the two closest points. Vec of length 1 -> Constant Vec of length 2 -> SaturatingLinear",
+        "type": "object",
+        "required": [
+          "steps"
+        ],
+        "properties": {
+          "steps": {
+            "type": "array",
+            "items": {
+              "type": "array",
+              "items": [
+                {
+                  "type": "integer",
+                  "format": "uint64",
+                  "minimum": 0.0
+                },
+                {
+                  "$ref": "#/definitions/Uint128"
+                }
+              ],
+              "maxItems": 2,
+              "minItems": 2
+            }
+          }
+        }
+      },
+      "SaturatingLinear": {
+        "description": "min_y for all x <= min_x, max_y for all x >= max_x, linear in between",
+        "type": "object",
+        "required": [
+          "max_x",
+          "max_y",
+          "min_x",
+          "min_y"
+        ],
+        "properties": {
+          "max_x": {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          },
+          "max_y": {
+            "$ref": "#/definitions/Uint128"
+          },
+          "min_x": {
+            "type": "integer",
+            "format": "uint64",
+            "minimum": 0.0
+          },
+          "min_y": {
+            "$ref": "#/definitions/Uint128"
+          }
+        }
+      },
+      "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"
+      },
+      "UncheckedDenom": {
+        "description": "A denom that has not been checked to confirm it points to a valid asset.",
+        "oneOf": [
+          {
+            "description": "A native (bank module) asset.",
+            "type": "object",
+            "required": [
+              "native"
+            ],
+            "properties": {
+              "native": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          },
+          {
+            "description": "A cw20 asset.",
+            "type": "object",
+            "required": [
+              "cw20"
+            ],
+            "properties": {
+              "cw20": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        ]
+      },
+      "UncheckedVestingParams": {
+        "type": "object",
+        "required": [
+          "amount",
+          "denom",
+          "recipient",
+          "vesting_schedule"
+        ],
+        "properties": {
+          "amount": {
+            "$ref": "#/definitions/Uint128"
+          },
+          "denom": {
+            "$ref": "#/definitions/UncheckedDenom"
+          },
+          "description": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "recipient": {
+            "type": "string"
+          },
+          "title": {
+            "type": [
+              "string",
+              "null"
+            ]
+          },
+          "vesting_schedule": {
+            "$ref": "#/definitions/Curve"
+          }
+        },
+        "additionalProperties": false
+      }
+    }
+  },
+  "execute": {
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "title": "ExecuteMsg",
+    "oneOf": [
+      {
+        "description": "Receive a cw20",
+        "type": "object",
+        "required": [
+          "receive"
+        ],
+        "properties": {
+          "receive": {
+            "$ref": "#/definitions/Cw20ReceiveMsg"
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Distribute unlocked vesting tokens",
+        "type": "object",
+        "required": [
+          "distribute"
+        ],
+        "properties": {
+          "distribute": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Cancel vesting contract and return funds to owner (if configured)",
+        "type": "object",
+        "required": [
+          "cancel"
+        ],
+        "properties": {
+          "cancel": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90). `delegator_address` is automatically filled with the current contract's address. Note: this only works with the native staking denom of a Cosmos chain. Only callable by Vesting Payment Recipient.",
+        "type": "object",
+        "required": [
+          "delegate"
+        ],
+        "properties": {
+          "delegate": {
+            "type": "object",
+            "required": [
+              "amount",
+              "validator"
+            ],
+            "properties": {
+              "amount": {
+                "description": "The amount to delegate",
+                "allOf": [
+                  {
+                    "$ref": "#/definitions/Uint128"
+                  }
+                ]
+              },
+              "validator": {
+                "description": "The validator to delegate to",
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L96). `delegator_address` is automatically filled with the current contract's address. Only callable by Vesting Payment Recipient.",
+        "type": "object",
+        "required": [
+          "redelegate"
+        ],
+        "properties": {
+          "redelegate": {
+            "type": "object",
+            "required": [
+              "amount",
+              "dst_validator",
+              "src_validator"
+            ],
+            "properties": {
+              "amount": {
+                "$ref": "#/definitions/Uint128"
+              },
+              "dst_validator": {
+                "type": "string"
+              },
+              "src_validator": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121). `delegator_address` is automatically filled with the current contract's address. Only callable by Vesting Payment Recipient.",
+        "type": "object",
+        "required": [
+          "undelegate"
+        ],
+        "properties": {
+          "undelegate": {
+            "type": "object",
+            "required": [
+              "amount",
+              "validator"
+            ],
+            "properties": {
+              "amount": {
+                "description": "The amount to delegate",
+                "allOf": [
+                  {
+                    "$ref": "#/definitions/Uint128"
+                  }
+                ]
+              },
+              "validator": {
+                "description": "The validator to undelegate from",
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L31-L37). `delegator_address` is automatically filled with the current contract's address. Only callable by Vesting Payment Recipient.",
+        "type": "object",
+        "required": [
+          "set_withdraw_address"
+        ],
+        "properties": {
+          "set_withdraw_address": {
+            "type": "object",
+            "required": [
+              "address"
+            ],
+            "properties": {
+              "address": {
+                "type": "string"
+              }
+            },
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "This is translated to a [MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50). `delegator_address` is automatically filled with the current contract's address. Only callable by Vesting Payment Recipient",
+        "type": "object",
+        "required": [
+          "withdraw_delegator_reward"
+        ],
+        "properties": {
+          "withdraw_delegator_reward": {
+            "type": "object",
+            "required": [
+              "validator"
+            ],
+            "properties": {
+              "validator": {
+                "description": "The `validator_address` to claim rewards for",
+                "type": "string"
+              }
+            },
+            "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"
+            ]
+          }
+        ]
+      },
+      "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"
+      },
+      "Cw20ReceiveMsg": {
+        "description": "Cw20ReceiveMsg should be de/serialized under `Receive()` variant in a ExecuteMsg",
+        "type": "object",
+        "required": [
+          "amount",
+          "msg",
+          "sender"
+        ],
+        "properties": {
+          "amount": {
+            "$ref": "#/definitions/Uint128"
+          },
+          "msg": {
+            "$ref": "#/definitions/Binary"
+          },
+          "sender": {
+            "type": "string"
+          }
+        },
+        "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
+          }
+        ]
+      },
+      "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": [
+      {
+        "description": "Returns info about the vesting payment",
+        "type": "object",
+        "required": [
+          "info"
+        ],
+        "properties": {
+          "info": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns info about the contract ownership, if set",
+        "type": "object",
+        "required": [
+          "ownership"
+        ],
+        "properties": {
+          "ownership": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      },
+      {
+        "description": "Returns the amount of funds that have vested at the current block",
+        "type": "object",
+        "required": [
+          "vested_amount"
+        ],
+        "properties": {
+          "vested_amount": {
+            "type": "object",
+            "additionalProperties": false
+          }
+        },
+        "additionalProperties": false
+      }
+    ]
+  },
+  "migrate": null,
+  "sudo": null,
+  "responses": {
+    "info": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "VestingPayment",
+      "type": "object",
+      "required": [
+        "amount",
+        "claimed_amount",
+        "denom",
+        "recipient",
+        "staked_amount",
+        "status",
+        "vesting_schedule"
+      ],
+      "properties": {
+        "amount": {
+          "description": "Vesting amount in Native and Cw20 tokens",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Uint128"
+            }
+          ]
+        },
+        "canceled_at_time": {
+          "description": "Canceled at time in seconds, only set if contract is canceled",
+          "type": [
+            "integer",
+            "null"
+          ],
+          "format": "uint64",
+          "minimum": 0.0
+        },
+        "claimed_amount": {
+          "description": "Amount claimed so far",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Uint128"
+            }
+          ]
+        },
+        "denom": {
+          "description": "The denom of a token (cw20 or native)",
+          "allOf": [
+            {
+              "$ref": "#/definitions/CheckedDenom"
+            }
+          ]
+        },
+        "description": {
+          "description": "Description of the payroll item, a more in depth description of how to meet the payroll conditions",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "recipient": {
+          "description": "The recipient for the vesting payment",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Addr"
+            }
+          ]
+        },
+        "staked_amount": {
+          "description": "The amount of the vesting payment that has been staked",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Uint128"
+            }
+          ]
+        },
+        "status": {
+          "description": "The status of the vesting payment",
+          "allOf": [
+            {
+              "$ref": "#/definitions/VestingPaymentStatus"
+            }
+          ]
+        },
+        "title": {
+          "description": "Title of the payroll item, for example for a bug bounty \"Fix issue in contract.rs\"",
+          "type": [
+            "string",
+            "null"
+          ]
+        },
+        "vesting_schedule": {
+          "description": "Vesting schedule",
+          "allOf": [
+            {
+              "$ref": "#/definitions/Curve"
+            }
+          ]
+        }
+      },
+      "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"
+        },
+        "CheckedDenom": {
+          "description": "A denom that has been checked to point to a valid asset. This enum should never be constructed literally and should always be built by calling `into_checked` on an `UncheckedDenom` instance.",
+          "oneOf": [
+            {
+              "description": "A native (bank module) asset.",
+              "type": "object",
+              "required": [
+                "native"
+              ],
+              "properties": {
+                "native": {
+                  "type": "string"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "description": "A cw20 asset.",
+              "type": "object",
+              "required": [
+                "cw20"
+              ],
+              "properties": {
+                "cw20": {
+                  "$ref": "#/definitions/Addr"
+                }
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "Curve": {
+          "oneOf": [
+            {
+              "type": "object",
+              "required": [
+                "constant"
+              ],
+              "properties": {
+                "constant": {
+                  "type": "object",
+                  "required": [
+                    "y"
+                  ],
+                  "properties": {
+                    "y": {
+                      "$ref": "#/definitions/Uint128"
+                    }
+                  }
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "object",
+              "required": [
+                "saturating_linear"
+              ],
+              "properties": {
+                "saturating_linear": {
+                  "$ref": "#/definitions/SaturatingLinear"
+                }
+              },
+              "additionalProperties": false
+            },
+            {
+              "type": "object",
+              "required": [
+                "piecewise_linear"
+              ],
+              "properties": {
+                "piecewise_linear": {
+                  "$ref": "#/definitions/PiecewiseLinear"
+                }
+              },
+              "additionalProperties": false
+            }
+          ]
+        },
+        "PiecewiseLinear": {
+          "description": "This is a generalization of SaturatingLinear, steps must be arranged with increasing time (u64). Any point before first step gets the first value, after last step the last value. Otherwise, it is a linear interpolation between the two closest points. Vec of length 1 -> Constant Vec of length 2 -> SaturatingLinear",
+          "type": "object",
+          "required": [
+            "steps"
+          ],
+          "properties": {
+            "steps": {
+              "type": "array",
+              "items": {
+                "type": "array",
+                "items": [
+                  {
+                    "type": "integer",
+                    "format": "uint64",
+                    "minimum": 0.0
+                  },
+                  {
+                    "$ref": "#/definitions/Uint128"
+                  }
+                ],
+                "maxItems": 2,
+                "minItems": 2
+              }
+            }
+          }
+        },
+        "SaturatingLinear": {
+          "description": "min_y for all x <= min_x, max_y for all x >= max_x, linear in between",
+          "type": "object",
+          "required": [
+            "max_x",
+            "max_y",
+            "min_x",
+            "min_y"
+          ],
+          "properties": {
+            "max_x": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            },
+            "max_y": {
+              "$ref": "#/definitions/Uint128"
+            },
+            "min_x": {
+              "type": "integer",
+              "format": "uint64",
+              "minimum": 0.0
+            },
+            "min_y": {
+              "$ref": "#/definitions/Uint128"
+            }
+          }
+        },
+        "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"
+        },
+        "VestingPaymentStatus": {
+          "type": "string",
+          "enum": [
+            "active",
+            "canceled",
+            "canceled_and_unbonding",
+            "fully_vested",
+            "unfunded"
+          ]
+        }
+      }
+    },
+    "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"
+        }
+      }
+    },
+    "vested_amount": {
+      "$schema": "http://json-schema.org/draft-07/schema#",
+      "title": "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/external/cw-vesting/src/contract.rs b/contracts/external/cw-vesting/src/contract.rs
new file mode 100644
index 00000000..8c8b149d
--- /dev/null
+++ b/contracts/external/cw-vesting/src/contract.rs
@@ -0,0 +1,564 @@
+#[cfg(not(feature = "library"))]
+use cosmwasm_std::entry_point;
+use cosmwasm_std::{
+    from_binary, to_binary, Binary, Coin, CosmosMsg, Deps, DepsMut, DistributionMsg, Env,
+    MessageInfo, Order, Response, StakingMsg, StdResult, Uint128,
+};
+use cw2::set_contract_version;
+use cw20::Cw20ReceiveMsg;
+use cw_denom::{CheckedDenom, UncheckedDenom};
+use cw_utils::{must_pay, nonpayable};
+
+use crate::error::ContractError;
+use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg};
+use crate::state::{
+    VestingPayment, VestingPaymentStatus, STAKED_VESTING_BY_VALIDATOR, VESTING_PAYMENT,
+};
+
+const CONTRACT_NAME: &str = "crates.io:cw-vesting";
+const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn instantiate(
+    deps: DepsMut,
+    _env: Env,
+    info: MessageInfo,
+    msg: InstantiateMsg,
+) -> Result<Response, ContractError> {
+    set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
+
+    cw_ownable::initialize_owner(deps.storage, deps.api, msg.owner.as_deref())?;
+
+    let mut msgs: Vec<CosmosMsg> = vec![];
+
+    // Check all params and create vesting payment with status based on denom
+    let vp = match msg.params.denom {
+        UncheckedDenom::Native(ref denom) => {
+            // Validate all vesting payment params
+            let checked_params = msg.params.clone().into_checked(deps.as_ref())?;
+
+            // Check amount sent matches amount in vesting payment
+            if checked_params.amount != must_pay(&info, denom)? {
+                return Err(ContractError::AmountDoesNotMatch);
+            }
+
+            // If the native denom matches the bonded denom, set withdraw address
+            // to the recipient so that any staking rewards go to the recipient
+            let local_denom = deps.querier.query_bonded_denom()?;
+            if local_denom == *denom {
+                msgs.push(CosmosMsg::Distribution(
+                    DistributionMsg::SetWithdrawAddress {
+                        address: checked_params.recipient.to_string(),
+                    },
+                ))
+            }
+
+            // Create a new vesting payment
+            VestingPayment::new(checked_params)?
+        }
+        UncheckedDenom::Cw20(_) => {
+            // If configured for cw20, check no native funds included
+            nonpayable(&info)?;
+
+            // Validate all vesting payment params
+            let checked_params = msg.params.into_checked(deps.as_ref())?;
+
+            // Create new vesting payment
+            let mut vesting_payment = VestingPayment::new(checked_params)?;
+
+            // Set status to unfunded
+            vesting_payment.status = VestingPaymentStatus::Unfunded;
+
+            vesting_payment
+        }
+    };
+
+    VESTING_PAYMENT.save(deps.storage, &vp)?;
+
+    Ok(Response::new()
+        .add_attribute("method", "instantiate")
+        .add_attribute("owner", msg.owner.unwrap_or_else(|| "None".to_string()))
+        .add_messages(msgs))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn execute(
+    deps: DepsMut,
+    env: Env,
+    info: MessageInfo,
+    msg: ExecuteMsg,
+) -> Result<Response, ContractError> {
+    match msg {
+        ExecuteMsg::Receive(msg) => execute_receive_cw20(env, deps, info, msg),
+        ExecuteMsg::Distribute {} => execute_distribute(env, deps),
+        ExecuteMsg::Cancel {} => execute_cancel_vesting_payment(env, deps, info),
+        ExecuteMsg::UpdateOwnership(action) => execute_update_owner(deps, info, env, action),
+        ExecuteMsg::Delegate { validator, amount } => {
+            execute_delegate(env, deps, info, validator, amount)
+        }
+        ExecuteMsg::Redelegate {
+            src_validator,
+            dst_validator,
+            amount,
+        } => execute_redelegate(env, deps, info, src_validator, dst_validator, amount),
+        ExecuteMsg::Undelegate { validator, amount } => {
+            execute_undelegate(env, deps, info, validator, amount)
+        }
+        ExecuteMsg::SetWithdrawAddress { address } => {
+            execute_set_withdraw_address(deps, info, address)
+        }
+        ExecuteMsg::WithdrawDelegatorReward { validator } => execute_withdraw_rewards(validator),
+    }
+}
+
+pub fn execute_receive_cw20(
+    _env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    receive_msg: Cw20ReceiveMsg,
+) -> Result<Response, ContractError> {
+    // Only accepts cw20 tokens
+    nonpayable(&info)?;
+
+    let msg: ReceiveMsg = from_binary(&receive_msg.msg)?;
+
+    match msg {
+        ReceiveMsg::Fund {} => {
+            let mut vesting_payment = VESTING_PAYMENT.load(deps.storage)?;
+
+            // Check amount sent matches amount in vesting payment
+            if vesting_payment.amount != receive_msg.amount {
+                return Err(ContractError::AmountDoesNotMatch);
+            }
+
+            // Check that the Cw20 specified in the denom *must be* matches sender
+            if !vesting_payment.denom.is_cw20(&info.sender) {
+                return Err(ContractError::Cw20DoesNotMatch);
+            }
+
+            // Check status is Unfunded to prevent funding twice
+            if vesting_payment.status != VestingPaymentStatus::Unfunded {
+                return Err(ContractError::AlreadyFunded);
+            }
+
+            // Update vesting payment status
+            vesting_payment.status = VestingPaymentStatus::Active;
+            VESTING_PAYMENT.save(deps.storage, &vesting_payment)?;
+
+            Ok(Response::new()
+                .add_attribute("method", "fund_cw20_vesting_payment")
+                .add_attribute("recipient", vesting_payment.recipient.to_string()))
+        }
+    }
+}
+
+pub fn execute_cancel_vesting_payment(
+    env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+) -> Result<Response, ContractError> {
+    // Check sender is contract owner
+    cw_ownable::assert_owner(deps.storage, &info.sender)?;
+
+    let mut msgs: Vec<CosmosMsg> = vec![];
+
+    let vesting_payment =
+        VESTING_PAYMENT.update(deps.storage, |mut vp| -> Result<_, ContractError> {
+            // Check if contract status is active
+            if vp.status != VestingPaymentStatus::Active {
+                return Err(ContractError::NotActive);
+            }
+
+            // Set status based on whether any funds are staked
+            if vp.staked_amount == Uint128::zero() {
+                vp.status = VestingPaymentStatus::Canceled;
+            } else {
+                vp.status = VestingPaymentStatus::CanceledAndUnbonding;
+                // Set canceled at time for use when distributing unbonded funds
+                vp.canceled_at_time = Some(env.block.time.seconds());
+            }
+            Ok(vp)
+        })?;
+
+    // In no funds are staked, we distibute contract funds
+    // Check if funds have vested or there are staking rewards
+    let vested_amount = vesting_payment.get_vested_amount_by_seconds(env.block.time.seconds())?;
+
+    // Transfer any vested amount to the original recipient
+    if vested_amount != Uint128::zero() {
+        let transfer_vested_to_recipient_msg = vesting_payment
+            .denom
+            .get_transfer_to_message(&vesting_payment.recipient, vested_amount)?;
+        msgs.push(transfer_vested_to_recipient_msg);
+    }
+
+    // Transfer any remaining unvested amount to the owner
+    let unvested = vesting_payment.amount.checked_sub(vested_amount)?;
+    if unvested != Uint128::zero() {
+        let transfer_unvested_to_owner_msg = vesting_payment
+            .denom
+            .get_transfer_to_message(&info.sender, unvested)?;
+        msgs.push(transfer_unvested_to_owner_msg);
+    }
+
+    // Handle edge case if funds are staked at the time of cancelation
+    if vesting_payment.status == VestingPaymentStatus::CanceledAndUnbonding {
+        let denom = deps.querier.query_bonded_denom()?;
+
+        // Unbond any staked funds
+        let mut undelegate_msgs = STAKED_VESTING_BY_VALIDATOR
+            .range(deps.storage, None, None, Order::Ascending)
+            .flat_map(|svc| -> StdResult<CosmosMsg> {
+                let (validator, amount) = svc?;
+                Ok(CosmosMsg::Staking(StakingMsg::Undelegate {
+                    validator,
+                    amount: Coin {
+                        denom: denom.clone(),
+                        amount,
+                    },
+                }))
+            })
+            .collect::<Vec<CosmosMsg>>();
+
+        msgs.append(&mut undelegate_msgs);
+    }
+
+    Ok(Response::new()
+        .add_attribute("method", "remove_vesting_payment")
+        .add_attribute("owner", info.sender)
+        .add_attribute("removed_time", env.block.time.to_string())
+        .add_messages(msgs))
+}
+
+pub fn execute_distribute(env: Env, deps: DepsMut) -> Result<Response, ContractError> {
+    let vesting_payment = VESTING_PAYMENT.load(deps.storage)?;
+
+    // Get vested amount based on vesting payment status
+    let vested_amount = match vesting_payment.status {
+        // If canceled and unbonding use canceled time
+        VestingPaymentStatus::CanceledAndUnbonding => vesting_payment
+            .get_vested_amount_by_seconds(
+                vesting_payment
+                    .canceled_at_time
+                    .unwrap_or_else(|| env.block.time.seconds()),
+            )?,
+        // Otherwise use current block time
+        _ => vesting_payment.get_vested_amount_by_seconds(env.block.time.seconds())?,
+    };
+
+    // Check there are funds to distribute
+    if vested_amount == Uint128::zero() {
+        return Err(ContractError::NoFundsToClaim);
+    }
+
+    // Update Vesting Payment with claimed amount and the correct status
+    VESTING_PAYMENT.update(deps.storage, |mut vp| -> Result<_, ContractError> {
+        // Decrease vesting amount
+        vp.amount = vp.amount.checked_sub(vested_amount)?;
+        // Increase the claimed amount
+        vp.claimed_amount = vp.claimed_amount.checked_add(vested_amount)?;
+
+        // Check if canceled and unbonding
+        if vp.status == VestingPaymentStatus::CanceledAndUnbonding {
+            // Set status to canceled as funds have unbonded if this executes
+            vp.status = VestingPaymentStatus::Canceled
+        }
+
+        // If the amount remaining in contract goes to zero, update status
+        if vp.amount == Uint128::zero() {
+            vp.status = VestingPaymentStatus::FullyVested
+        }
+        Ok(vp)
+    })?;
+
+    let mut msgs: Vec<CosmosMsg> = vec![];
+
+    // Get transfer message for recipient payout
+    if vested_amount != Uint128::zero() {
+        let transfer_msg = vesting_payment
+            .denom
+            .get_transfer_to_message(&vesting_payment.recipient, vested_amount)?;
+        msgs.push(transfer_msg);
+    }
+
+    // Get owner's payout message in event of canceled and unbonding
+    if vesting_payment.status == VestingPaymentStatus::CanceledAndUnbonding {
+        let unvested_amount = vesting_payment.amount.checked_sub(vested_amount)?;
+        let owner = cw_ownable::get_ownership(deps.storage)?;
+        if let Some(owner) = owner.owner {
+            if unvested_amount != Uint128::zero() {
+                let withdraw_unbonded_msg = vesting_payment
+                    .denom
+                    .get_transfer_to_message(&owner, unvested_amount)?;
+                msgs.push(withdraw_unbonded_msg);
+            }
+        }
+    }
+
+    Ok(Response::new()
+        .add_attribute("method", "distribute")
+        .add_attribute("vested_amount", vested_amount)
+        .add_messages(msgs))
+}
+
+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 execute_delegate(
+    _env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    validator: String,
+    amount: Uint128,
+) -> Result<Response, ContractError> {
+    let vesting_payment = VESTING_PAYMENT.load(deps.storage)?;
+
+    // Check sender is the recipient of the vesting payment
+    if info.sender != vesting_payment.recipient {
+        return Err(ContractError::Unauthorized);
+    }
+
+    // Non-payable as this method delegates funds in the contract
+    nonpayable(&info)?;
+
+    // Check status is active
+    if vesting_payment.status != VestingPaymentStatus::Active {
+        return Err(ContractError::NotActive);
+    }
+
+    // Check vesting payment denom matches local denom
+    let denom = match vesting_payment.denom {
+        CheckedDenom::Cw20(_) => {
+            return Err(ContractError::NotStakeable);
+        }
+        CheckedDenom::Native(denom) => {
+            // Get local denom, ensure it matches the vesting denom
+            let local_denom = deps.querier.query_bonded_denom()?;
+            if local_denom != denom {
+                return Err(ContractError::NotStakeable);
+            } else {
+                denom
+            }
+        }
+    };
+
+    // Update vesting payment with amount of staked rewards
+    VESTING_PAYMENT.update(deps.storage, |mut vp| -> Result<_, ContractError> {
+        // Check stake amount is not greater than vesting payment amount
+        if amount > vp.amount {
+            return Err(ContractError::NotEnoughFunds {});
+        }
+
+        if vp.amount < vp.staked_amount.checked_add(amount)? {
+            return Err(ContractError::StakingMoreThanVesting);
+        }
+
+        // Update amounts
+        vp.staked_amount = vp.staked_amount.checked_add(amount)?;
+        Ok(vp)
+    })?;
+
+    // Save a record of staking this vesting payment amount to a validator
+    match STAKED_VESTING_BY_VALIDATOR.may_load(deps.storage, &validator)? {
+        // If already staked to this validator, increase staked amount
+        Some(staked_amount) => STAKED_VESTING_BY_VALIDATOR.save(
+            deps.storage,
+            &validator,
+            &staked_amount.checked_add(amount)?,
+        ),
+        // If not currently staked to this validator save a new record with staked amount
+        None => STAKED_VESTING_BY_VALIDATOR.save(deps.storage, &validator, &amount),
+    }?;
+
+    // Create message to delegate the underlying tokens
+    let msg = StakingMsg::Delegate {
+        validator: validator.clone(),
+        amount: Coin { denom, amount },
+    };
+
+    Ok(Response::new()
+        .add_attribute("method", "delegate")
+        .add_attribute("amount", amount.to_string())
+        .add_attribute("validator", validator)
+        .add_message(msg))
+}
+
+pub fn execute_redelegate(
+    _env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    src_validator: String,
+    dst_validator: String,
+    amount: Uint128,
+) -> Result<Response, ContractError> {
+    let vesting_payment = VESTING_PAYMENT.load(deps.storage)?;
+
+    // Check sender is the recipient of the vesting payment
+    if info.sender != vesting_payment.recipient {
+        return Err(ContractError::Unauthorized);
+    }
+
+    // Non-payable as this method redelegates funds in the contract
+    nonpayable(&info)?;
+
+    if src_validator == dst_validator {
+        return Err(ContractError::SameValidator);
+    }
+
+    // Check status is active
+    if vesting_payment.status != VestingPaymentStatus::Active {
+        return Err(ContractError::NotActive);
+    }
+
+    // Check vesting payment denom matches local denom
+    let denom = match vesting_payment.denom {
+        CheckedDenom::Cw20(_) => {
+            return Err(ContractError::NotStakeable);
+        }
+        CheckedDenom::Native(denom) => {
+            // Get local denom, ensure it matches the vesting denom
+            let local_denom = deps.querier.query_bonded_denom()?;
+            if local_denom != denom {
+                return Err(ContractError::NotStakeable);
+            } else {
+                denom
+            }
+        }
+    };
+
+    // Load source validator, subtract redelegated amount
+    let amount_staked_src = STAKED_VESTING_BY_VALIDATOR
+        .load(deps.storage, &src_validator)
+        .map_err(|_| ContractError::NoDelegationsForValidator {})?;
+
+    STAKED_VESTING_BY_VALIDATOR.save(
+        deps.storage,
+        &src_validator,
+        &amount_staked_src.checked_sub(amount)?,
+    )?;
+
+    // Load destination validator, increase amount
+    match STAKED_VESTING_BY_VALIDATOR.may_load(deps.storage, &dst_validator)? {
+        // If already staked to this validator, increase staked amount
+        Some(staked_amount) => STAKED_VESTING_BY_VALIDATOR.save(
+            deps.storage,
+            &src_validator,
+            &staked_amount.checked_add(amount)?,
+        ),
+        // If not currently staked to this validator save a new record with staked amount
+        None => STAKED_VESTING_BY_VALIDATOR.save(deps.storage, &dst_validator, &amount),
+    }?;
+
+    // Create message to delegate the underlying tokens
+    let msg = StakingMsg::Redelegate {
+        src_validator: src_validator.clone(),
+        dst_validator: dst_validator.clone(),
+        amount: Coin { denom, amount },
+    };
+
+    Ok(Response::new()
+        .add_attribute("method", "redelegate")
+        .add_attribute("amount", amount.to_string())
+        .add_attribute("src_validator", src_validator)
+        .add_attribute("dst_validator", dst_validator)
+        .add_message(msg))
+}
+
+pub fn execute_undelegate(
+    _env: Env,
+    deps: DepsMut,
+    info: MessageInfo,
+    validator: String,
+    amount: Uint128,
+) -> Result<Response, ContractError> {
+    let vesting_payment = VESTING_PAYMENT.load(deps.storage)?;
+
+    // Check sender is the recipient of the vesting payment
+    if info.sender != vesting_payment.recipient {
+        return Err(ContractError::Unauthorized);
+    }
+
+    // Update vesting payment with amount of staked rewards
+    VESTING_PAYMENT.update(deps.storage, |mut vp| -> Result<_, ContractError> {
+        // Check unstake amount is not greater than vesting payment amount
+        if amount > vp.staked_amount {
+            return Err(ContractError::NotEnoughFunds {});
+        }
+
+        // Update amounts
+        vp.staked_amount = vp.staked_amount.checked_sub(amount)?;
+        Ok(vp)
+    })?;
+
+    let denom = deps.querier.query_bonded_denom()?;
+
+    // Create message to delegate the underlying tokens
+    let msg = StakingMsg::Undelegate {
+        validator: validator.clone(),
+        amount: Coin { denom, amount },
+    };
+
+    // Update amount delegated to validator
+    STAKED_VESTING_BY_VALIDATOR.update(deps.storage, &validator, |staked_amount| {
+        match staked_amount {
+            Some(staked_amount) => Ok(staked_amount.checked_sub(amount)?),
+            None => Err(ContractError::NoDelegationsForValidator {}),
+        }
+    })?;
+
+    Ok(Response::default().add_message(msg))
+}
+
+pub fn execute_set_withdraw_address(
+    deps: DepsMut,
+    info: MessageInfo,
+    address: String,
+) -> Result<Response, ContractError> {
+    let vesting_payment = VESTING_PAYMENT.load(deps.storage)?;
+
+    // Check sender is the recipient of the vesting payment
+    if info.sender != vesting_payment.recipient {
+        return Err(ContractError::Unauthorized);
+    }
+
+    // Set withdraw address
+    let msg = DistributionMsg::SetWithdrawAddress {
+        address: address.clone(),
+    };
+
+    Ok(Response::default()
+        .add_attribute("method", "set_withdraw_address")
+        .add_attribute("address", address)
+        .add_message(msg))
+}
+
+pub fn execute_withdraw_rewards(validator: String) -> Result<Response, ContractError> {
+    // Withdraw rewards from validator
+    let withdraw_msg = DistributionMsg::WithdrawDelegatorReward { validator };
+
+    Ok(Response::default().add_message(withdraw_msg))
+}
+
+#[cfg_attr(not(feature = "library"), entry_point)]
+pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
+    match msg {
+        QueryMsg::Info {} => to_binary(&VESTING_PAYMENT.load(deps.storage)?),
+        QueryMsg::Ownership {} => to_binary(&cw_ownable::get_ownership(deps.storage)?),
+        QueryMsg::VestedAmount {} => {
+            let vesting_payment = VESTING_PAYMENT.load(deps.storage)?;
+
+            to_binary(
+                &vesting_payment
+                    .vesting_schedule
+                    .value(env.block.time.seconds()),
+            )
+        }
+    }
+}
diff --git a/contracts/external/cw-vesting/src/error.rs b/contracts/external/cw-vesting/src/error.rs
new file mode 100644
index 00000000..b7e4b183
--- /dev/null
+++ b/contracts/external/cw-vesting/src/error.rs
@@ -0,0 +1,107 @@
+use cosmwasm_std::{
+    CheckedFromRatioError, DecimalRangeExceeded, DivideByZeroError, OverflowError, StdError,
+};
+use cw_denom::DenomError;
+use cw_ownable::OwnershipError;
+use cw_utils::PaymentError;
+use thiserror::Error;
+use wynd_utils::CurveError;
+
+#[derive(Error, Debug, PartialEq)]
+pub enum ContractError {
+    #[error("{0}")]
+    Std(#[from] StdError),
+
+    #[error(transparent)]
+    CheckedFromRatioError(#[from] CheckedFromRatioError),
+
+    #[error("{0}")]
+    Curve(#[from] CurveError),
+
+    #[error(transparent)]
+    Denom(#[from] DenomError),
+
+    #[error(transparent)]
+    DivideByZeroError(#[from] DivideByZeroError),
+
+    #[error(transparent)]
+    DecimalRangeExceeded(#[from] DecimalRangeExceeded),
+
+    #[error(transparent)]
+    Ownable(#[from] OwnershipError),
+
+    #[error("{0}")]
+    OverflowErr(#[from] OverflowError),
+
+    #[error("{0}")]
+    PaymentError(#[from] PaymentError),
+
+    #[error("Contract has already been funded")]
+    AlreadyFunded,
+
+    #[error("Amount sent does not match vesting amount")]
+    AmountDoesNotMatch,
+
+    #[error("Cw20 contract does not match vesting denom")]
+    Cw20DoesNotMatch,
+
+    #[error("Fully vested")]
+    FullyVested,
+
+    #[error("Title must be less than 280 characters and not be an empty string")]
+    InvalidTitle,
+
+    #[error("Cannot undelegate more than you previously delegated")]
+    InsufficientDelegation {},
+
+    #[error("Only callable if vesting payment is active")]
+    NotActive,
+
+    #[error("Must redelegate to a different validator")]
+    SameValidator,
+
+    #[error("Vesting payment does not have enough funds")]
+    NotEnoughFunds {},
+
+    #[error("Cannot undelegate or claim rewards from a validator that does not have delegations")]
+    NoDelegationsForValidator {},
+
+    #[error("No tokens have vested at the moment")]
+    NoFundsToClaim,
+
+    #[error("Contract has run out of funds to delegate")]
+    NoFundsToDelegate {},
+
+    #[error("Only callable if contract has been canceled and is unbonding")]
+    NotCanceledAndUnbonding,
+
+    #[error("Tokens for this vesting payment are not stakeable")]
+    NotStakeable,
+
+    #[error("The transfer will never become fully vested. Must hit 0 eventually")]
+    NeverFullyVested,
+
+    #[error("This account is unauthorized to perform the transaction")]
+    Unauthorized,
+
+    #[error("The transfer tries to vest a different amount of tokens than it sends")]
+    VestsDifferently,
+
+    #[error("Nothing to vest, amount is zero")]
+    NothingToVest,
+
+    #[error("Cannot stake more than what is vesting")]
+    StakingMoreThanVesting,
+
+    #[error("Vesting Payment has been cancelled by contract owner")]
+    VestingPaymentCanceled,
+
+    #[error("Vesting Payment does not exist")]
+    VestingPaymentNotFound,
+
+    #[error("Rewards amount is 0")]
+    ZeroRewardsToSend {},
+
+    #[error("Validator is not delegated to")]
+    ValidatorNotDelegatedTo {},
+}
diff --git a/contracts/external/cw-vesting/src/lib.rs b/contracts/external/cw-vesting/src/lib.rs
new file mode 100644
index 00000000..fdb83b5a
--- /dev/null
+++ b/contracts/external/cw-vesting/src/lib.rs
@@ -0,0 +1,14 @@
+#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
+
+pub mod contract;
+mod error;
+pub mod msg;
+pub mod state;
+
+pub use crate::error::ContractError;
+
+// so that consumers don't need a cw_ownable dependency to consume this contract's queries.
+pub use cw_ownable::Ownership;
+
+#[cfg(test)]
+mod tests;
diff --git a/contracts/external/cw-vesting/src/msg.rs b/contracts/external/cw-vesting/src/msg.rs
new file mode 100644
index 00000000..cbeed100
--- /dev/null
+++ b/contracts/external/cw-vesting/src/msg.rs
@@ -0,0 +1,82 @@
+use cosmwasm_schema::{cw_serde, QueryResponses};
+use cosmwasm_std::Uint128;
+use cw20::Cw20ReceiveMsg;
+use cw_ownable::cw_ownable;
+
+use crate::state::UncheckedVestingParams;
+
+#[cw_serde]
+pub struct InstantiateMsg {
+    pub owner: Option<String>,
+    pub params: UncheckedVestingParams,
+}
+
+#[cw_ownable]
+#[cw_serde]
+pub enum ExecuteMsg {
+    /// Receive a cw20
+    Receive(Cw20ReceiveMsg),
+    /// Distribute unlocked vesting tokens
+    Distribute {},
+    /// Cancel vesting contract and return funds to owner (if configured)
+    Cancel {},
+    /// This is translated to a [MsgDelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L81-L90).
+    /// `delegator_address` is automatically filled with the current contract's address.
+    /// Note: this only works with the native staking denom of a Cosmos chain.
+    /// Only callable by Vesting Payment Recipient.
+    Delegate {
+        /// The validator to delegate to
+        validator: String,
+        /// The amount to delegate
+        amount: Uint128,
+    },
+    /// This is translated to a [MsgBeginRedelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L96).
+    /// `delegator_address` is automatically filled with the current contract's address.
+    /// Only callable by Vesting Payment Recipient.
+    Redelegate {
+        src_validator: String,
+        dst_validator: String,
+        amount: Uint128,
+    },
+    /// This is translated to a [MsgUndelegate](https://github.com/cosmos/cosmos-sdk/blob/v0.40.0/proto/cosmos/staking/v1beta1/tx.proto#L112-L121).
+    /// `delegator_address` is automatically filled with the current contract's address.
+    /// Only callable by Vesting Payment Recipient.
+    Undelegate {
+        /// The validator to undelegate from
+        validator: String,
+        /// The amount to delegate
+        amount: Uint128,
+    },
+    /// This is translated to a [MsgSetWithdrawAddress](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L31-L37).
+    /// `delegator_address` is automatically filled with the current contract's address.
+    /// Only callable by Vesting Payment Recipient.
+    SetWithdrawAddress { address: String },
+    /// This is translated to a [MsgWithdrawDelegatorReward](https://github.com/cosmos/cosmos-sdk/blob/v0.42.4/proto/cosmos/distribution/v1beta1/tx.proto#L42-L50).
+    /// `delegator_address` is automatically filled with the current contract's address.
+    /// Only callable by Vesting Payment Recipient
+    WithdrawDelegatorReward {
+        /// The `validator_address` to claim rewards for
+        validator: String,
+    },
+}
+
+// Receiver setup
+#[cw_serde]
+pub enum ReceiveMsg {
+    /// Funds a vesting contract with a cw20 token
+    Fund {},
+}
+
+#[cw_serde]
+#[derive(QueryResponses)]
+pub enum QueryMsg {
+    /// Returns info about the vesting payment
+    #[returns(crate::state::VestingPayment)]
+    Info {},
+    /// Returns info about the contract ownership, if set
+    #[returns(::cw_ownable::Ownership<::cosmwasm_std::Addr>)]
+    Ownership {},
+    /// Returns the amount of funds that have vested at the current block
+    #[returns(::cosmwasm_std::Uint128)]
+    VestedAmount {},
+}
diff --git a/contracts/external/cw-vesting/src/state.rs b/contracts/external/cw-vesting/src/state.rs
new file mode 100644
index 00000000..dc6f754d
--- /dev/null
+++ b/contracts/external/cw-vesting/src/state.rs
@@ -0,0 +1,308 @@
+use cosmwasm_schema::cw_serde;
+use cosmwasm_std::{Addr, Deps, OverflowError, Uint128};
+use cw_storage_plus::{Item, Map};
+
+use crate::ContractError;
+use cw_denom::{CheckedDenom, UncheckedDenom};
+
+use wynd_utils::Curve;
+
+#[cw_serde]
+pub struct UncheckedVestingParams {
+    pub recipient: String,       // address
+    pub amount: Uint128,         // non-zero
+    pub denom: UncheckedDenom,   // valid
+    pub vesting_schedule: Curve, // high == amount && low == 0
+    pub title: Option<String>,
+    pub description: Option<String>,
+}
+
+#[cw_serde]
+pub struct CheckedVestingParams {
+    pub recipient: Addr,
+    pub amount: Uint128,
+    pub denom: CheckedDenom,
+    pub vesting_schedule: Curve,
+    pub title: Option<String>,
+    pub description: Option<String>,
+}
+
+impl UncheckedVestingParams {
+    pub fn into_checked(self, deps: Deps) -> Result<CheckedVestingParams, ContractError> {
+        if self.amount.is_zero() {
+            return Err(ContractError::NothingToVest);
+        }
+
+        // Check vesting schedule
+        self.assert_schedule_vests_amount()?;
+
+        // Check valid recipient address
+        let recipient = deps.api.addr_validate(&self.recipient)?;
+
+        // Check denom
+        let checked_denom = self.denom.into_checked(deps)?;
+
+        // If title is included, validate title length (max 280 characters)
+        if let Some(ref title) = self.title {
+            if title.len() > 280 || title.is_empty() {
+                return Err(ContractError::InvalidTitle);
+            }
+        }
+
+        Ok(CheckedVestingParams {
+            recipient,
+            amount: self.amount,
+            denom: checked_denom,
+            vesting_schedule: self.vesting_schedule,
+            title: self.title,
+            description: self.description,
+        })
+    }
+
+    /// Asserts the vesting schedule decreases to 0 eventually, 2and is never more than the
+    /// amount being sent. If it doesn't match these conditions, returns an error.
+    pub fn assert_schedule_vests_amount(&self) -> Result<(), ContractError> {
+        self.vesting_schedule.validate_monotonic_decreasing()?;
+        let (low, high) = self.vesting_schedule.range();
+        if low == 0 && high == self.amount.u128() {
+            Ok(())
+        } else if low != 0 {
+            Err(ContractError::NeverFullyVested)
+        } else {
+            // high != amount
+            Err(ContractError::VestsDifferently)
+        }
+    }
+}
+
+#[cw_serde]
+pub enum VestingPaymentStatus {
+    Active,
+    Canceled,
+    CanceledAndUnbonding,
+    FullyVested,
+    Unfunded,
+}
+
+#[cw_serde]
+pub struct VestingPayment {
+    /// The recipient for the vesting payment
+    pub recipient: Addr,
+    /// Vesting amount in Native and Cw20 tokens
+    pub amount: Uint128,
+    /// Amount claimed so far
+    pub claimed_amount: Uint128,
+    /// Canceled at time in seconds, only set if contract is canceled
+    pub canceled_at_time: Option<u64>,
+    /// Vesting schedule
+    pub vesting_schedule: Curve,
+    /// The denom of a token (cw20 or native)
+    pub denom: CheckedDenom,
+    /// Title of the payroll item, for example for a bug bounty "Fix issue in contract.rs"
+    pub title: Option<String>,
+    /// Description of the payroll item, a more in depth description of how to meet the payroll conditions
+    pub description: Option<String>,
+    /// The status of the vesting payment
+    pub status: VestingPaymentStatus,
+    /// The amount of the vesting payment that has been staked
+    pub staked_amount: Uint128,
+}
+
+impl VestingPayment {
+    /// Create a new VestingPayment from CheckedVestingParams
+    pub fn new(checked_vesting_params: CheckedVestingParams) -> Result<Self, ContractError> {
+        let vesting_payment = Self {
+            status: VestingPaymentStatus::Active,
+            claimed_amount: Uint128::zero(),
+            staked_amount: Uint128::zero(),
+            canceled_at_time: None,
+            recipient: checked_vesting_params.recipient,
+            amount: checked_vesting_params.amount,
+            denom: checked_vesting_params.denom,
+            vesting_schedule: checked_vesting_params.vesting_schedule,
+            title: checked_vesting_params.title,
+            description: checked_vesting_params.description,
+        };
+
+        Ok(vesting_payment)
+    }
+
+    pub fn get_vested_amount_by_seconds(&self, time: u64) -> Result<Uint128, OverflowError> {
+        let vesting_funds = self.vesting_schedule.value(time);
+        self.amount.checked_sub(vesting_funds)
+    }
+}
+
+/// Holds information about the vesting payment
+pub const VESTING_PAYMENT: Item<VestingPayment> = Item::new("vesting_payment");
+/// A map of staked vesting claims by validator
+pub const STAKED_VESTING_BY_VALIDATOR: Map<&str, Uint128> = Map::new("staked_vesting_by_validator");
+
+#[cfg(test)]
+mod tests {
+    use cosmwasm_std::testing::{mock_dependencies, mock_env};
+    use wynd_utils::PiecewiseLinear;
+
+    use super::*;
+
+    #[test]
+    fn test_catches_vests_more_than_sent() {
+        let deps = mock_dependencies();
+        let amount = Uint128::new(10);
+        let start = mock_env().block.time.seconds();
+        let end = start + 10_000;
+        let vesting_schedule = Curve::saturating_linear((start, 69), (end, 0));
+        let params = UncheckedVestingParams {
+            recipient: "test".to_string(),
+            amount,
+            denom: UncheckedDenom::Native("ujuno".to_string()),
+            vesting_schedule,
+            title: None,
+            description: None,
+        };
+        let err = params.into_checked(deps.as_ref()).unwrap_err();
+        assert_eq!(err, ContractError::VestsDifferently);
+    }
+
+    #[test]
+    fn test_catches_never_fully_vesting() {
+        let deps = mock_dependencies();
+        let amount = Uint128::new(11223344);
+        let start = mock_env().block.time.seconds();
+        let end = start + 10_000;
+        let vesting_schedule = Curve::saturating_linear((start, amount.into()), (end, 1));
+        let params = UncheckedVestingParams {
+            recipient: "test".to_string(),
+            amount,
+            denom: UncheckedDenom::Native("ujuno".to_string()),
+            vesting_schedule,
+            title: None,
+            description: None,
+        };
+        let err = params.into_checked(deps.as_ref()).unwrap_err();
+        assert_eq!(err, ContractError::NeverFullyVested);
+    }
+
+    #[test]
+    fn test_catches_non_decreasing_curve() {
+        let deps = mock_dependencies();
+        let amount = Uint128::new(11223344);
+        let start = mock_env().block.time.seconds();
+        let end = start + 10_000;
+        let vesting_schedule = Curve::saturating_linear((start, 0), (end, amount.into()));
+        let params = UncheckedVestingParams {
+            recipient: "test".to_string(),
+            amount,
+            denom: UncheckedDenom::Native("ujuno".to_string()),
+            vesting_schedule,
+            title: None,
+            description: None,
+        };
+        let err = params.into_checked(deps.as_ref()).unwrap_err();
+        assert_eq!(
+            err,
+            ContractError::Curve(wynd_utils::CurveError::MonotonicIncreasing)
+        );
+    }
+
+    #[test]
+    fn test_valdiate_title() {
+        let deps = mock_dependencies();
+        let amount = Uint128::new(11223344);
+        let start = mock_env().block.time.seconds();
+        let end = start + 10_000;
+        let vesting_schedule = Curve::saturating_linear((start, amount.into()), (end, 0));
+
+        // Catch empty string title
+        let params = UncheckedVestingParams {
+            recipient: "test".to_string(),
+            amount,
+            denom: UncheckedDenom::Native("ujuno".to_string()),
+            vesting_schedule: vesting_schedule.clone(),
+            title: Some("".to_string()),
+            description: None,
+        };
+        let err = params.into_checked(deps.as_ref()).unwrap_err();
+        assert_eq!(err, ContractError::InvalidTitle);
+
+        // Catch long title
+        let params = UncheckedVestingParams {
+            recipient: "test".to_string(),
+            amount,
+            denom: UncheckedDenom::Native("ujuno".to_string()),
+            vesting_schedule,
+            title: Some(
+                "
+                58
+
+                If a country is governed with tolerance,
+                the people are comfortable and honest.
+                If a country is governed with repression,
+                the people are depressed and crafty.
+
+                When the will to power is in charge,
+                the higher the ideals, the lower the results.
+                Try to make people happy,
+                and you lay the groundwork for misery.
+                Try to make people moral,
+                and you lay the groundwork for vice.
+
+                Thus the Master is content
+                to serve as an example
+                and not to impose her will.
+                She is pointed, but doesn't pierce.
+                Straightforward, but supple.
+                Radiant, but easy on the eyes.
+
+                59
+
+                For governing a country well
+                there is nothing better than moderation.
+
+                The mark of a moderate man
+                is freedom from his own ideas.
+                Tolerant like the sky,
+                all-pervading like sunlight,
+                firm like a mountain,
+                supple like a tree in the wind,
+                he has no destination in view
+                and makes use of anything
+                life happens to bring his way.
+
+                Nothing is impossible for him.
+                Because he has let go,
+                he can care for the people's welfare
+                as a mother cares for her child.
+
+                (Text sourced from https://www.organism.earth/library/document/tao-te-ching)
+                "
+                .to_string(),
+            ),
+            description: None,
+        };
+        let err = params.into_checked(deps.as_ref()).unwrap_err();
+        assert_eq!(err, ContractError::InvalidTitle);
+    }
+
+    #[test]
+    fn test_validate_piecewise_vessting_schedule() {
+        let deps = mock_dependencies();
+        let amount = Uint128::new(11223344);
+        let start = mock_env().block.time.seconds();
+        let complexity = 100;
+        let steps: Vec<_> = (0..complexity)
+            .map(|x| (start + x, amount - Uint128::from(x)))
+            .chain(std::iter::once((start + complexity, Uint128::new(0)))) // fully vest
+            .collect();
+        let vesting_schedule = Curve::PiecewiseLinear(PiecewiseLinear { steps });
+        let params = UncheckedVestingParams {
+            recipient: "test".to_string(),
+            amount,
+            denom: UncheckedDenom::Native("ujuno".to_string()),
+            vesting_schedule,
+            title: None,
+            description: None,
+        };
+        params.into_checked(deps.as_ref()).unwrap();
+    }
+}
diff --git a/packages/cw-denom/src/lib.rs b/packages/cw-denom/src/lib.rs
index 1c6f63b7..5a8cfaee 100644
--- a/packages/cw-denom/src/lib.rs
+++ b/packages/cw-denom/src/lib.rs
@@ -78,6 +78,44 @@ impl UncheckedDenom {
 }
 
 impl CheckedDenom {
+    /// Is the `CheckedDenom` this cw20?
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use cosmwasm_std::{Addr, coin};
+    /// use cw_denom::CheckedDenom;
+    ///
+    /// let cw20 = Addr::unchecked("fleesp");
+    /// assert!(CheckedDenom::Cw20(Addr::unchecked("fleesp")).is_cw20(&cw20));
+    /// assert!(!CheckedDenom::Native("fleesp".to_string()).is_cw20(&cw20));
+    /// ```
+    pub fn is_cw20(&self, cw20: &Addr) -> bool {
+        match self {
+            CheckedDenom::Native(_) => false,
+            CheckedDenom::Cw20(a) => a == cw20,
+        }
+    }
+
+    /// Is the `CheckedDenom` this native denom?
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use cosmwasm_std::{Addr, coin};
+    /// use cw_denom::CheckedDenom;
+    ///
+    /// let coin = coin(10, "floob");
+    /// assert!(CheckedDenom::Native("floob".to_string()).is_native(&coin.denom));
+    /// assert!(!CheckedDenom::Cw20(Addr::unchecked("floob")).is_native(&coin.denom));
+    /// ```
+    pub fn is_native(&self, denom: &str) -> bool {
+        match self {
+            CheckedDenom::Native(n) => n == denom,
+            CheckedDenom::Cw20(_) => false,
+        }
+    }
+
     /// Queries WHO's balance for the denomination.
     pub fn query_balance<C: CustomQuery>(
         &self,
diff --git a/typescript/contracts/cw-payroll-factory/CwPayrollFactory.client.ts b/typescript/contracts/cw-payroll-factory/CwPayrollFactory.client.ts
new file mode 100644
index 00000000..baaa952b
--- /dev/null
+++ b/typescript/contracts/cw-payroll-factory/CwPayrollFactory.client.ts
@@ -0,0 +1,278 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
+import { Coin, StdFee } from "@cosmjs/amino";
+import { InstantiateMsg, ExecuteMsg, Uint128, Binary, UncheckedDenom, Curve, Action, Expiration, Timestamp, Uint64, Cw20ReceiveMsg, UncheckedVestingParams, SaturatingLinear, PiecewiseLinear, QueryMsg, Addr, ArrayOfAddr, OwnershipForAddr } from "./CwPayrollFactory.types";
+export interface CwPayrollFactoryReadOnlyInterface {
+  contractAddress: string;
+  listVestingContracts: ({
+    limit,
+    startAfter
+  }: {
+    limit?: number;
+    startAfter?: string;
+  }) => Promise<ArrayOfAddr>;
+  listVestingContractsReverse: ({
+    limit,
+    startBefore
+  }: {
+    limit?: number;
+    startBefore?: string;
+  }) => Promise<ArrayOfAddr>;
+  listVestingContractsByInstantiator: ({
+    instantiator,
+    limit,
+    startAfter
+  }: {
+    instantiator: string;
+    limit?: number;
+    startAfter?: string;
+  }) => Promise<ArrayOfAddr>;
+  listVestingContractsByInstantiatorReverse: ({
+    instantiator,
+    limit,
+    startBefore
+  }: {
+    instantiator: string;
+    limit?: number;
+    startBefore?: string;
+  }) => Promise<ArrayOfAddr>;
+  listVestingContractsByRecipient: ({
+    limit,
+    recipient,
+    startAfter
+  }: {
+    limit?: number;
+    recipient: string;
+    startAfter?: string;
+  }) => Promise<ArrayOfAddr>;
+  listVestingContractsByRecipientReverse: ({
+    limit,
+    recipient,
+    startBefore
+  }: {
+    limit?: number;
+    recipient: string;
+    startBefore?: string;
+  }) => Promise<ArrayOfAddr>;
+  ownership: () => Promise<OwnershipForAddr>;
+  codeId: () => Promise<Uint64>;
+}
+export class CwPayrollFactoryQueryClient implements CwPayrollFactoryReadOnlyInterface {
+  client: CosmWasmClient;
+  contractAddress: string;
+
+  constructor(client: CosmWasmClient, contractAddress: string) {
+    this.client = client;
+    this.contractAddress = contractAddress;
+    this.listVestingContracts = this.listVestingContracts.bind(this);
+    this.listVestingContractsReverse = this.listVestingContractsReverse.bind(this);
+    this.listVestingContractsByInstantiator = this.listVestingContractsByInstantiator.bind(this);
+    this.listVestingContractsByInstantiatorReverse = this.listVestingContractsByInstantiatorReverse.bind(this);
+    this.listVestingContractsByRecipient = this.listVestingContractsByRecipient.bind(this);
+    this.listVestingContractsByRecipientReverse = this.listVestingContractsByRecipientReverse.bind(this);
+    this.ownership = this.ownership.bind(this);
+    this.codeId = this.codeId.bind(this);
+  }
+
+  listVestingContracts = async ({
+    limit,
+    startAfter
+  }: {
+    limit?: number;
+    startAfter?: string;
+  }): Promise<ArrayOfAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      list_vesting_contracts: {
+        limit,
+        start_after: startAfter
+      }
+    });
+  };
+  listVestingContractsReverse = async ({
+    limit,
+    startBefore
+  }: {
+    limit?: number;
+    startBefore?: string;
+  }): Promise<ArrayOfAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      list_vesting_contracts_reverse: {
+        limit,
+        start_before: startBefore
+      }
+    });
+  };
+  listVestingContractsByInstantiator = async ({
+    instantiator,
+    limit,
+    startAfter
+  }: {
+    instantiator: string;
+    limit?: number;
+    startAfter?: string;
+  }): Promise<ArrayOfAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      list_vesting_contracts_by_instantiator: {
+        instantiator,
+        limit,
+        start_after: startAfter
+      }
+    });
+  };
+  listVestingContractsByInstantiatorReverse = async ({
+    instantiator,
+    limit,
+    startBefore
+  }: {
+    instantiator: string;
+    limit?: number;
+    startBefore?: string;
+  }): Promise<ArrayOfAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      list_vesting_contracts_by_instantiator_reverse: {
+        instantiator,
+        limit,
+        start_before: startBefore
+      }
+    });
+  };
+  listVestingContractsByRecipient = async ({
+    limit,
+    recipient,
+    startAfter
+  }: {
+    limit?: number;
+    recipient: string;
+    startAfter?: string;
+  }): Promise<ArrayOfAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      list_vesting_contracts_by_recipient: {
+        limit,
+        recipient,
+        start_after: startAfter
+      }
+    });
+  };
+  listVestingContractsByRecipientReverse = async ({
+    limit,
+    recipient,
+    startBefore
+  }: {
+    limit?: number;
+    recipient: string;
+    startBefore?: string;
+  }): Promise<ArrayOfAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      list_vesting_contracts_by_recipient_reverse: {
+        limit,
+        recipient,
+        start_before: startBefore
+      }
+    });
+  };
+  ownership = async (): Promise<OwnershipForAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      ownership: {}
+    });
+  };
+  codeId = async (): Promise<Uint64> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      code_id: {}
+    });
+  };
+}
+export interface CwPayrollFactoryInterface extends CwPayrollFactoryReadOnlyInterface {
+  contractAddress: string;
+  sender: string;
+  receive: ({
+    amount,
+    msg,
+    sender
+  }: {
+    amount: Uint128;
+    msg: Binary;
+    sender: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  instantiateNativePayrollContract: ({
+    instantiateMsg,
+    label
+  }: {
+    instantiateMsg: InstantiateMsg;
+    label: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  updateCodeId: ({
+    vestingCodeId
+  }: {
+    vestingCodeId: number;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  updateOwnership: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+}
+export class CwPayrollFactoryClient extends CwPayrollFactoryQueryClient implements CwPayrollFactoryInterface {
+  client: SigningCosmWasmClient;
+  sender: string;
+  contractAddress: string;
+
+  constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) {
+    super(client, contractAddress);
+    this.client = client;
+    this.sender = sender;
+    this.contractAddress = contractAddress;
+    this.receive = this.receive.bind(this);
+    this.instantiateNativePayrollContract = this.instantiateNativePayrollContract.bind(this);
+    this.updateCodeId = this.updateCodeId.bind(this);
+    this.updateOwnership = this.updateOwnership.bind(this);
+  }
+
+  receive = async ({
+    amount,
+    msg,
+    sender
+  }: {
+    amount: Uint128;
+    msg: Binary;
+    sender: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      receive: {
+        amount,
+        msg,
+        sender
+      }
+    }, fee, memo, funds);
+  };
+  instantiateNativePayrollContract = async ({
+    instantiateMsg,
+    label
+  }: {
+    instantiateMsg: InstantiateMsg;
+    label: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      instantiate_native_payroll_contract: {
+        instantiate_msg: instantiateMsg,
+        label
+      }
+    }, fee, memo, funds);
+  };
+  updateCodeId = async ({
+    vestingCodeId
+  }: {
+    vestingCodeId: number;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      update_code_id: {
+        vesting_code_id: vestingCodeId
+      }
+    }, 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/cw-payroll-factory/CwPayrollFactory.types.ts b/typescript/contracts/cw-payroll-factory/CwPayrollFactory.types.ts
new file mode 100644
index 00000000..e79dd6cc
--- /dev/null
+++ b/typescript/contracts/cw-payroll-factory/CwPayrollFactory.types.ts
@@ -0,0 +1,126 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+export interface InstantiateMsg {
+  owner?: string | null;
+  params: UncheckedVestingParams;
+}
+export type ExecuteMsg = {
+  receive: Cw20ReceiveMsg;
+} | {
+  instantiate_native_payroll_contract: {
+    instantiate_msg: InstantiateMsg;
+    label: string;
+  };
+} | {
+  update_code_id: {
+    vesting_code_id: number;
+  };
+} | {
+  update_ownership: Action;
+};
+export type Uint128 = string;
+export type Binary = string;
+export type UncheckedDenom = {
+  native: string;
+} | {
+  cw20: string;
+};
+export type Curve = {
+  constant: {
+    y: Uint128;
+    [k: string]: unknown;
+  };
+} | {
+  saturating_linear: SaturatingLinear;
+} | {
+  piecewise_linear: PiecewiseLinear;
+};
+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;
+  sender: string;
+}
+export interface UncheckedVestingParams {
+  amount: Uint128;
+  denom: UncheckedDenom;
+  description?: string | null;
+  recipient: string;
+  title?: string | null;
+  vesting_schedule: Curve;
+}
+export interface SaturatingLinear {
+  max_x: number;
+  max_y: Uint128;
+  min_x: number;
+  min_y: Uint128;
+  [k: string]: unknown;
+}
+export interface PiecewiseLinear {
+  steps: [number, Uint128][];
+  [k: string]: unknown;
+}
+export type QueryMsg = {
+  list_vesting_contracts: {
+    limit?: number | null;
+    start_after?: string | null;
+  };
+} | {
+  list_vesting_contracts_reverse: {
+    limit?: number | null;
+    start_before?: string | null;
+  };
+} | {
+  list_vesting_contracts_by_instantiator: {
+    instantiator: string;
+    limit?: number | null;
+    start_after?: string | null;
+  };
+} | {
+  list_vesting_contracts_by_instantiator_reverse: {
+    instantiator: string;
+    limit?: number | null;
+    start_before?: string | null;
+  };
+} | {
+  list_vesting_contracts_by_recipient: {
+    limit?: number | null;
+    recipient: string;
+    start_after?: string | null;
+  };
+} | {
+  list_vesting_contracts_by_recipient_reverse: {
+    limit?: number | null;
+    recipient: string;
+    start_before?: string | null;
+  };
+} | {
+  ownership: {};
+} | {
+  code_id: {};
+};
+export type Addr = string;
+export type ArrayOfAddr = 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/cw-payroll-factory/bundle.ts b/typescript/contracts/cw-payroll-factory/bundle.ts
new file mode 100644
index 00000000..808cd512
--- /dev/null
+++ b/typescript/contracts/cw-payroll-factory/bundle.ts
@@ -0,0 +1,13 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+import * as _22 from "./CwPayrollFactory.types";
+import * as _23 from "./CwPayrollFactory.client";
+export namespace contracts {
+  export const CwPayrollFactory = { ..._22,
+    ..._23
+  };
+}
\ No newline at end of file
diff --git a/typescript/contracts/cw-vesting/CwVesting.client.ts b/typescript/contracts/cw-vesting/CwVesting.client.ts
new file mode 100644
index 00000000..0876e930
--- /dev/null
+++ b/typescript/contracts/cw-vesting/CwVesting.client.ts
@@ -0,0 +1,213 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate";
+import { Coin, StdFee } from "@cosmjs/amino";
+import { Uint128, UncheckedDenom, Curve, InstantiateMsg, UncheckedVestingParams, SaturatingLinear, PiecewiseLinear, ExecuteMsg, Binary, Action, Expiration, Timestamp, Uint64, Cw20ReceiveMsg, QueryMsg, CheckedDenom, Addr, VestingPaymentStatus, VestingPayment, OwnershipForAddr } from "./CwVesting.types";
+export interface CwVestingReadOnlyInterface {
+  contractAddress: string;
+  info: () => Promise<VestingPayment>;
+  ownership: () => Promise<OwnershipForAddr>;
+  vestedAmount: () => Promise<Uint128>;
+}
+export class CwVestingQueryClient implements CwVestingReadOnlyInterface {
+  client: CosmWasmClient;
+  contractAddress: string;
+
+  constructor(client: CosmWasmClient, contractAddress: string) {
+    this.client = client;
+    this.contractAddress = contractAddress;
+    this.info = this.info.bind(this);
+    this.ownership = this.ownership.bind(this);
+    this.vestedAmount = this.vestedAmount.bind(this);
+  }
+
+  info = async (): Promise<VestingPayment> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      info: {}
+    });
+  };
+  ownership = async (): Promise<OwnershipForAddr> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      ownership: {}
+    });
+  };
+  vestedAmount = async (): Promise<Uint128> => {
+    return this.client.queryContractSmart(this.contractAddress, {
+      vested_amount: {}
+    });
+  };
+}
+export interface CwVestingInterface extends CwVestingReadOnlyInterface {
+  contractAddress: string;
+  sender: string;
+  receive: ({
+    amount,
+    msg,
+    sender
+  }: {
+    amount: Uint128;
+    msg: Binary;
+    sender: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  distribute: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  cancel: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  delegate: ({
+    amount,
+    validator
+  }: {
+    amount: Uint128;
+    validator: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  redelegate: ({
+    amount,
+    dstValidator,
+    srcValidator
+  }: {
+    amount: Uint128;
+    dstValidator: string;
+    srcValidator: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  undelegate: ({
+    amount,
+    validator
+  }: {
+    amount: Uint128;
+    validator: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  setWithdrawAddress: ({
+    address
+  }: {
+    address: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  withdrawDelegatorReward: ({
+    validator
+  }: {
+    validator: string;
+  }, fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+  updateOwnership: (fee?: number | StdFee | "auto", memo?: string, funds?: Coin[]) => Promise<ExecuteResult>;
+}
+export class CwVestingClient extends CwVestingQueryClient implements CwVestingInterface {
+  client: SigningCosmWasmClient;
+  sender: string;
+  contractAddress: string;
+
+  constructor(client: SigningCosmWasmClient, sender: string, contractAddress: string) {
+    super(client, contractAddress);
+    this.client = client;
+    this.sender = sender;
+    this.contractAddress = contractAddress;
+    this.receive = this.receive.bind(this);
+    this.distribute = this.distribute.bind(this);
+    this.cancel = this.cancel.bind(this);
+    this.delegate = this.delegate.bind(this);
+    this.redelegate = this.redelegate.bind(this);
+    this.undelegate = this.undelegate.bind(this);
+    this.setWithdrawAddress = this.setWithdrawAddress.bind(this);
+    this.withdrawDelegatorReward = this.withdrawDelegatorReward.bind(this);
+    this.updateOwnership = this.updateOwnership.bind(this);
+  }
+
+  receive = async ({
+    amount,
+    msg,
+    sender
+  }: {
+    amount: Uint128;
+    msg: Binary;
+    sender: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      receive: {
+        amount,
+        msg,
+        sender
+      }
+    }, fee, memo, funds);
+  };
+  distribute = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      distribute: {}
+    }, fee, memo, funds);
+  };
+  cancel = async (fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      cancel: {}
+    }, fee, memo, funds);
+  };
+  delegate = async ({
+    amount,
+    validator
+  }: {
+    amount: Uint128;
+    validator: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      delegate: {
+        amount,
+        validator
+      }
+    }, fee, memo, funds);
+  };
+  redelegate = async ({
+    amount,
+    dstValidator,
+    srcValidator
+  }: {
+    amount: Uint128;
+    dstValidator: string;
+    srcValidator: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      redelegate: {
+        amount,
+        dst_validator: dstValidator,
+        src_validator: srcValidator
+      }
+    }, fee, memo, funds);
+  };
+  undelegate = async ({
+    amount,
+    validator
+  }: {
+    amount: Uint128;
+    validator: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      undelegate: {
+        amount,
+        validator
+      }
+    }, fee, memo, funds);
+  };
+  setWithdrawAddress = async ({
+    address
+  }: {
+    address: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      set_withdraw_address: {
+        address
+      }
+    }, fee, memo, funds);
+  };
+  withdrawDelegatorReward = async ({
+    validator
+  }: {
+    validator: string;
+  }, fee: number | StdFee | "auto" = "auto", memo?: string, funds?: Coin[]): Promise<ExecuteResult> => {
+    return await this.client.execute(this.sender, this.contractAddress, {
+      withdraw_delegator_reward: {
+        validator
+      }
+    }, 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/cw-vesting/CwVesting.types.ts b/typescript/contracts/cw-vesting/CwVesting.types.ts
new file mode 100644
index 00000000..25df3cbc
--- /dev/null
+++ b/typescript/contracts/cw-vesting/CwVesting.types.ts
@@ -0,0 +1,130 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+export type Uint128 = string;
+export type UncheckedDenom = {
+  native: string;
+} | {
+  cw20: string;
+};
+export type Curve = {
+  constant: {
+    y: Uint128;
+    [k: string]: unknown;
+  };
+} | {
+  saturating_linear: SaturatingLinear;
+} | {
+  piecewise_linear: PiecewiseLinear;
+};
+export interface InstantiateMsg {
+  owner?: string | null;
+  params: UncheckedVestingParams;
+}
+export interface UncheckedVestingParams {
+  amount: Uint128;
+  denom: UncheckedDenom;
+  description?: string | null;
+  recipient: string;
+  title?: string | null;
+  vesting_schedule: Curve;
+}
+export interface SaturatingLinear {
+  max_x: number;
+  max_y: Uint128;
+  min_x: number;
+  min_y: Uint128;
+  [k: string]: unknown;
+}
+export interface PiecewiseLinear {
+  steps: [number, Uint128][];
+  [k: string]: unknown;
+}
+export type ExecuteMsg = {
+  receive: Cw20ReceiveMsg;
+} | {
+  distribute: {};
+} | {
+  cancel: {};
+} | {
+  delegate: {
+    amount: Uint128;
+    validator: string;
+  };
+} | {
+  redelegate: {
+    amount: Uint128;
+    dst_validator: string;
+    src_validator: string;
+  };
+} | {
+  undelegate: {
+    amount: Uint128;
+    validator: string;
+  };
+} | {
+  set_withdraw_address: {
+    address: string;
+  };
+} | {
+  withdraw_delegator_reward: {
+    validator: string;
+  };
+} | {
+  update_ownership: Action;
+};
+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;
+  sender: string;
+}
+export type QueryMsg = {
+  info: {};
+} | {
+  ownership: {};
+} | {
+  vested_amount: {};
+};
+export type CheckedDenom = {
+  native: string;
+} | {
+  cw20: Addr;
+};
+export type Addr = string;
+export type VestingPaymentStatus = "active" | "canceled" | "canceled_and_unbonding" | "fully_vested" | "unfunded";
+export interface VestingPayment {
+  amount: Uint128;
+  canceled_at_time?: number | null;
+  claimed_amount: Uint128;
+  denom: CheckedDenom;
+  description?: string | null;
+  recipient: Addr;
+  staked_amount: Uint128;
+  status: VestingPaymentStatus;
+  title?: string | null;
+  vesting_schedule: Curve;
+}
+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/cw-vesting/bundle.ts b/typescript/contracts/cw-vesting/bundle.ts
new file mode 100644
index 00000000..5772c57a
--- /dev/null
+++ b/typescript/contracts/cw-vesting/bundle.ts
@@ -0,0 +1,13 @@
+/**
+* This file was automatically generated by @cosmwasm/ts-codegen@0.19.0.
+* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+* and run the @cosmwasm/ts-codegen generate command to regenerate this file.
+*/
+
+import * as _4 from "./CwVesting.types";
+import * as _5 from "./CwVesting.client";
+export namespace contracts {
+  export const CwVesting = { ..._4,
+    ..._5
+  };
+}
\ No newline at end of file
diff --git a/typescript/contracts/cw20-stake-external-rewards/bundle.ts b/typescript/contracts/cw20-stake-external-rewards/bundle.ts
index 3a7128fd..0f6903f3 100644
--- a/typescript/contracts/cw20-stake-external-rewards/bundle.ts
+++ b/typescript/contracts/cw20-stake-external-rewards/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _6 from "./Cw20StakeExternalRewards.types";
-import * as _7 from "./Cw20StakeExternalRewards.client";
+import * as _8 from "./Cw20StakeExternalRewards.types";
+import * as _9 from "./Cw20StakeExternalRewards.client";
 export namespace contracts {
-  export const Cw20StakeExternalRewards = { ..._6,
-    ..._7
+  export const Cw20StakeExternalRewards = { ..._8,
+    ..._9
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/cw20-stake-reward-distributor/bundle.ts b/typescript/contracts/cw20-stake-reward-distributor/bundle.ts
index cc1d3e59..58a00dc3 100644
--- a/typescript/contracts/cw20-stake-reward-distributor/bundle.ts
+++ b/typescript/contracts/cw20-stake-reward-distributor/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _4 from "./Cw20StakeRewardDistributor.types";
-import * as _5 from "./Cw20StakeRewardDistributor.client";
+import * as _6 from "./Cw20StakeRewardDistributor.types";
+import * as _7 from "./Cw20StakeRewardDistributor.client";
 export namespace contracts {
-  export const Cw20StakeRewardDistributor = { ..._4,
-    ..._5
+  export const Cw20StakeRewardDistributor = { ..._6,
+    ..._7
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/cw20-stake/bundle.ts b/typescript/contracts/cw20-stake/bundle.ts
index 10ab9528..ac4229e4 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 _26 from "./Cw20Stake.types";
-import * as _27 from "./Cw20Stake.client";
+import * as _30 from "./Cw20Stake.types";
+import * as _31 from "./Cw20Stake.client";
 export namespace contracts {
-  export const Cw20Stake = { ..._26,
-    ..._27
+  export const Cw20Stake = { ..._30,
+    ..._31
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-core/bundle.ts b/typescript/contracts/dao-core/bundle.ts
index 3198954c..26e18507 100644
--- a/typescript/contracts/dao-core/bundle.ts
+++ b/typescript/contracts/dao-core/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _32 from "./DaoCore.types";
-import * as _33 from "./DaoCore.client";
+import * as _36 from "./DaoCore.types";
+import * as _37 from "./DaoCore.client";
 export namespace contracts {
-  export const DaoCore = { ..._32,
-    ..._33
+  export const DaoCore = { ..._36,
+    ..._37
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-pre-propose-approval-single/bundle.ts b/typescript/contracts/dao-pre-propose-approval-single/bundle.ts
index 4b00c324..d1b3e4ce 100644
--- a/typescript/contracts/dao-pre-propose-approval-single/bundle.ts
+++ b/typescript/contracts/dao-pre-propose-approval-single/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _10 from "./DaoPreProposeApprovalSingle.types";
-import * as _11 from "./DaoPreProposeApprovalSingle.client";
+import * as _12 from "./DaoPreProposeApprovalSingle.types";
+import * as _13 from "./DaoPreProposeApprovalSingle.client";
 export namespace contracts {
-  export const DaoPreProposeApprovalSingle = { ..._10,
-    ..._11
+  export const DaoPreProposeApprovalSingle = { ..._12,
+    ..._13
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-pre-propose-approver/bundle.ts b/typescript/contracts/dao-pre-propose-approver/bundle.ts
index 8d927c56..59bc6f28 100644
--- a/typescript/contracts/dao-pre-propose-approver/bundle.ts
+++ b/typescript/contracts/dao-pre-propose-approver/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _12 from "./DaoPreProposeApprover.types";
-import * as _13 from "./DaoPreProposeApprover.client";
+import * as _14 from "./DaoPreProposeApprover.types";
+import * as _15 from "./DaoPreProposeApprover.client";
 export namespace contracts {
-  export const DaoPreProposeApprover = { ..._12,
-    ..._13
+  export const DaoPreProposeApprover = { ..._14,
+    ..._15
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-pre-propose-multiple/bundle.ts b/typescript/contracts/dao-pre-propose-multiple/bundle.ts
index 0b04eaf1..c17e48ac 100644
--- a/typescript/contracts/dao-pre-propose-multiple/bundle.ts
+++ b/typescript/contracts/dao-pre-propose-multiple/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _14 from "./DaoPreProposeMultiple.types";
-import * as _15 from "./DaoPreProposeMultiple.client";
+import * as _16 from "./DaoPreProposeMultiple.types";
+import * as _17 from "./DaoPreProposeMultiple.client";
 export namespace contracts {
-  export const DaoPreProposeMultiple = { ..._14,
-    ..._15
+  export const DaoPreProposeMultiple = { ..._16,
+    ..._17
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-pre-propose-single/bundle.ts b/typescript/contracts/dao-pre-propose-single/bundle.ts
index 072ba43d..f037a41e 100644
--- a/typescript/contracts/dao-pre-propose-single/bundle.ts
+++ b/typescript/contracts/dao-pre-propose-single/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _16 from "./DaoPreProposeSingle.types";
-import * as _17 from "./DaoPreProposeSingle.client";
+import * as _18 from "./DaoPreProposeSingle.types";
+import * as _19 from "./DaoPreProposeSingle.client";
 export namespace contracts {
-  export const DaoPreProposeSingle = { ..._16,
-    ..._17
+  export const DaoPreProposeSingle = { ..._18,
+    ..._19
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-proposal-condorcet/bundle.ts b/typescript/contracts/dao-proposal-condorcet/bundle.ts
index af38e974..f99d50eb 100644
--- a/typescript/contracts/dao-proposal-condorcet/bundle.ts
+++ b/typescript/contracts/dao-proposal-condorcet/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _8 from "./DaoProposalCondorcet.types";
-import * as _9 from "./DaoProposalCondorcet.client";
+import * as _10 from "./DaoProposalCondorcet.types";
+import * as _11 from "./DaoProposalCondorcet.client";
 export namespace contracts {
-  export const DaoProposalCondorcet = { ..._8,
-    ..._9
+  export const DaoProposalCondorcet = { ..._10,
+    ..._11
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-proposal-multiple/bundle.ts b/typescript/contracts/dao-proposal-multiple/bundle.ts
index 5e83e83d..e5202774 100644
--- a/typescript/contracts/dao-proposal-multiple/bundle.ts
+++ b/typescript/contracts/dao-proposal-multiple/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _28 from "./DaoProposalMultiple.types";
-import * as _29 from "./DaoProposalMultiple.client";
+import * as _32 from "./DaoProposalMultiple.types";
+import * as _33 from "./DaoProposalMultiple.client";
 export namespace contracts {
-  export const DaoProposalMultiple = { ..._28,
-    ..._29
+  export const DaoProposalMultiple = { ..._32,
+    ..._33
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-proposal-single/bundle.ts b/typescript/contracts/dao-proposal-single/bundle.ts
index ac2ca748..8d64214f 100644
--- a/typescript/contracts/dao-proposal-single/bundle.ts
+++ b/typescript/contracts/dao-proposal-single/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _30 from "./DaoProposalSingle.types";
-import * as _31 from "./DaoProposalSingle.client";
+import * as _34 from "./DaoProposalSingle.types";
+import * as _35 from "./DaoProposalSingle.client";
 export namespace contracts {
-  export const DaoProposalSingle = { ..._30,
-    ..._31
+  export const DaoProposalSingle = { ..._34,
+    ..._35
   };
 }
\ 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..8d30e998 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 _28 from "./DaoVotingCw20Staked.types";
+import * as _29 from "./DaoVotingCw20Staked.client";
 export namespace contracts {
-  export const DaoVotingCw20Staked = { ..._24,
-    ..._25
+  export const DaoVotingCw20Staked = { ..._28,
+    ..._29
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-voting-cw4/bundle.ts b/typescript/contracts/dao-voting-cw4/bundle.ts
index 2da5edca..7e6f0f62 100644
--- a/typescript/contracts/dao-voting-cw4/bundle.ts
+++ b/typescript/contracts/dao-voting-cw4/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _18 from "./DaoVotingCw4.types";
-import * as _19 from "./DaoVotingCw4.client";
+import * as _20 from "./DaoVotingCw4.types";
+import * as _21 from "./DaoVotingCw4.client";
 export namespace contracts {
-  export const DaoVotingCw4 = { ..._18,
-    ..._19
+  export const DaoVotingCw4 = { ..._20,
+    ..._21
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-voting-cw721-staked/bundle.ts b/typescript/contracts/dao-voting-cw721-staked/bundle.ts
index b58339f7..02e25291 100644
--- a/typescript/contracts/dao-voting-cw721-staked/bundle.ts
+++ b/typescript/contracts/dao-voting-cw721-staked/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _20 from "./DaoVotingCw721Staked.types";
-import * as _21 from "./DaoVotingCw721Staked.client";
+import * as _24 from "./DaoVotingCw721Staked.types";
+import * as _25 from "./DaoVotingCw721Staked.client";
 export namespace contracts {
-  export const DaoVotingCw721Staked = { ..._20,
-    ..._21
+  export const DaoVotingCw721Staked = { ..._24,
+    ..._25
   };
 }
\ No newline at end of file
diff --git a/typescript/contracts/dao-voting-native-staked/bundle.ts b/typescript/contracts/dao-voting-native-staked/bundle.ts
index fbaf739d..e3689cf5 100644
--- a/typescript/contracts/dao-voting-native-staked/bundle.ts
+++ b/typescript/contracts/dao-voting-native-staked/bundle.ts
@@ -4,10 +4,10 @@
 * and run the @cosmwasm/ts-codegen generate command to regenerate this file.
 */
 
-import * as _22 from "./DaoVotingNativeStaked.types";
-import * as _23 from "./DaoVotingNativeStaked.client";
+import * as _26 from "./DaoVotingNativeStaked.types";
+import * as _27 from "./DaoVotingNativeStaked.client";
 export namespace contracts {
-  export const DaoVotingNativeStaked = { ..._22,
-    ..._23
+  export const DaoVotingNativeStaked = { ..._26,
+    ..._27
   };
 }
\ No newline at end of file


Test patch:
diff --git a/contracts/external/cw-payroll-factory/src/tests.rs b/contracts/external/cw-payroll-factory/src/tests.rs
new file mode 100644
index 00000000..78f3a87a
--- /dev/null
+++ b/contracts/external/cw-payroll-factory/src/tests.rs
@@ -0,0 +1,625 @@
+use cosmwasm_std::{coins, to_binary, Addr, Empty, Uint128};
+use cw20::{Cw20Coin, Cw20ExecuteMsg};
+use cw_denom::UncheckedDenom;
+use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg};
+use cw_ownable::OwnershipError;
+use cw_vesting::{
+    msg::{InstantiateMsg as PayrollInstantiateMsg, QueryMsg as PayrollQueryMsg},
+    state::{UncheckedVestingParams, VestingPayment, VestingPaymentStatus},
+};
+use wynd_utils::Curve;
+
+use crate::{
+    msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg},
+    state::VestingContract,
+    ContractError,
+};
+
+const ALICE: &str = "alice";
+const BOB: &str = "bob";
+const INITIAL_BALANCE: u128 = 1000000000;
+const NATIVE_DENOM: &str = "denom";
+
+fn factory_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        crate::contract::execute,
+        crate::contract::instantiate,
+        crate::contract::query,
+    )
+    .with_reply(crate::contract::reply);
+    Box::new(contract)
+}
+
+fn cw20_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw20_base::contract::execute,
+        cw20_base::contract::instantiate,
+        cw20_base::contract::query,
+    );
+    Box::new(contract)
+}
+
+pub fn cw_vesting_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        cw_vesting::contract::execute,
+        cw_vesting::contract::instantiate,
+        cw_vesting::contract::query,
+    );
+    Box::new(contract)
+}
+
+#[test]
+pub fn test_instantiate_native_payroll_contract() {
+    let mut app = App::default();
+    let code_id = app.store_code(factory_contract());
+    let cw_vesting_code_id = app.store_code(cw_vesting_contract());
+
+    // Instantiate factory with only Alice allowed to instantiate payroll contracts
+    let instantiate = InstantiateMsg {
+        owner: Some(ALICE.to_string()),
+        vesting_code_id: cw_vesting_code_id,
+    };
+    let factory_addr = app
+        .instantiate_contract(
+            code_id,
+            Addr::unchecked("CREATOR"),
+            &instantiate,
+            &[],
+            "cw-admin-factory",
+            None,
+        )
+        .unwrap();
+
+    // Mint alice and bob native tokens
+    app.sudo(SudoMsg::Bank({
+        BankSudo::Mint {
+            to_address: ALICE.to_string(),
+            amount: coins(INITIAL_BALANCE, NATIVE_DENOM),
+        }
+    }))
+    .unwrap();
+    app.sudo(SudoMsg::Bank({
+        BankSudo::Mint {
+            to_address: BOB.to_string(),
+            amount: coins(INITIAL_BALANCE, NATIVE_DENOM),
+        }
+    }))
+    .unwrap();
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Native(NATIVE_DENOM.to_string());
+
+    // Basic linear vesting schedule
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    let instantiate_payroll_msg = ExecuteMsg::InstantiateNativePayrollContract {
+        instantiate_msg: PayrollInstantiateMsg {
+            owner: Some(ALICE.to_string()),
+            params: UncheckedVestingParams {
+                recipient: BOB.to_string(),
+                amount: Uint128::new(1000000),
+                denom: unchecked_denom,
+                vesting_schedule,
+                title: None,
+                description: None,
+            },
+        },
+        label: "Payroll".to_string(),
+    };
+
+    let res = app
+        .execute_contract(
+            Addr::unchecked(ALICE),
+            factory_addr.clone(),
+            &instantiate_payroll_msg,
+            &coins(amount.into(), NATIVE_DENOM),
+        )
+        .unwrap();
+
+    // BOB can't instantiate as owner is configured
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked(BOB),
+            factory_addr.clone(),
+            &instantiate_payroll_msg,
+            &coins(amount.into(), NATIVE_DENOM),
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::Unauthorized {});
+
+    // Get the payroll address from the instantiate event
+    let instantiate_event = &res.events[2];
+    assert_eq!(instantiate_event.ty, "instantiate");
+    let cw_vesting_addr = instantiate_event.attributes[0].value.clone();
+
+    // Check that admin of contract is owner specified in Instantiation Message
+    let contract_info = app
+        .wrap()
+        .query_wasm_contract_info(cw_vesting_addr)
+        .unwrap();
+    assert_eq!(contract_info.admin, Some(ALICE.to_string()));
+
+    // Test query list of contracts
+    let contracts: Vec<VestingContract> = app
+        .wrap()
+        .query_wasm_smart(
+            factory_addr.clone(),
+            &QueryMsg::ListVestingContracts {
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(contracts.len(), 1);
+
+    // Test query by instantiator
+    let contracts: Vec<VestingContract> = app
+        .wrap()
+        .query_wasm_smart(
+            factory_addr.clone(),
+            &QueryMsg::ListVestingContractsByInstantiator {
+                instantiator: ALICE.to_string(),
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(contracts.len(), 1);
+
+    // Test query by instantiator with no results
+    let contracts: Vec<VestingContract> = app
+        .wrap()
+        .query_wasm_smart(
+            factory_addr.clone(),
+            &QueryMsg::ListVestingContractsByInstantiator {
+                instantiator: BOB.to_string(),
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(contracts.len(), 0);
+
+    // Test query by recipient
+    let contracts: Vec<VestingContract> = app
+        .wrap()
+        .query_wasm_smart(
+            factory_addr.clone(),
+            &QueryMsg::ListVestingContractsByRecipient {
+                recipient: BOB.to_string(),
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(contracts.len(), 1);
+
+    // Test query by recipient no results
+    let contracts: Vec<VestingContract> = app
+        .wrap()
+        .query_wasm_smart(
+            factory_addr,
+            &QueryMsg::ListVestingContractsByRecipient {
+                recipient: ALICE.to_string(),
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(contracts.len(), 0);
+}
+
+#[test]
+pub fn test_instantiate_cw20_payroll_contract() {
+    let mut app = App::default();
+    let code_id = app.store_code(factory_contract());
+    let cw20_code_id = app.store_code(cw20_contract());
+    let cw_vesting_code_id = app.store_code(cw_vesting_contract());
+
+    // Instantiate cw20 contract with balances for Alice
+    let cw20_addr = app
+        .instantiate_contract(
+            cw20_code_id,
+            Addr::unchecked(ALICE),
+            &cw20_base::msg::InstantiateMsg {
+                name: "cw20 token".to_string(),
+                symbol: "cwtwenty".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: ALICE.to_string(),
+                    amount: Uint128::new(INITIAL_BALANCE),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "cw20-base",
+            None,
+        )
+        .unwrap();
+
+    let instantiate = InstantiateMsg {
+        owner: Some(ALICE.to_string()),
+        vesting_code_id: cw_vesting_code_id,
+    };
+    let factory_addr = app
+        .instantiate_contract(
+            code_id,
+            Addr::unchecked("CREATOR"),
+            &instantiate,
+            &[],
+            "cw-admin-factory",
+            None,
+        )
+        .unwrap();
+
+    // Mint alice native tokens
+    app.sudo(SudoMsg::Bank({
+        BankSudo::Mint {
+            to_address: ALICE.to_string(),
+            amount: coins(INITIAL_BALANCE, NATIVE_DENOM),
+        }
+    }))
+    .unwrap();
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Cw20(cw20_addr.to_string());
+
+    // Basic linear vesting schedule
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    let instantiate_payroll_msg = PayrollInstantiateMsg {
+        owner: Some(ALICE.to_string()),
+        params: UncheckedVestingParams {
+            recipient: BOB.to_string(),
+            amount: Uint128::new(1000000),
+            denom: unchecked_denom,
+            vesting_schedule,
+            title: None,
+            description: None,
+        },
+    };
+
+    // Attempting to call InstantiatePayrollContract directly with cw20 fails
+    app.execute_contract(
+        Addr::unchecked(ALICE),
+        factory_addr.clone(),
+        &ExecuteMsg::InstantiateNativePayrollContract {
+            instantiate_msg: instantiate_payroll_msg.clone(),
+            label: "Payroll".to_string(),
+        },
+        &coins(amount.into(), NATIVE_DENOM),
+    )
+    .unwrap_err();
+
+    let res = app
+        .execute_contract(
+            Addr::unchecked(ALICE),
+            cw20_addr,
+            &Cw20ExecuteMsg::Send {
+                contract: factory_addr.to_string(),
+                amount: instantiate_payroll_msg.params.amount,
+                msg: to_binary(&ReceiveMsg::InstantiatePayrollContract {
+                    instantiate_msg: instantiate_payroll_msg,
+                    label: "Payroll".to_string(),
+                })
+                .unwrap(),
+            },
+            &coins(amount.into(), NATIVE_DENOM),
+        )
+        .unwrap();
+
+    // Get the payroll address from the instantiate event
+    let instantiate_event = &res.events[4];
+    assert_eq!(instantiate_event.ty, "instantiate");
+    let cw_vesting_addr = instantiate_event.attributes[0].value.clone();
+
+    // Check that admin of contract is owner specified in Instantiation Message
+    let contract_info = app
+        .wrap()
+        .query_wasm_contract_info(cw_vesting_addr.clone())
+        .unwrap();
+    assert_eq!(contract_info.admin, Some(ALICE.to_string()));
+
+    // Test query by instantiator
+    let contracts: Vec<VestingContract> = app
+        .wrap()
+        .query_wasm_smart(
+            factory_addr,
+            &QueryMsg::ListVestingContractsByInstantiator {
+                instantiator: ALICE.to_string(),
+                start_after: None,
+                limit: None,
+            },
+        )
+        .unwrap();
+    assert_eq!(contracts.len(), 1);
+
+    // Check that the vesting payment contract is active
+    let vp: VestingPayment = app
+        .wrap()
+        .query_wasm_smart(cw_vesting_addr, &PayrollQueryMsg::Info {})
+        .unwrap();
+    assert_eq!(vp.status, VestingPaymentStatus::Active);
+}
+
+#[test]
+fn test_instantiate_wrong_ownership_native() {
+    let mut app = App::default();
+    let code_id = app.store_code(factory_contract());
+    let cw_vesting_code_id = app.store_code(cw_vesting_contract());
+
+    // Alice is the owner. Contracts are only allowed if their owner
+    // is alice or none and the sender is alice.
+    let instantiate = InstantiateMsg {
+        owner: Some(ALICE.to_string()),
+        vesting_code_id: cw_vesting_code_id,
+    };
+    let factory_addr = app
+        .instantiate_contract(
+            code_id,
+            Addr::unchecked("CREATOR"),
+            &instantiate,
+            &[],
+            "cw-admin-factory",
+            None,
+        )
+        .unwrap();
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked("ekez"),
+            factory_addr.clone(),
+            &ExecuteMsg::InstantiateNativePayrollContract {
+                instantiate_msg: PayrollInstantiateMsg {
+                    owner: Some(ALICE.to_string()),
+                    params: UncheckedVestingParams {
+                        recipient: BOB.to_string(),
+                        amount: Uint128::new(10),
+                        denom: UncheckedDenom::Native("tbucks".to_string()),
+                        vesting_schedule: Curve::Constant {
+                            y: Uint128::new(10),
+                        },
+                        title: None,
+                        description: None,
+                    },
+                },
+                label: "vesting".to_string(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    // Can't instantiate if you are not the owner.
+    assert_eq!(err, ContractError::Unauthorized {});
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked(ALICE),
+            factory_addr,
+            &ExecuteMsg::InstantiateNativePayrollContract {
+                instantiate_msg: PayrollInstantiateMsg {
+                    owner: Some("ekez".to_string()),
+                    params: UncheckedVestingParams {
+                        recipient: BOB.to_string(),
+                        amount: Uint128::new(10),
+                        denom: UncheckedDenom::Native("tbucks".to_string()),
+                        vesting_schedule: Curve::Constant {
+                            y: Uint128::new(10),
+                        },
+                        title: None,
+                        description: None,
+                    },
+                },
+                label: "vesting".to_string(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+
+    // Can't instantiate with an owner who is not the factory owner.
+    assert_eq!(
+        err,
+        ContractError::OwnerMissmatch {
+            actual: Some("ekez".to_string()),
+            expected: Some(ALICE.to_string())
+        }
+    );
+}
+
+#[test]
+fn test_instantiate_wrong_owner_cw20() {
+    let mut app = App::default();
+    let code_id = app.store_code(factory_contract());
+    let cw20_code_id = app.store_code(cw20_contract());
+    let cw_vesting_code_id = app.store_code(cw_vesting_contract());
+
+    let cw20_addr = app
+        .instantiate_contract(
+            cw20_code_id,
+            Addr::unchecked(ALICE),
+            &cw20_base::msg::InstantiateMsg {
+                name: "cw20 token".to_string(),
+                symbol: "cwtwenty".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: ALICE.to_string(),
+                    amount: Uint128::new(INITIAL_BALANCE),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "cw20-base",
+            None,
+        )
+        .unwrap();
+
+    let instantiate = InstantiateMsg {
+        owner: Some(ALICE.to_string()),
+        vesting_code_id: cw_vesting_code_id,
+    };
+    let factory_addr = app
+        .instantiate_contract(
+            code_id,
+            Addr::unchecked("CREATOR"),
+            &instantiate,
+            &[],
+            "cw-admin-factory",
+            None,
+        )
+        .unwrap();
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Cw20(cw20_addr.to_string());
+
+    // Basic linear vesting schedule
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    let instantiate_payroll_msg = PayrollInstantiateMsg {
+        owner: Some(BOB.to_string()),
+        params: UncheckedVestingParams {
+            recipient: BOB.to_string(),
+            amount: Uint128::new(1000000),
+            denom: unchecked_denom,
+            vesting_schedule,
+            title: None,
+            description: None,
+        },
+    };
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked(ALICE),
+            cw20_addr,
+            &Cw20ExecuteMsg::Send {
+                contract: factory_addr.to_string(),
+                amount: instantiate_payroll_msg.params.amount,
+                msg: to_binary(&ReceiveMsg::InstantiatePayrollContract {
+                    instantiate_msg: instantiate_payroll_msg,
+                    label: "Payroll".to_string(),
+                })
+                .unwrap(),
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(
+        err,
+        ContractError::OwnerMissmatch {
+            actual: Some(BOB.to_string()),
+            expected: Some(ALICE.to_string())
+        }
+    )
+}
+
+#[test]
+fn test_update_vesting_code_id() {
+    let mut app = App::default();
+    let code_id = app.store_code(factory_contract());
+    let cw_vesting_code_id = app.store_code(cw_vesting_contract());
+    let cw_vesting_code_two = app.store_code(cw_vesting_contract());
+
+    // Instantiate factory with only Alice allowed to instantiate payroll contracts
+    let instantiate = InstantiateMsg {
+        owner: Some(ALICE.to_string()),
+        vesting_code_id: cw_vesting_code_id,
+    };
+    let factory_addr = app
+        .instantiate_contract(
+            code_id,
+            Addr::unchecked("CREATOR"),
+            &instantiate,
+            &[],
+            "cw-admin-factory",
+            None,
+        )
+        .unwrap();
+
+    // Update the code ID to a new one.
+    app.execute_contract(
+        Addr::unchecked(ALICE),
+        factory_addr.clone(),
+        &ExecuteMsg::UpdateCodeId {
+            vesting_code_id: cw_vesting_code_two,
+        },
+        &[],
+    )
+    .unwrap();
+
+    let err: ContractError = app
+        .execute_contract(
+            Addr::unchecked(BOB),
+            factory_addr.clone(),
+            &ExecuteMsg::UpdateCodeId {
+                vesting_code_id: cw_vesting_code_two,
+            },
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::Ownable(OwnershipError::NotOwner));
+
+    app.sudo(SudoMsg::Bank({
+        BankSudo::Mint {
+            to_address: ALICE.to_string(),
+            amount: coins(INITIAL_BALANCE, NATIVE_DENOM),
+        }
+    }))
+    .unwrap();
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Native(NATIVE_DENOM.to_string());
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    let instantiate_payroll_msg = ExecuteMsg::InstantiateNativePayrollContract {
+        instantiate_msg: PayrollInstantiateMsg {
+            owner: Some(ALICE.to_string()),
+            params: UncheckedVestingParams {
+                recipient: BOB.to_string(),
+                amount: Uint128::new(1000000),
+                denom: unchecked_denom,
+                vesting_schedule,
+                title: None,
+                description: None,
+            },
+        },
+        label: "Payroll".to_string(),
+    };
+
+    let res = app
+        .execute_contract(
+            Addr::unchecked(ALICE),
+            factory_addr,
+            &instantiate_payroll_msg,
+            &coins(amount.into(), NATIVE_DENOM),
+        )
+        .unwrap();
+
+    // Check that the contract was instantiated using the new code ID.
+    let instantiate_event = &res.events[2];
+    assert_eq!(instantiate_event.ty, "instantiate");
+    let cw_vesting_addr = instantiate_event.attributes[0].value.clone();
+    let info = app
+        .wrap()
+        .query_wasm_contract_info(cw_vesting_addr)
+        .unwrap();
+    assert_eq!(info.code_id, cw_vesting_code_two);
+}
diff --git a/contracts/external/cw-vesting/src/tests.rs b/contracts/external/cw-vesting/src/tests.rs
new file mode 100644
index 00000000..9a58c3df
--- /dev/null
+++ b/contracts/external/cw-vesting/src/tests.rs
@@ -0,0 +1,870 @@
+use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
+use cosmwasm_std::{
+    coin, coins, to_binary, Addr, Coin, CosmosMsg, DistributionMsg, Empty, StakingMsg, Uint128,
+};
+use cw20::{Cw20Coin, Cw20ExecuteMsg};
+use cw_denom::{CheckedDenom, UncheckedDenom};
+use cw_multi_test::{App, BankSudo, Contract, ContractWrapper, Executor, SudoMsg};
+use dao_testing::contracts::cw20_base_contract;
+use wynd_utils::Curve;
+
+use crate::contract::{execute, instantiate};
+use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ReceiveMsg};
+use crate::state::{UncheckedVestingParams, VestingPayment, VestingPaymentStatus};
+use crate::ContractError;
+
+const ALICE: &str = "alice";
+const BOB: &str = "bob";
+const INITIAL_BALANCE: u128 = 1000000000;
+const OWNER: &str = "owner";
+const NATIVE_DENOM: &str = "ujuno";
+const VALIDATOR: &str = "validator";
+const VALIDATOR_TWO: &str = "validator2";
+
+fn cw_vesting_contract() -> Box<dyn Contract<Empty>> {
+    let contract = ContractWrapper::new(
+        crate::contract::execute,
+        crate::contract::instantiate,
+        crate::contract::query,
+    );
+    Box::new(contract)
+}
+
+fn get_vesting_payment(app: &App, cw_vesting_addr: Addr) -> VestingPayment {
+    app.wrap()
+        .query_wasm_smart(cw_vesting_addr, &QueryMsg::Info {})
+        .unwrap()
+}
+
+fn get_balance_cw20<T: Into<String>, U: Into<String>>(
+    app: &App,
+    contract_addr: T,
+    address: U,
+) -> Uint128 {
+    let msg = cw20::Cw20QueryMsg::Balance {
+        address: address.into(),
+    };
+    let result: cw20::BalanceResponse = app.wrap().query_wasm_smart(contract_addr, &msg).unwrap();
+    result.balance
+}
+
+fn get_balance_native<T: Into<String>, U: Into<String>>(
+    app: &App,
+    address: T,
+    denom: U,
+) -> Uint128 {
+    app.wrap().query_balance(address, denom).unwrap().amount
+}
+
+pub fn setup_app() -> App {
+    let mut app = App::default();
+
+    // Mint Alice and Bob native tokens
+    app.sudo(SudoMsg::Bank({
+        BankSudo::Mint {
+            to_address: ALICE.to_string(),
+            amount: coins(INITIAL_BALANCE, NATIVE_DENOM),
+        }
+    }))
+    .unwrap();
+    app.sudo(SudoMsg::Bank({
+        BankSudo::Mint {
+            to_address: BOB.to_string(),
+            amount: coins(INITIAL_BALANCE, NATIVE_DENOM),
+        }
+    }))
+    .unwrap();
+    app.sudo(SudoMsg::Bank({
+        BankSudo::Mint {
+            to_address: OWNER.to_string(),
+            amount: coins(INITIAL_BALANCE, NATIVE_DENOM),
+        }
+    }))
+    .unwrap();
+
+    app
+}
+
+pub fn setup_contracts(app: &mut App) -> (Addr, u64, u64) {
+    let cw20_code_id = app.store_code(cw20_base_contract());
+    let cw_vesting_code_id = app.store_code(cw_vesting_contract());
+
+    // Instantiate cw20 contract with balances for Alice and Bob
+    let cw20_addr = app
+        .instantiate_contract(
+            cw20_code_id,
+            Addr::unchecked(OWNER),
+            &cw20_base::msg::InstantiateMsg {
+                name: "cw20 token".to_string(),
+                symbol: "cwtwenty".to_string(),
+                decimals: 6,
+                initial_balances: vec![
+                    Cw20Coin {
+                        address: ALICE.to_string(),
+                        amount: Uint128::new(INITIAL_BALANCE),
+                    },
+                    Cw20Coin {
+                        address: BOB.to_string(),
+                        amount: Uint128::new(INITIAL_BALANCE),
+                    },
+                    Cw20Coin {
+                        address: OWNER.to_string(),
+                        amount: Uint128::new(INITIAL_BALANCE),
+                    },
+                ],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "cw20-base",
+            None,
+        )
+        .unwrap();
+
+    (cw20_addr, cw20_code_id, cw_vesting_code_id)
+}
+
+struct TestCase {
+    cw20_addr: Addr,
+    cw_vesting_addr: Addr,
+    owner: Addr,
+    recipient: Addr,
+    vesting_payment: VestingPayment,
+}
+
+fn setup_test_case(
+    app: &mut App,
+    vesting_schedule: Curve,
+    amount: Uint128,
+    denom: UncheckedDenom,
+    recipient: &str,
+    owner: Option<String>,
+    funds: &[Coin],
+) -> TestCase {
+    let (cw20_addr, _, cw_vesting_code_id) = setup_contracts(app);
+
+    // Instantiate cw-vesting contract
+    let cw_vesting_addr = app
+        .instantiate_contract(
+            cw_vesting_code_id,
+            Addr::unchecked(OWNER),
+            &InstantiateMsg {
+                owner,
+                params: UncheckedVestingParams {
+                    recipient: recipient.to_string(),
+                    amount,
+                    denom: denom.clone(),
+                    vesting_schedule,
+                    title: None,
+                    description: None,
+                },
+            },
+            funds,
+            "cw-vesting",
+            None,
+        )
+        .unwrap();
+
+    let vesting_payment = match denom {
+        UncheckedDenom::Cw20(ref cw20_addr) => {
+            let msg = Cw20ExecuteMsg::Send {
+                contract: cw_vesting_addr.to_string(),
+                amount,
+                msg: to_binary(&ReceiveMsg::Fund {}).unwrap(),
+            };
+            app.execute_contract(
+                Addr::unchecked(OWNER),
+                Addr::unchecked(cw20_addr.clone()),
+                &msg,
+                &[],
+            )
+            .unwrap();
+
+            get_vesting_payment(app, cw_vesting_addr.clone())
+        }
+        UncheckedDenom::Native(_) => get_vesting_payment(app, cw_vesting_addr.clone()),
+    };
+
+    TestCase {
+        cw20_addr,
+        cw_vesting_addr,
+        owner: Addr::unchecked(OWNER),
+        recipient: Addr::unchecked(recipient),
+        vesting_payment,
+    }
+}
+
+#[test]
+fn test_happy_cw20_path() {
+    let mut app = setup_app();
+
+    let amount = Uint128::new(1000000);
+    // cw20 is the first contract instantiated and will hve the address "contract0"
+    let unchecked_denom = UncheckedDenom::Cw20("contract0".to_string());
+
+    // Basic linear vesting schedule
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    let TestCase {
+        cw20_addr,
+        cw_vesting_addr,
+        recipient: bob,
+        vesting_payment,
+        ..
+    } = setup_test_case(
+        &mut app,
+        vesting_schedule.clone(),
+        amount,
+        unchecked_denom,
+        BOB,
+        None,
+        &[],
+    );
+
+    // Check Vesting Payment was created correctly
+    assert_eq!(
+        vesting_payment,
+        VestingPayment {
+            recipient: Addr::unchecked(BOB),
+            amount,
+            claimed_amount: Uint128::zero(),
+            denom: CheckedDenom::Cw20(Addr::unchecked(cw20_addr.clone())),
+            canceled_at_time: None,
+            vesting_schedule,
+            title: None,
+            description: None,
+            status: VestingPaymentStatus::Active,
+            staked_amount: Uint128::zero(),
+        }
+    );
+
+    // VestingPayment has not started
+    let err: ContractError = app
+        .execute_contract(
+            bob.clone(),
+            cw_vesting_addr.clone(),
+            &ExecuteMsg::Distribute {},
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::NoFundsToClaim);
+
+    // Advance the clock
+    app.update_block(|block| {
+        block.time = block.time.plus_seconds(150);
+    });
+
+    // VestingPayment has started so tokens have vested
+    app.execute_contract(
+        bob,
+        cw_vesting_addr.clone(),
+        &ExecuteMsg::Distribute {},
+        &[],
+    )
+    .unwrap();
+
+    // Check final amounts after distribution
+    assert_eq!(
+        get_vesting_payment(&app, cw_vesting_addr),
+        VestingPayment {
+            recipient: Addr::unchecked(BOB),
+            amount: Uint128::new(750000),
+            claimed_amount: Uint128::new(250000),
+            ..vesting_payment
+        }
+    );
+
+    // Owner has funded the contract and down 1000
+    assert_eq!(
+        get_balance_cw20(&app, cw20_addr.clone(), OWNER),
+        Uint128::new(INITIAL_BALANCE) - amount
+    );
+
+    // Bob has claimed vested funds and is up 250
+    assert_eq!(
+        get_balance_cw20(&app, cw20_addr, BOB),
+        Uint128::new(INITIAL_BALANCE) + Uint128::new(250000)
+    );
+}
+
+#[test]
+fn test_catch_imposter_cw20() {
+    let mut app = setup_app();
+    let (cw20_addr, cw20_code_id, _) = setup_contracts(&mut app);
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Cw20(cw20_addr.to_string());
+
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    // Instantiate cw-vesting
+    let TestCase {
+        cw_vesting_addr, ..
+    } = setup_test_case(
+        &mut app,
+        vesting_schedule,
+        amount,
+        unchecked_denom,
+        BOB,
+        None,
+        &[],
+    );
+
+    // Create imposter cw20
+    let cw20_imposter_addr = app
+        .instantiate_contract(
+            cw20_code_id,
+            Addr::unchecked(OWNER),
+            &cw20_base::msg::InstantiateMsg {
+                name: "cw20 token".to_string(),
+                symbol: "cwtwenty".to_string(),
+                decimals: 6,
+                initial_balances: vec![Cw20Coin {
+                    address: OWNER.to_string(),
+                    amount: Uint128::new(INITIAL_BALANCE),
+                }],
+                mint: None,
+                marketing: None,
+            },
+            &[],
+            "cw20-base",
+            None,
+        )
+        .unwrap();
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_vesting_addr.to_string(),
+        amount,
+        msg: to_binary(&ReceiveMsg::Fund {}).unwrap(),
+    };
+
+    // Errors that cw20 does not match what was expected
+    let error: ContractError = app
+        .execute_contract(
+            Addr::unchecked(OWNER),
+            Addr::unchecked(cw20_imposter_addr),
+            &msg,
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(error, ContractError::Cw20DoesNotMatch);
+}
+
+#[test]
+fn test_catch_incorrect_cw20_funding_amount() {
+    let mut app = setup_app();
+    let (cw20_addr, _, _) = setup_contracts(&mut app);
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Cw20(cw20_addr.to_string());
+
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    // Instantiate cw-vesting
+    let TestCase {
+        cw_vesting_addr, ..
+    } = setup_test_case(
+        &mut app,
+        vesting_schedule,
+        amount,
+        unchecked_denom,
+        BOB,
+        None,
+        &[],
+    );
+
+    let msg = Cw20ExecuteMsg::Send {
+        contract: cw_vesting_addr.to_string(),
+        amount: Uint128::new(100),
+        msg: to_binary(&ReceiveMsg::Fund {}).unwrap(),
+    };
+
+    // Errors that cw20 does not match what was expected
+    let error: ContractError = app
+        .execute_contract(
+            Addr::unchecked(OWNER),
+            Addr::unchecked(cw20_addr),
+            &msg,
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(error, ContractError::AmountDoesNotMatch);
+}
+
+#[test]
+fn test_happy_native_path() {
+    let mut app = setup_app();
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Native(NATIVE_DENOM.to_string());
+
+    // Basic linear vesting schedule
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    let TestCase {
+        cw_vesting_addr,
+        recipient: bob,
+        vesting_payment,
+        ..
+    } = setup_test_case(
+        &mut app,
+        vesting_schedule.clone(),
+        amount,
+        unchecked_denom,
+        BOB,
+        None,
+        &coins(amount.into(), NATIVE_DENOM),
+    );
+
+    // Check Vesting Payment was created correctly
+    assert_eq!(
+        vesting_payment,
+        VestingPayment {
+            recipient: Addr::unchecked(BOB),
+            amount,
+            claimed_amount: Uint128::zero(),
+            canceled_at_time: None,
+            denom: CheckedDenom::Native(NATIVE_DENOM.to_string()),
+            vesting_schedule,
+            title: None,
+            description: None,
+            status: VestingPaymentStatus::Active,
+            staked_amount: Uint128::zero(),
+        }
+    );
+
+    // VestingPayment has not started
+    let err: ContractError = app
+        .execute_contract(
+            bob.clone(),
+            cw_vesting_addr.clone(),
+            &ExecuteMsg::Distribute {},
+            &[],
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(err, ContractError::NoFundsToClaim);
+
+    // Advance the clock
+    app.update_block(|block| {
+        block.time = block.time.plus_seconds(150);
+    });
+
+    // VestingPayment has started so tokens have vested
+    app.execute_contract(
+        bob,
+        cw_vesting_addr.clone(),
+        &ExecuteMsg::Distribute {},
+        &[],
+    )
+    .unwrap();
+
+    // Check final amounts after distribution
+    assert_eq!(
+        get_vesting_payment(&app, cw_vesting_addr),
+        VestingPayment {
+            recipient: Addr::unchecked(BOB),
+            amount: Uint128::new(750000),
+            claimed_amount: Uint128::new(250000),
+            ..vesting_payment
+        }
+    );
+
+    // Owner has funded the contract and down 1000
+    assert_eq!(
+        get_balance_native(&app, OWNER, NATIVE_DENOM),
+        Uint128::new(INITIAL_BALANCE) - amount
+    );
+    // Bob has claimed vested funds and is up 250
+    assert_eq!(
+        get_balance_native(&app, BOB, NATIVE_DENOM),
+        Uint128::new(INITIAL_BALANCE) + Uint128::new(250000)
+    );
+}
+
+#[test]
+#[should_panic(expected = "The transfer will never become fully vested. Must hit 0 eventually")]
+fn test_constant_vesting_curve_fails() {
+    let mut app = setup_app();
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Native(NATIVE_DENOM.to_string());
+
+    let vesting_schedule = Curve::Constant { y: amount };
+
+    // Fails because constant curve never fully vests
+    let TestCase { .. } = setup_test_case(
+        &mut app,
+        vesting_schedule.clone(),
+        amount,
+        unchecked_denom,
+        BOB,
+        None,
+        &coins(amount.into(), NATIVE_DENOM),
+    );
+}
+
+#[test]
+fn test_incorrect_native_funding_amount() {
+    let mut app = setup_app();
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Native(NATIVE_DENOM.to_string());
+
+    // Basic linear vesting schedule
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    let alice = Addr::unchecked(ALICE);
+
+    let (_, _, cw_vesting_code_id) = setup_contracts(&mut app);
+
+    // Instantiate cw-vesting contract errors with incorrect amount
+    let error: ContractError = app
+        .instantiate_contract(
+            cw_vesting_code_id,
+            alice.clone(),
+            &InstantiateMsg {
+                owner: Some(alice.to_string()),
+                params: UncheckedVestingParams {
+                    recipient: BOB.to_string(),
+                    amount,
+                    denom: unchecked_denom,
+                    vesting_schedule,
+                    title: None,
+                    description: None,
+                },
+            },
+            &coins(100, NATIVE_DENOM),
+            "cw-vesting",
+            None,
+        )
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(error, ContractError::AmountDoesNotMatch)
+}
+
+#[test]
+fn test_cancel_vesting() {
+    let mut app = setup_app();
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Native(NATIVE_DENOM.to_string());
+
+    // Basic linear vesting schedule
+    let start_time = app.block_info().time.plus_seconds(100).seconds();
+    let end_time = app.block_info().time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    let alice = Addr::unchecked(ALICE);
+
+    let TestCase {
+        cw_vesting_addr,
+        owner,
+        recipient: bob,
+        ..
+    } = setup_test_case(
+        &mut app,
+        vesting_schedule,
+        amount,
+        unchecked_denom,
+        BOB,
+        Some(OWNER.to_string()),
+        &coins(amount.into(), NATIVE_DENOM),
+    );
+
+    // Non-owner can't cancel
+    let err: ContractError = app
+        .execute_contract(alice, cw_vesting_addr.clone(), &ExecuteMsg::Cancel {}, &[])
+        .unwrap_err()
+        .downcast()
+        .unwrap();
+    assert_eq!(
+        err,
+        ContractError::Ownable(cw_ownable::OwnershipError::NotOwner)
+    );
+
+    // Advance the clock
+    app.update_block(|block| {
+        block.time = block.time.plus_seconds(150);
+    });
+
+    // Owner DAO cancels vesting contract
+    app.execute_contract(owner, cw_vesting_addr.clone(), &ExecuteMsg::Cancel {}, &[])
+        .unwrap();
+
+    // Bob tries to withdraw but can't
+    app.execute_contract(bob, cw_vesting_addr, &ExecuteMsg::Distribute {}, &[])
+        .unwrap_err();
+
+    // Unvested funds have been returned to contract owner
+    assert_eq!(
+        get_balance_native(&app, "owner", NATIVE_DENOM),
+        Uint128::new(INITIAL_BALANCE) - Uint128::new(250000)
+    );
+    // Bob has gets the funds vest up until cancelation
+    assert_eq!(
+        get_balance_native(&app, BOB, NATIVE_DENOM),
+        Uint128::new(INITIAL_BALANCE) + Uint128::new(250000)
+    );
+}
+
+#[test]
+fn staking_unit_tests() {
+    let mut deps = mock_dependencies();
+
+    // Update staking querier info
+    deps.querier.update_staking(NATIVE_DENOM, &[], &[]);
+
+    let env = mock_env();
+    let alice = mock_info(ALICE, &[]);
+    let bob = mock_info(BOB, &[]);
+
+    let amount = Uint128::new(1000000);
+    let unchecked_denom = UncheckedDenom::Native(NATIVE_DENOM.to_string());
+
+    // Basic linear vesting schedule
+    let start_time = env.block.time.plus_seconds(100).seconds();
+    let end_time = env.block.time.plus_seconds(300).seconds();
+    let vesting_schedule = Curve::saturating_linear((start_time, amount.into()), (end_time, 0));
+
+    // Alice successfully instantiates
+    instantiate(
+        deps.as_mut(),
+        env.clone(),
+        mock_info(ALICE, &coins(amount.into(), NATIVE_DENOM.to_string())),
+        InstantiateMsg {
+            owner: Some(OWNER.to_string()),
+            params: UncheckedVestingParams {
+                recipient: BOB.to_string(),
+                amount,
+                denom: unchecked_denom,
+                vesting_schedule,
+                title: None,
+                description: None,
+            },
+        },
+    )
+    .unwrap();
+
+    // Alice can't delegate his vesting payment
+    let err = execute(
+        deps.as_mut(),
+        env.clone(),
+        alice.clone(),
+        ExecuteMsg::Delegate {
+            validator: VALIDATOR.to_string(),
+            amount,
+        },
+    )
+    .unwrap_err();
+    assert_eq!(err, ContractError::Unauthorized);
+
+    // Bob delegates his vesting payment
+    let res = execute(
+        deps.as_mut(),
+        env.clone(),
+        bob.clone(),
+        ExecuteMsg::Delegate {
+            validator: VALIDATOR.to_string(),
+            amount,
+        },
+    )
+    .unwrap();
+    assert_eq!(
+        res.messages[0].msg,
+        CosmosMsg::Staking(StakingMsg::Delegate {
+            validator: VALIDATOR.to_string(),
+            amount: coin(amount.into(), NATIVE_DENOM)
+        })
+    );
+
+    // Bob can't delegate more than he has
+    execute(
+        deps.as_mut(),
+        env.clone(),
+        bob.clone(),
+        ExecuteMsg::Delegate {
+            validator: VALIDATOR.to_string(),
+            amount,
+        },
+    )
+    .unwrap_err();
+
+    // Any can call Withdraw Rewards, even alice
+    let res = execute(
+        deps.as_mut(),
+        env.clone(),
+        alice.clone(),
+        ExecuteMsg::WithdrawDelegatorReward {
+            validator: VALIDATOR.to_string(),
+        },
+    )
+    .unwrap();
+    assert_eq!(
+        res.messages[0].msg,
+        CosmosMsg::Distribution(DistributionMsg::WithdrawDelegatorReward {
+            validator: VALIDATOR.to_string(),
+        })
+    );
+
+    // Alice can't redelegate or undelegate on bob's behalf
+    let err = execute(
+        deps.as_mut(),
+        env.clone(),
+        alice.clone(),
+        ExecuteMsg::Redelegate {
+            src_validator: VALIDATOR.to_string(),
+            dst_validator: VALIDATOR_TWO.to_string(),
+            amount,
+        },
+    )
+    .unwrap_err();
+    assert_eq!(err, ContractError::Unauthorized);
+    let err = execute(
+        deps.as_mut(),
+        env.clone(),
+        alice.clone(),
+        ExecuteMsg::Undelegate {
+            validator: VALIDATOR.to_string(),
+            amount,
+        },
+    )
+    .unwrap_err();
+    assert_eq!(err, ContractError::Unauthorized);
+
+    // Bob (recipient) can't redelegate more than they have
+    execute(
+        deps.as_mut(),
+        env.clone(),
+        bob.clone(),
+        ExecuteMsg::Redelegate {
+            src_validator: VALIDATOR.to_string(),
+            dst_validator: VALIDATOR_TWO.to_string(),
+            amount: amount + Uint128::new(1000),
+        },
+    )
+    .unwrap_err();
+
+    // Bob redelegates half their tokens
+    let res = execute(
+        deps.as_mut(),
+        env.clone(),
+        bob.clone(),
+        ExecuteMsg::Redelegate {
+            src_validator: VALIDATOR.to_string(),
+            dst_validator: VALIDATOR_TWO.to_string(),
+            amount: amount - amount.checked_div(Uint128::new(2)).unwrap(),
+        },
+    )
+    .unwrap();
+    assert_eq!(
+        res.messages[0].msg,
+        CosmosMsg::Staking(StakingMsg::Redelegate {
+            src_validator: VALIDATOR.to_string(),
+            dst_validator: VALIDATOR_TWO.to_string(),
+            amount: Coin {
+                denom: NATIVE_DENOM.to_string(),
+                amount: amount - amount.checked_div(Uint128::new(2)).unwrap(),
+            }
+        })
+    );
+
+    // Bob undelegates a little from validator two
+    let res = execute(
+        deps.as_mut(),
+        env.clone(),
+        bob.clone(),
+        ExecuteMsg::Undelegate {
+            validator: VALIDATOR_TWO.to_string(),
+            amount: Uint128::new(10),
+        },
+    )
+    .unwrap();
+    assert_eq!(
+        res.messages[0].msg,
+        CosmosMsg::Staking(StakingMsg::Undelegate {
+            validator: VALIDATOR_TWO.to_string(),
+            amount: coin(10, NATIVE_DENOM)
+        })
+    );
+
+    // Only Bob (the recipient) can call SetWithdrawAddress
+    let err = execute(
+        deps.as_mut(),
+        env.clone(),
+        alice,
+        ExecuteMsg::SetWithdrawAddress {
+            address: ALICE.to_string(),
+        },
+    )
+    .unwrap_err();
+    assert_eq!(err, ContractError::Unauthorized);
+    let res = execute(
+        deps.as_mut(),
+        env.clone(),
+        bob,
+        ExecuteMsg::SetWithdrawAddress {
+            address: "bob2".to_string(),
+        },
+    )
+    .unwrap();
+    assert_eq!(
+        res.messages[0].msg,
+        CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress {
+            address: "bob2".to_string()
+        })
+    );
+
+    // Contract owner cancels contract, it includes unbonding message for all validators bob delegates to
+    let res = execute(
+        deps.as_mut(),
+        env,
+        mock_info(OWNER, &[]),
+        ExecuteMsg::Cancel {},
+    )
+    .unwrap();
+    assert_eq!(res.messages.len(), 3);
+    assert_eq!(
+        res.messages[1].msg,
+        CosmosMsg::Staking(StakingMsg::Undelegate {
+            validator: VALIDATOR.to_string(),
+            amount: coin(
+                amount.checked_div(Uint128::new(2)).unwrap().into(),
+                NATIVE_DENOM
+            )
+        })
+    );
+    assert_eq!(
+        res.messages[2].msg,
+        CosmosMsg::Staking(StakingMsg::Undelegate {
+            validator: VALIDATOR_TWO.to_string(),
+            amount: coin(
+                amount
+                    .checked_div(Uint128::new(2))
+                    .unwrap()
+                    .checked_sub(Uint128::new(10))
+                    .unwrap()
+                    .into(),
+                NATIVE_DENOM
+            )
+        })
+    );
+}