From 4ddeeaed96c4fd070c429e84989e984cb8e83bd0 Mon Sep 17 00:00:00 2001 From: quasisamurai Date: Mon, 29 Jan 2024 07:37:23 -0300 Subject: [PATCH] commit base --- Cargo.lock | 67 +- contracts/vesting-lp-pcl/.cargo/config | 6 + contracts/vesting-lp-pcl/Cargo.toml | 28 + .../examples/vesting-lp_schema.rs | 12 + .../vesting-lp-pcl/schema/raw/execute.json | 480 ++++++ .../schema/raw/instantiate.json | 29 + .../vesting-lp-pcl/schema/raw/migrate.json | 7 + .../vesting-lp-pcl/schema/raw/query.json | 290 ++++ .../raw/response_to_available_amount.json | 6 + .../schema/raw/response_to_config.json | 128 ++ .../raw/response_to_historical_extension.json | 59 + .../raw/response_to_managed_extension.json | 6 + .../schema/raw/response_to_timestamp.json | 7 + .../raw/response_to_vesting_account.json | 119 ++ .../raw/response_to_vesting_accounts.json | 136 ++ .../schema/raw/response_to_vesting_state.json | 35 + .../response_to_with_managers_extension.json | 21 + .../vesting-lp-pcl/schema/vesting-lp.json | 1331 +++++++++++++++++ contracts/vesting-lp-pcl/src/contract.rs | 46 + contracts/vesting-lp-pcl/src/lib.rs | 5 + contracts/vesting-lp-pcl/src/msg.rs | 12 + .../vesting-lp-pcl/src/tests/integration.rs | 1281 ++++++++++++++++ contracts/vesting-lp-pcl/src/tests/mod.rs | 1 + contracts/vesting-lp/.cargo/config | 2 +- contracts/vesting-lp/Cargo.toml | 18 +- .../vesting-lp/examples/vesting_schema.rs | 12 + contracts/vesting-lp/src/contract.rs | 15 +- contracts/vesting-lp/src/lib.rs | 3 - packages/vesting-base-lp/Cargo.toml | 25 + packages/vesting-base-lp/NOTICE | 14 + packages/vesting-base-lp/README.md | 251 ++++ packages/vesting-base-lp/src/builder.rs | 60 + packages/vesting-base-lp/src/error.rs | 65 + .../vesting-base-lp/src/ext_historical.rs | 101 ++ packages/vesting-base-lp/src/ext_managed.rs | 121 ++ .../vesting-base-lp/src/ext_with_managers.rs | 105 ++ packages/vesting-base-lp/src/handlers.rs | 820 ++++++++++ packages/vesting-base-lp/src/lib.rs | 10 + packages/vesting-base-lp/src/msg.rs | 209 +++ packages/vesting-base-lp/src/state.rs | 166 ++ packages/vesting-base-lp/src/types.rs | 131 ++ packages/vesting-base-pcl/Cargo.toml | 23 + packages/vesting-base-pcl/NOTICE | 14 + packages/vesting-base-pcl/README.md | 251 ++++ packages/vesting-base-pcl/src/builder.rs | 60 + packages/vesting-base-pcl/src/error.rs | 47 + .../vesting-base-pcl/src/ext_historical.rs | 101 ++ packages/vesting-base-pcl/src/ext_managed.rs | 121 ++ .../vesting-base-pcl/src/ext_with_managers.rs | 105 ++ packages/vesting-base-pcl/src/handlers.rs | 450 ++++++ packages/vesting-base-pcl/src/lib.rs | 13 + packages/vesting-base-pcl/src/msg.rs | 160 ++ packages/vesting-base-pcl/src/state.rs | 159 ++ packages/vesting-base-pcl/src/testing.rs | 484 ++++++ packages/vesting-base-pcl/src/types.rs | 109 ++ 55 files changed, 8313 insertions(+), 24 deletions(-) create mode 100644 contracts/vesting-lp-pcl/.cargo/config create mode 100644 contracts/vesting-lp-pcl/Cargo.toml create mode 100644 contracts/vesting-lp-pcl/examples/vesting-lp_schema.rs create mode 100644 contracts/vesting-lp-pcl/schema/raw/execute.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/instantiate.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/migrate.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/query.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_available_amount.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_config.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_historical_extension.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_managed_extension.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_timestamp.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_vesting_account.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_vesting_accounts.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_vesting_state.json create mode 100644 contracts/vesting-lp-pcl/schema/raw/response_to_with_managers_extension.json create mode 100644 contracts/vesting-lp-pcl/schema/vesting-lp.json create mode 100644 contracts/vesting-lp-pcl/src/contract.rs create mode 100644 contracts/vesting-lp-pcl/src/lib.rs create mode 100644 contracts/vesting-lp-pcl/src/msg.rs create mode 100644 contracts/vesting-lp-pcl/src/tests/integration.rs create mode 100644 contracts/vesting-lp-pcl/src/tests/mod.rs create mode 100644 contracts/vesting-lp/examples/vesting_schema.rs create mode 100644 packages/vesting-base-lp/Cargo.toml create mode 100644 packages/vesting-base-lp/NOTICE create mode 100644 packages/vesting-base-lp/README.md create mode 100644 packages/vesting-base-lp/src/builder.rs create mode 100644 packages/vesting-base-lp/src/error.rs create mode 100644 packages/vesting-base-lp/src/ext_historical.rs create mode 100644 packages/vesting-base-lp/src/ext_managed.rs create mode 100644 packages/vesting-base-lp/src/ext_with_managers.rs create mode 100644 packages/vesting-base-lp/src/handlers.rs create mode 100644 packages/vesting-base-lp/src/lib.rs create mode 100644 packages/vesting-base-lp/src/msg.rs create mode 100644 packages/vesting-base-lp/src/state.rs create mode 100644 packages/vesting-base-lp/src/types.rs create mode 100644 packages/vesting-base-pcl/Cargo.toml create mode 100644 packages/vesting-base-pcl/NOTICE create mode 100644 packages/vesting-base-pcl/README.md create mode 100644 packages/vesting-base-pcl/src/builder.rs create mode 100644 packages/vesting-base-pcl/src/error.rs create mode 100644 packages/vesting-base-pcl/src/ext_historical.rs create mode 100644 packages/vesting-base-pcl/src/ext_managed.rs create mode 100644 packages/vesting-base-pcl/src/ext_with_managers.rs create mode 100644 packages/vesting-base-pcl/src/handlers.rs create mode 100644 packages/vesting-base-pcl/src/lib.rs create mode 100644 packages/vesting-base-pcl/src/msg.rs create mode 100644 packages/vesting-base-pcl/src/state.rs create mode 100644 packages/vesting-base-pcl/src/testing.rs create mode 100644 packages/vesting-base-pcl/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index e1863ae8..b7c97722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,20 @@ dependencies = [ "uint", ] +[[package]] +name = "astroport" +version = "2.8.0" +source = "git+https://github.com/astroport-fi/astroport-core.git?tag=v2.8.0#3b44a4044b823a145730f66ffaf7ae4205b2cd35" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw20 0.15.1", + "itertools 0.10.5", + "uint", +] + [[package]] name = "astroport-factory" version = "1.5.0" @@ -101,7 +115,7 @@ dependencies = [ "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.16.5", "cw-storage-plus 1.1.0", "cw2 1.1.1", "cw20 1.1.1", @@ -416,6 +430,25 @@ dependencies = [ "obi", ] +[[package]] +name = "cw-multi-test" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e81b4a7821d5eeba0d23f737c16027b39a600742ca8c32eb980895ffd270f4" +dependencies = [ + "anyhow", + "cosmwasm-std", + "cosmwasm-storage", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "derivative", + "itertools 0.10.5", + "prost", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw-multi-test" version = "0.16.5" @@ -642,7 +675,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "credits", - "cw-multi-test", + "cw-multi-test 0.16.5", "cw-storage-plus 1.1.0", "cw2 1.1.1", "cw20 1.1.1", @@ -1477,6 +1510,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "vesting-base-lp" +version = "1.1.0" +dependencies = [ + "astroport 2.8.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw2 0.15.1", + "cw20 0.15.1", + "serde", + "thiserror", + "vesting-base", +] + [[package]] name = "vesting-investors" version = "1.1.1" @@ -1485,7 +1534,7 @@ dependencies = [ "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.16.5", "cw-utils 0.15.1", "cw2 1.1.1", "cw20 1.1.1", @@ -1496,15 +1545,17 @@ dependencies = [ name = "vesting-lp" version = "1.1.0" dependencies = [ - "astroport 2.0.0", + "astroport 2.8.0", "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", "cw-utils 0.15.1", - "cw2 1.1.1", - "cw20 1.1.1", + "cw2 0.15.1", + "cw20 0.15.1", "vesting-base", + "vesting-base-lp", ] [[package]] @@ -1515,7 +1566,7 @@ dependencies = [ "astroport-token", "cosmwasm-schema", "cosmwasm-std", - "cw-multi-test", + "cw-multi-test 0.16.5", "cw-utils 0.15.1", "cw2 1.1.1", "cw20 1.1.1", diff --git a/contracts/vesting-lp-pcl/.cargo/config b/contracts/vesting-lp-pcl/.cargo/config new file mode 100644 index 00000000..f2fbc6a9 --- /dev/null +++ b/contracts/vesting-lp-pcl/.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 vesting-lp_schema" diff --git a/contracts/vesting-lp-pcl/Cargo.toml b/contracts/vesting-lp-pcl/Cargo.toml new file mode 100644 index 00000000..31f70c7c --- /dev/null +++ b/contracts/vesting-lp-pcl/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "vesting-lp" +version = "1.1.0" +authors = ["Neutron"] +edition = "2021" +description = "Vesting contract with a voting capabilities. Provides queries to get the amount of tokens are being held by user at certain height." + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +cw2 = { workspace = true } +cw20 = { workspace = true } +astroport = { workspace = true } +vesting-base = { workspace = true } +cosmwasm-schema = { workspace = true } +cosmwasm-std = { workspace = true } + +[dev-dependencies] +cw-multi-test = { workspace = true } +astroport-token = {git = "https://github.com/astroport-fi/astroport-core.git", rev = "65ce7d1879cc5d95b09fa14202f0423bba52ae0e" } +cw-utils = "0.15" diff --git a/contracts/vesting-lp-pcl/examples/vesting-lp_schema.rs b/contracts/vesting-lp-pcl/examples/vesting-lp_schema.rs new file mode 100644 index 00000000..90284134 --- /dev/null +++ b/contracts/vesting-lp-pcl/examples/vesting-lp_schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; +use vesting_base::msg::{ExecuteMsg, MigrateMsg, QueryMsg}; +use vesting_lp::msg::InstantiateMsg; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/vesting-lp-pcl/schema/raw/execute.json b/contracts/vesting-lp-pcl/schema/raw/execute.json new file mode 100644 index 00000000..e5aa732d --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/execute.json @@ -0,0 +1,480 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in a vesting contract.", + "oneOf": [ + { + "description": "Claim claims vested tokens and sends them to a recipient", + "type": "object", + "required": [ + "claim" + ], + "properties": { + "claim": { + "type": "object", + "properties": { + "amount": { + "description": "The amount of tokens to claim", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "recipient": { + "description": "The address that receives the vested tokens", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + }, + { + "description": "RegisterVestingAccounts registers vesting targets/accounts", + "type": "object", + "required": [ + "register_vesting_accounts" + ], + "properties": { + "register_vesting_accounts": { + "type": "object", + "required": [ + "vesting_accounts" + ], + "properties": { + "vesting_accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/VestingAccount" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Creates a request to change contract ownership ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "propose_new_owner" + ], + "properties": { + "propose_new_owner": { + "type": "object", + "required": [ + "expires_in", + "owner" + ], + "properties": { + "expires_in": { + "description": "The validity period of the offer to change the owner", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "description": "The newly proposed owner", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes a request to change contract ownership ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "drop_ownership_proposal" + ], + "properties": { + "drop_ownership_proposal": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Claims contract ownership ## Executor Only the newly proposed owner can execute this", + "type": "object", + "required": [ + "claim_ownership" + ], + "properties": { + "claim_ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets vesting token ## Executor Only the current owner or token info manager can execute this", + "type": "object", + "required": [ + "set_vesting_token" + ], + "properties": { + "set_vesting_token": { + "type": "object", + "required": [ + "vesting_token" + ], + "properties": { + "vesting_token": { + "$ref": "#/definitions/AssetInfo" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the managed extension for vesting contracts.", + "type": "object", + "required": [ + "managed_extension" + ], + "properties": { + "managed_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/ExecuteMsgManaged" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the with_managers extension for vesting contracts.", + "type": "object", + "required": [ + "with_managers_extension" + ], + "properties": { + "with_managers_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/ExecuteMsgWithManagers" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the historical extension for vesting contracts.", + "type": "object", + "required": [ + "historical_extension" + ], + "properties": { + "historical_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/ExecuteMsgHistorical" + } + }, + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec 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. See also .", + "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 + }, + "ExecuteMsgHistorical": { + "description": "This structure describes the execute messages available in a historical vesting contract.", + "type": "string", + "enum": [] + }, + "ExecuteMsgManaged": { + "description": "This structure describes the execute messages available in a managed vesting contract.", + "oneOf": [ + { + "description": "Removes vesting targets/accounts. ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "remove_vesting_accounts" + ], + "properties": { + "remove_vesting_accounts": { + "type": "object", + "required": [ + "clawback_account", + "vesting_accounts" + ], + "properties": { + "clawback_account": { + "description": "Specifies the account that will receive the funds taken from the vesting accounts.", + "type": "string" + }, + "vesting_accounts": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ExecuteMsgWithManagers": { + "description": "This structure describes the execute messages available in a with_managers vesting contract.", + "oneOf": [ + { + "description": "Adds vesting managers ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "add_vesting_managers" + ], + "properties": { + "add_vesting_managers": { + "type": "object", + "required": [ + "managers" + ], + "properties": { + "managers": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes vesting managers ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "remove_vesting_managers" + ], + "properties": { + "remove_vesting_managers": { + "type": "object", + "required": [ + "managers" + ], + "properties": { + "managers": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "VestingAccount": { + "description": "This structure stores vesting information for a specific address that is getting tokens.", + "type": "object", + "required": [ + "address", + "schedules" + ], + "properties": { + "address": { + "description": "The address that is getting tokens", + "type": "string" + }, + "schedules": { + "description": "The vesting schedules targeted at the `address`", + "type": "array", + "items": { + "$ref": "#/definitions/VestingSchedule" + } + } + }, + "additionalProperties": false + }, + "VestingSchedule": { + "description": "This structure stores parameters for a specific vesting schedule", + "type": "object", + "required": [ + "start_point" + ], + "properties": { + "end_point": { + "description": "The end point for the vesting schedule", + "anyOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + }, + { + "type": "null" + } + ] + }, + "start_point": { + "description": "The start date for the vesting schedule", + "allOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + } + ] + } + }, + "additionalProperties": false + }, + "VestingSchedulePoint": { + "description": "This structure stores the parameters used to create a vesting schedule.", + "type": "object", + "required": [ + "amount", + "time" + ], + "properties": { + "amount": { + "description": "The amount of tokens being vested", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "time": { + "description": "The start time for the vesting schedule", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/vesting-lp-pcl/schema/raw/instantiate.json b/contracts/vesting-lp-pcl/schema/raw/instantiate.json new file mode 100644 index 00000000..044bc78c --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/instantiate.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a contract.", + "type": "object", + "required": [ + "owner", + "token_info_manager", + "vesting_managers" + ], + "properties": { + "owner": { + "description": "Address allowed to change contract parameters", + "type": "string" + }, + "token_info_manager": { + "description": "Token info manager address", + "type": "string" + }, + "vesting_managers": { + "description": "Initial list of whitelisted vesting managers", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/contracts/vesting-lp-pcl/schema/raw/migrate.json b/contracts/vesting-lp-pcl/schema/raw/migrate.json new file mode 100644 index 00000000..1b9dcecf --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/migrate.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object", + "additionalProperties": false +} diff --git a/contracts/vesting-lp-pcl/schema/raw/query.json b/contracts/vesting-lp-pcl/schema/raw/query.json new file mode 100644 index 00000000..64a0b398 --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/query.json @@ -0,0 +1,290 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in a vesting contract.", + "oneOf": [ + { + "description": "Returns the configuration for the contract using a [`ConfigResponse`] object.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about an address vesting tokens using a [`VestingAccountResponse`] object.", + "type": "object", + "required": [ + "vesting_account" + ], + "properties": { + "vesting_account": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object.", + "type": "object", + "required": [ + "vesting_accounts" + ], + "properties": { + "vesting_accounts": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "order_by": { + "anyOf": [ + { + "$ref": "#/definitions/OrderBy" + }, + { + "type": "null" + } + ] + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total unvested amount of tokens for a specific address.", + "type": "object", + "required": [ + "available_amount" + ], + "properties": { + "available_amount": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Timestamp returns the current timestamp", + "type": "object", + "required": [ + "timestamp" + ], + "properties": { + "timestamp": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "VestingState returns the current vesting state.", + "type": "object", + "required": [ + "vesting_state" + ], + "properties": { + "vesting_state": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the managed extension for vesting contracts.", + "type": "object", + "required": [ + "managed_extension" + ], + "properties": { + "managed_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryMsgManaged" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the with_managers extension for vesting contracts.", + "type": "object", + "required": [ + "with_managers_extension" + ], + "properties": { + "with_managers_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryMsgWithManagers" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the historical extension for vesting contracts.", + "type": "object", + "required": [ + "historical_extension" + ], + "properties": { + "historical_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryMsgHistorical" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "OrderBy": { + "description": "This enum describes the types of sorting that can be applied to some piece of data", + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "QueryMsgHistorical": { + "description": "This structure describes the query messages available in a historical vesting contract.", + "oneOf": [ + { + "description": "Returns the total unclaimed amount of tokens for a specific address at certain height.", + "type": "object", + "required": [ + "unclaimed_amount_at_height" + ], + "properties": { + "unclaimed_amount_at_height": { + "type": "object", + "required": [ + "address", + "height" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total unclaimed amount of tokens for all the users at certain height.", + "type": "object", + "required": [ + "unclaimed_total_amount_at_height" + ], + "properties": { + "unclaimed_total_amount_at_height": { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "QueryMsgManaged": { + "description": "This structure describes the query messages available in a managed vesting contract.", + "type": "string", + "enum": [] + }, + "QueryMsgWithManagers": { + "description": "This structure describes the query messages available in a with_managers vesting contract.", + "oneOf": [ + { + "description": "Returns list of vesting managers (the persons who are able to add/remove vesting schedules)", + "type": "object", + "required": [ + "vesting_managers" + ], + "properties": { + "vesting_managers": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/vesting-lp-pcl/schema/raw/response_to_available_amount.json b/contracts/vesting-lp-pcl/schema/raw/response_to_available_amount.json new file mode 100644 index 00000000..25b73e8f --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_available_amount.json @@ -0,0 +1,6 @@ +{ + "$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/vesting-lp-pcl/schema/raw/response_to_config.json b/contracts/vesting-lp-pcl/schema/raw/response_to_config.json new file mode 100644 index 00000000..de35d970 --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_config.json @@ -0,0 +1,128 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "description": "This structure stores the main parameters for the generator vesting contract.", + "type": "object", + "required": [ + "extensions", + "owner", + "token_info_manager" + ], + "properties": { + "extensions": { + "description": "Contains extensions information of the contract", + "allOf": [ + { + "$ref": "#/definitions/Extensions" + } + ] + }, + "owner": { + "description": "Address that's allowed to change contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "token_info_manager": { + "description": "Address that's allowed to change vesting token", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "vesting_token": { + "description": "[`AssetInfo`] of the vested token", + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Extensions": { + "description": "Contains extensions information for the contract.", + "type": "object", + "required": [ + "historical", + "managed", + "with_managers" + ], + "properties": { + "historical": { + "description": "Whether the historical extension is enabled for the contract.", + "type": "boolean" + }, + "managed": { + "description": "Whether the managed extension is enabled for the contract.", + "type": "boolean" + }, + "with_managers": { + "description": "Whether the with_managers extension is enabled for the contract.", + "type": "boolean" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/vesting-lp-pcl/schema/raw/response_to_historical_extension.json b/contracts/vesting-lp-pcl/schema/raw/response_to_historical_extension.json new file mode 100644 index 00000000..83edad79 --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_historical_extension.json @@ -0,0 +1,59 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsgHistorical", + "description": "This structure describes the query messages available in a historical vesting contract.", + "oneOf": [ + { + "description": "Returns the total unclaimed amount of tokens for a specific address at certain height.", + "type": "object", + "required": [ + "unclaimed_amount_at_height" + ], + "properties": { + "unclaimed_amount_at_height": { + "type": "object", + "required": [ + "address", + "height" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total unclaimed amount of tokens for all the users at certain height.", + "type": "object", + "required": [ + "unclaimed_total_amount_at_height" + ], + "properties": { + "unclaimed_total_amount_at_height": { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/vesting-lp-pcl/schema/raw/response_to_managed_extension.json b/contracts/vesting-lp-pcl/schema/raw/response_to_managed_extension.json new file mode 100644 index 00000000..a46573aa --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_managed_extension.json @@ -0,0 +1,6 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Binary", + "description": "Binary is a wrapper around Vec 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. See also .", + "type": "string" +} diff --git a/contracts/vesting-lp-pcl/schema/raw/response_to_timestamp.json b/contracts/vesting-lp-pcl/schema/raw/response_to_timestamp.json new file mode 100644 index 00000000..7b729a7b --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_timestamp.json @@ -0,0 +1,7 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "uint64", + "type": "integer", + "format": "uint64", + "minimum": 0.0 +} diff --git a/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_account.json b/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_account.json new file mode 100644 index 00000000..b85fc3d3 --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_account.json @@ -0,0 +1,119 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VestingAccountResponse", + "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", + "type": "object", + "required": [ + "address", + "info" + ], + "properties": { + "address": { + "description": "The address that's vesting tokens", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "info": { + "description": "Vesting information", + "allOf": [ + { + "$ref": "#/definitions/VestingInfo" + } + ] + } + }, + "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" + }, + "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" + }, + "VestingInfo": { + "description": "This structure stores parameters for a batch of vesting schedules.", + "type": "object", + "required": [ + "released_amount", + "schedules" + ], + "properties": { + "released_amount": { + "description": "The total amount of vested tokens already claimed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "schedules": { + "description": "The vesting schedules", + "type": "array", + "items": { + "$ref": "#/definitions/VestingSchedule" + } + } + }, + "additionalProperties": false + }, + "VestingSchedule": { + "description": "This structure stores parameters for a specific vesting schedule", + "type": "object", + "required": [ + "start_point" + ], + "properties": { + "end_point": { + "description": "The end point for the vesting schedule", + "anyOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + }, + { + "type": "null" + } + ] + }, + "start_point": { + "description": "The start date for the vesting schedule", + "allOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + } + ] + } + }, + "additionalProperties": false + }, + "VestingSchedulePoint": { + "description": "This structure stores the parameters used to create a vesting schedule.", + "type": "object", + "required": [ + "amount", + "time" + ], + "properties": { + "amount": { + "description": "The amount of tokens being vested", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "time": { + "description": "The start time for the vesting schedule", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_accounts.json b/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_accounts.json new file mode 100644 index 00000000..6d2af531 --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_accounts.json @@ -0,0 +1,136 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VestingAccountsResponse", + "description": "This structure describes a custom struct used to return vesting data for multiple vesting targets.", + "type": "object", + "required": [ + "vesting_accounts" + ], + "properties": { + "vesting_accounts": { + "description": "A list of accounts that are vesting tokens", + "type": "array", + "items": { + "$ref": "#/definitions/VestingAccountResponse" + } + } + }, + "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" + }, + "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" + }, + "VestingAccountResponse": { + "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", + "type": "object", + "required": [ + "address", + "info" + ], + "properties": { + "address": { + "description": "The address that's vesting tokens", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "info": { + "description": "Vesting information", + "allOf": [ + { + "$ref": "#/definitions/VestingInfo" + } + ] + } + }, + "additionalProperties": false + }, + "VestingInfo": { + "description": "This structure stores parameters for a batch of vesting schedules.", + "type": "object", + "required": [ + "released_amount", + "schedules" + ], + "properties": { + "released_amount": { + "description": "The total amount of vested tokens already claimed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "schedules": { + "description": "The vesting schedules", + "type": "array", + "items": { + "$ref": "#/definitions/VestingSchedule" + } + } + }, + "additionalProperties": false + }, + "VestingSchedule": { + "description": "This structure stores parameters for a specific vesting schedule", + "type": "object", + "required": [ + "start_point" + ], + "properties": { + "end_point": { + "description": "The end point for the vesting schedule", + "anyOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + }, + { + "type": "null" + } + ] + }, + "start_point": { + "description": "The start date for the vesting schedule", + "allOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + } + ] + } + }, + "additionalProperties": false + }, + "VestingSchedulePoint": { + "description": "This structure stores the parameters used to create a vesting schedule.", + "type": "object", + "required": [ + "amount", + "time" + ], + "properties": { + "amount": { + "description": "The amount of tokens being vested", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "time": { + "description": "The start time for the vesting schedule", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_state.json b/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_state.json new file mode 100644 index 00000000..b38701e4 --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_vesting_state.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VestingState", + "description": "This structure stores the accumulated vesting information for all addresses.", + "type": "object", + "required": [ + "total_granted", + "total_released" + ], + "properties": { + "total_granted": { + "description": "The total amount of tokens granted to the users", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "total_released": { + "description": "The total amount of tokens already claimed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/vesting-lp-pcl/schema/raw/response_to_with_managers_extension.json b/contracts/vesting-lp-pcl/schema/raw/response_to_with_managers_extension.json new file mode 100644 index 00000000..bcacc2ac --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/raw/response_to_with_managers_extension.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsgWithManagers", + "description": "This structure describes the query messages available in a with_managers vesting contract.", + "oneOf": [ + { + "description": "Returns list of vesting managers (the persons who are able to add/remove vesting schedules)", + "type": "object", + "required": [ + "vesting_managers" + ], + "properties": { + "vesting_managers": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/vesting-lp-pcl/schema/vesting-lp.json b/contracts/vesting-lp-pcl/schema/vesting-lp.json new file mode 100644 index 00000000..395775f8 --- /dev/null +++ b/contracts/vesting-lp-pcl/schema/vesting-lp.json @@ -0,0 +1,1331 @@ +{ + "contract_name": "vesting-lp", + "contract_version": "1.1.0", + "idl_version": "1.0.0", + "instantiate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "InstantiateMsg", + "description": "This structure describes the parameters used for creating a contract.", + "type": "object", + "required": [ + "owner", + "token_info_manager", + "vesting_managers" + ], + "properties": { + "owner": { + "description": "Address allowed to change contract parameters", + "type": "string" + }, + "token_info_manager": { + "description": "Token info manager address", + "type": "string" + }, + "vesting_managers": { + "description": "Initial list of whitelisted vesting managers", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in a vesting contract.", + "oneOf": [ + { + "description": "Claim claims vested tokens and sends them to a recipient", + "type": "object", + "required": [ + "claim" + ], + "properties": { + "claim": { + "type": "object", + "properties": { + "amount": { + "description": "The amount of tokens to claim", + "anyOf": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "null" + } + ] + }, + "recipient": { + "description": "The address that receives the vested tokens", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template", + "type": "object", + "required": [ + "receive" + ], + "properties": { + "receive": { + "$ref": "#/definitions/Cw20ReceiveMsg" + } + }, + "additionalProperties": false + }, + { + "description": "RegisterVestingAccounts registers vesting targets/accounts", + "type": "object", + "required": [ + "register_vesting_accounts" + ], + "properties": { + "register_vesting_accounts": { + "type": "object", + "required": [ + "vesting_accounts" + ], + "properties": { + "vesting_accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/VestingAccount" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Creates a request to change contract ownership ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "propose_new_owner" + ], + "properties": { + "propose_new_owner": { + "type": "object", + "required": [ + "expires_in", + "owner" + ], + "properties": { + "expires_in": { + "description": "The validity period of the offer to change the owner", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "owner": { + "description": "The newly proposed owner", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes a request to change contract ownership ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "drop_ownership_proposal" + ], + "properties": { + "drop_ownership_proposal": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Claims contract ownership ## Executor Only the newly proposed owner can execute this", + "type": "object", + "required": [ + "claim_ownership" + ], + "properties": { + "claim_ownership": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Sets vesting token ## Executor Only the current owner or token info manager can execute this", + "type": "object", + "required": [ + "set_vesting_token" + ], + "properties": { + "set_vesting_token": { + "type": "object", + "required": [ + "vesting_token" + ], + "properties": { + "vesting_token": { + "$ref": "#/definitions/AssetInfo" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the managed extension for vesting contracts.", + "type": "object", + "required": [ + "managed_extension" + ], + "properties": { + "managed_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/ExecuteMsgManaged" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the with_managers extension for vesting contracts.", + "type": "object", + "required": [ + "with_managers_extension" + ], + "properties": { + "with_managers_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/ExecuteMsgWithManagers" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the historical extension for vesting contracts.", + "type": "object", + "required": [ + "historical_extension" + ], + "properties": { + "historical_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/ExecuteMsgHistorical" + } + }, + "additionalProperties": false + } + }, + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Binary": { + "description": "Binary is a wrapper around Vec 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. See also .", + "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 + }, + "ExecuteMsgHistorical": { + "description": "This structure describes the execute messages available in a historical vesting contract.", + "type": "string", + "enum": [] + }, + "ExecuteMsgManaged": { + "description": "This structure describes the execute messages available in a managed vesting contract.", + "oneOf": [ + { + "description": "Removes vesting targets/accounts. ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "remove_vesting_accounts" + ], + "properties": { + "remove_vesting_accounts": { + "type": "object", + "required": [ + "clawback_account", + "vesting_accounts" + ], + "properties": { + "clawback_account": { + "description": "Specifies the account that will receive the funds taken from the vesting accounts.", + "type": "string" + }, + "vesting_accounts": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "ExecuteMsgWithManagers": { + "description": "This structure describes the execute messages available in a with_managers vesting contract.", + "oneOf": [ + { + "description": "Adds vesting managers ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "add_vesting_managers" + ], + "properties": { + "add_vesting_managers": { + "type": "object", + "required": [ + "managers" + ], + "properties": { + "managers": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Removes vesting managers ## Executor Only the current owner can execute this", + "type": "object", + "required": [ + "remove_vesting_managers" + ], + "properties": { + "remove_vesting_managers": { + "type": "object", + "required": [ + "managers" + ], + "properties": { + "managers": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + }, + "VestingAccount": { + "description": "This structure stores vesting information for a specific address that is getting tokens.", + "type": "object", + "required": [ + "address", + "schedules" + ], + "properties": { + "address": { + "description": "The address that is getting tokens", + "type": "string" + }, + "schedules": { + "description": "The vesting schedules targeted at the `address`", + "type": "array", + "items": { + "$ref": "#/definitions/VestingSchedule" + } + } + }, + "additionalProperties": false + }, + "VestingSchedule": { + "description": "This structure stores parameters for a specific vesting schedule", + "type": "object", + "required": [ + "start_point" + ], + "properties": { + "end_point": { + "description": "The end point for the vesting schedule", + "anyOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + }, + { + "type": "null" + } + ] + }, + "start_point": { + "description": "The start date for the vesting schedule", + "allOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + } + ] + } + }, + "additionalProperties": false + }, + "VestingSchedulePoint": { + "description": "This structure stores the parameters used to create a vesting schedule.", + "type": "object", + "required": [ + "amount", + "time" + ], + "properties": { + "amount": { + "description": "The amount of tokens being vested", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "time": { + "description": "The start time for the vesting schedule", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "query": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in a vesting contract.", + "oneOf": [ + { + "description": "Returns the configuration for the contract using a [`ConfigResponse`] object.", + "type": "object", + "required": [ + "config" + ], + "properties": { + "config": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns information about an address vesting tokens using a [`VestingAccountResponse`] object.", + "type": "object", + "required": [ + "vesting_account" + ], + "properties": { + "vesting_account": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object.", + "type": "object", + "required": [ + "vesting_accounts" + ], + "properties": { + "vesting_accounts": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "order_by": { + "anyOf": [ + { + "$ref": "#/definitions/OrderBy" + }, + { + "type": "null" + } + ] + }, + "start_after": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total unvested amount of tokens for a specific address.", + "type": "object", + "required": [ + "available_amount" + ], + "properties": { + "available_amount": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Timestamp returns the current timestamp", + "type": "object", + "required": [ + "timestamp" + ], + "properties": { + "timestamp": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "VestingState returns the current vesting state.", + "type": "object", + "required": [ + "vesting_state" + ], + "properties": { + "vesting_state": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the managed extension for vesting contracts.", + "type": "object", + "required": [ + "managed_extension" + ], + "properties": { + "managed_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryMsgManaged" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the with_managers extension for vesting contracts.", + "type": "object", + "required": [ + "with_managers_extension" + ], + "properties": { + "with_managers_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryMsgWithManagers" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Contains messages associated with the historical extension for vesting contracts.", + "type": "object", + "required": [ + "historical_extension" + ], + "properties": { + "historical_extension": { + "type": "object", + "required": [ + "msg" + ], + "properties": { + "msg": { + "$ref": "#/definitions/QueryMsgHistorical" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "OrderBy": { + "description": "This enum describes the types of sorting that can be applied to some piece of data", + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "QueryMsgHistorical": { + "description": "This structure describes the query messages available in a historical vesting contract.", + "oneOf": [ + { + "description": "Returns the total unclaimed amount of tokens for a specific address at certain height.", + "type": "object", + "required": [ + "unclaimed_amount_at_height" + ], + "properties": { + "unclaimed_amount_at_height": { + "type": "object", + "required": [ + "address", + "height" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total unclaimed amount of tokens for all the users at certain height.", + "type": "object", + "required": [ + "unclaimed_total_amount_at_height" + ], + "properties": { + "unclaimed_total_amount_at_height": { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "QueryMsgManaged": { + "description": "This structure describes the query messages available in a managed vesting contract.", + "type": "string", + "enum": [] + }, + "QueryMsgWithManagers": { + "description": "This structure describes the query messages available in a with_managers vesting contract.", + "oneOf": [ + { + "description": "Returns list of vesting managers (the persons who are able to add/remove vesting schedules)", + "type": "object", + "required": [ + "vesting_managers" + ], + "properties": { + "vesting_managers": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "description": "This structure describes a migration message. We currently take no arguments for migrations.", + "type": "object", + "additionalProperties": false + }, + "sudo": null, + "responses": { + "available_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" + }, + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Config", + "description": "This structure stores the main parameters for the generator vesting contract.", + "type": "object", + "required": [ + "extensions", + "owner", + "token_info_manager" + ], + "properties": { + "extensions": { + "description": "Contains extensions information of the contract", + "allOf": [ + { + "$ref": "#/definitions/Extensions" + } + ] + }, + "owner": { + "description": "Address that's allowed to change contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "token_info_manager": { + "description": "Address that's allowed to change vesting token", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "vesting_token": { + "description": "[`AssetInfo`] of the vested token", + "anyOf": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "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" + }, + "AssetInfo": { + "description": "This enum describes available Token types. ## Examples ``` # use cosmwasm_std::Addr; # use astroport::asset::AssetInfo::{NativeToken, Token}; Token { contract_addr: Addr::unchecked(\"stake...\") }; NativeToken { denom: String::from(\"uluna\") }; ```", + "oneOf": [ + { + "description": "Non-native Token", + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "object", + "required": [ + "contract_addr" + ], + "properties": { + "contract_addr": { + "$ref": "#/definitions/Addr" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Native token", + "type": "object", + "required": [ + "native_token" + ], + "properties": { + "native_token": { + "type": "object", + "required": [ + "denom" + ], + "properties": { + "denom": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Extensions": { + "description": "Contains extensions information for the contract.", + "type": "object", + "required": [ + "historical", + "managed", + "with_managers" + ], + "properties": { + "historical": { + "description": "Whether the historical extension is enabled for the contract.", + "type": "boolean" + }, + "managed": { + "description": "Whether the managed extension is enabled for the contract.", + "type": "boolean" + }, + "with_managers": { + "description": "Whether the with_managers extension is enabled for the contract.", + "type": "boolean" + } + }, + "additionalProperties": false + } + } + }, + "historical_extension": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsgHistorical", + "description": "This structure describes the query messages available in a historical vesting contract.", + "oneOf": [ + { + "description": "Returns the total unclaimed amount of tokens for a specific address at certain height.", + "type": "object", + "required": [ + "unclaimed_amount_at_height" + ], + "properties": { + "unclaimed_amount_at_height": { + "type": "object", + "required": [ + "address", + "height" + ], + "properties": { + "address": { + "type": "string" + }, + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Returns the total unclaimed amount of tokens for all the users at certain height.", + "type": "object", + "required": [ + "unclaimed_total_amount_at_height" + ], + "properties": { + "unclaimed_total_amount_at_height": { + "type": "object", + "required": [ + "height" + ], + "properties": { + "height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "managed_extension": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Binary", + "description": "Binary is a wrapper around Vec 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. See also .", + "type": "string" + }, + "timestamp": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "uint64", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "vesting_account": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VestingAccountResponse", + "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", + "type": "object", + "required": [ + "address", + "info" + ], + "properties": { + "address": { + "description": "The address that's vesting tokens", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "info": { + "description": "Vesting information", + "allOf": [ + { + "$ref": "#/definitions/VestingInfo" + } + ] + } + }, + "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" + }, + "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" + }, + "VestingInfo": { + "description": "This structure stores parameters for a batch of vesting schedules.", + "type": "object", + "required": [ + "released_amount", + "schedules" + ], + "properties": { + "released_amount": { + "description": "The total amount of vested tokens already claimed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "schedules": { + "description": "The vesting schedules", + "type": "array", + "items": { + "$ref": "#/definitions/VestingSchedule" + } + } + }, + "additionalProperties": false + }, + "VestingSchedule": { + "description": "This structure stores parameters for a specific vesting schedule", + "type": "object", + "required": [ + "start_point" + ], + "properties": { + "end_point": { + "description": "The end point for the vesting schedule", + "anyOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + }, + { + "type": "null" + } + ] + }, + "start_point": { + "description": "The start date for the vesting schedule", + "allOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + } + ] + } + }, + "additionalProperties": false + }, + "VestingSchedulePoint": { + "description": "This structure stores the parameters used to create a vesting schedule.", + "type": "object", + "required": [ + "amount", + "time" + ], + "properties": { + "amount": { + "description": "The amount of tokens being vested", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "time": { + "description": "The start time for the vesting schedule", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "vesting_accounts": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VestingAccountsResponse", + "description": "This structure describes a custom struct used to return vesting data for multiple vesting targets.", + "type": "object", + "required": [ + "vesting_accounts" + ], + "properties": { + "vesting_accounts": { + "description": "A list of accounts that are vesting tokens", + "type": "array", + "items": { + "$ref": "#/definitions/VestingAccountResponse" + } + } + }, + "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" + }, + "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" + }, + "VestingAccountResponse": { + "description": "This structure describes a custom struct used to return vesting data about a specific vesting target.", + "type": "object", + "required": [ + "address", + "info" + ], + "properties": { + "address": { + "description": "The address that's vesting tokens", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "info": { + "description": "Vesting information", + "allOf": [ + { + "$ref": "#/definitions/VestingInfo" + } + ] + } + }, + "additionalProperties": false + }, + "VestingInfo": { + "description": "This structure stores parameters for a batch of vesting schedules.", + "type": "object", + "required": [ + "released_amount", + "schedules" + ], + "properties": { + "released_amount": { + "description": "The total amount of vested tokens already claimed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "schedules": { + "description": "The vesting schedules", + "type": "array", + "items": { + "$ref": "#/definitions/VestingSchedule" + } + } + }, + "additionalProperties": false + }, + "VestingSchedule": { + "description": "This structure stores parameters for a specific vesting schedule", + "type": "object", + "required": [ + "start_point" + ], + "properties": { + "end_point": { + "description": "The end point for the vesting schedule", + "anyOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + }, + { + "type": "null" + } + ] + }, + "start_point": { + "description": "The start date for the vesting schedule", + "allOf": [ + { + "$ref": "#/definitions/VestingSchedulePoint" + } + ] + } + }, + "additionalProperties": false + }, + "VestingSchedulePoint": { + "description": "This structure stores the parameters used to create a vesting schedule.", + "type": "object", + "required": [ + "amount", + "time" + ], + "properties": { + "amount": { + "description": "The amount of tokens being vested", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "time": { + "description": "The start time for the vesting schedule", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "vesting_state": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "VestingState", + "description": "This structure stores the accumulated vesting information for all addresses.", + "type": "object", + "required": [ + "total_granted", + "total_released" + ], + "properties": { + "total_granted": { + "description": "The total amount of tokens granted to the users", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + }, + "total_released": { + "description": "The total amount of tokens already claimed", + "allOf": [ + { + "$ref": "#/definitions/Uint128" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "with_managers_extension": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsgWithManagers", + "description": "This structure describes the query messages available in a with_managers vesting contract.", + "oneOf": [ + { + "description": "Returns list of vesting managers (the persons who are able to add/remove vesting schedules)", + "type": "object", + "required": [ + "vesting_managers" + ], + "properties": { + "vesting_managers": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/contracts/vesting-lp-pcl/src/contract.rs b/contracts/vesting-lp-pcl/src/contract.rs new file mode 100644 index 00000000..4b3a4659 --- /dev/null +++ b/contracts/vesting-lp-pcl/src/contract.rs @@ -0,0 +1,46 @@ +use crate::msg::InstantiateMsg; +use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cw2::set_contract_version; +use vesting_base::builder::VestingBaseBuilder; +use vesting_base::error::ContractError; +use vesting_base::handlers::{execute as base_execute, query as base_query}; +use vesting_base::msg::{ExecuteMsg, QueryMsg}; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "neutron-vesting-lp"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +/// Creates a new contract with the specified parameters packed in the `msg` variable. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + VestingBaseBuilder::default() + .historical() + .with_managers(msg.vesting_managers) + .build(deps, msg.owner, msg.token_info_manager)?; + Ok(Response::default()) +} + +/// Exposes execute functions available in the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + base_execute(deps, env, info, msg) +} + +/// Exposes all the queries available in the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + base_query(deps, env, msg) +} diff --git a/contracts/vesting-lp-pcl/src/lib.rs b/contracts/vesting-lp-pcl/src/lib.rs new file mode 100644 index 00000000..08d6d688 --- /dev/null +++ b/contracts/vesting-lp-pcl/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod msg; + +#[cfg(test)] +mod tests; diff --git a/contracts/vesting-lp-pcl/src/msg.rs b/contracts/vesting-lp-pcl/src/msg.rs new file mode 100644 index 00000000..b35ee7ff --- /dev/null +++ b/contracts/vesting-lp-pcl/src/msg.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::cw_serde; + +/// This structure describes the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Address allowed to change contract parameters + pub owner: String, + /// Initial list of whitelisted vesting managers + pub vesting_managers: Vec, + /// Token info manager address + pub token_info_manager: String, +} diff --git a/contracts/vesting-lp-pcl/src/tests/integration.rs b/contracts/vesting-lp-pcl/src/tests/integration.rs new file mode 100644 index 00000000..5a150e97 --- /dev/null +++ b/contracts/vesting-lp-pcl/src/tests/integration.rs @@ -0,0 +1,1281 @@ +use crate::msg::InstantiateMsg; +use astroport::asset::{native_asset_info, token_asset_info}; +use astroport::querier::query_balance; +use astroport::token::InstantiateMsg as TokenInstantiateMsg; +use cosmwasm_std::{coin, coins, to_binary, Addr, StdResult, Timestamp, Uint128}; +use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, MinterResponse}; +use cw_multi_test::{App, ContractWrapper, Executor}; +use cw_utils::PaymentError; +use vesting_base::error::ContractError; +use vesting_base::msg::{ + Cw20HookMsg, ExecuteMsg, ExecuteMsgWithManagers, QueryMsg, QueryMsgHistorical, + QueryMsgWithManagers, +}; +use vesting_base::types::{ + Config, VestingAccount, VestingAccountResponse, VestingSchedule, VestingSchedulePoint, +}; + +const OWNER1: &str = "owner1"; +const TOKEN_MANAGER: &str = "token_manager"; +const USER1: &str = "user1"; +const USER2: &str = "user2"; +const TOKEN_INITIAL_AMOUNT: u128 = 1_000_000_000_000_000; +const VESTING_TOKEN: &str = "vesting_token"; +const BLOCK_TIME: u64 = 5; + +#[test] +fn claim() { + let user1 = Addr::unchecked(USER1); + let owner = Addr::unchecked(OWNER1); + + let mut app = mock_app(&owner); + + let token_code_id = store_token_code(&mut app); + + let cw20_token_instance = + instantiate_token(&mut app, token_code_id, "NTRN", Some(1_000_000_000_000_000)); + + let vesting_instance = instantiate_vesting(&mut app, &cw20_token_instance); + + let msg = Cw20ExecuteMsg::Send { + contract: vesting_instance.to_string(), + msg: to_binary(&Cw20HookMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![ + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(101).seconds(), + amount: Uint128::new(200), + }), + }, + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(110).seconds(), + amount: Uint128::new(100), + }), + }, + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(200).seconds(), + amount: Uint128::new(100), + }), + }, + ], + }], + }) + .unwrap(), + amount: Uint128::from(300u128), + }; + + let res = app + .execute_contract(owner.clone(), cw20_token_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!(res.root_cause().to_string(), "Vesting schedule amount error. The total amount should be equal to the CW20 receive amount."); + + let msg = Cw20ExecuteMsg::Send { + contract: vesting_instance.to_string(), + msg: to_binary(&Cw20HookMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![ + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(101).seconds(), + amount: Uint128::new(100), + }), + }, + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(110).seconds(), + amount: Uint128::new(100), + }), + }, + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(200).seconds(), + amount: Uint128::new(100), + }), + }, + ], + }], + }) + .unwrap(), + amount: Uint128::from(300u128), + }; + + app.execute_contract(owner.clone(), cw20_token_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + let user1_vesting_amount: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + assert_eq!(user1_vesting_amount.clone(), Uint128::new(300u128)); + + // Check owner balance + check_token_balance( + &mut app, + &cw20_token_instance, + &owner, + TOKEN_INITIAL_AMOUNT - 300u128, + ); + + // Check vesting balance + check_token_balance(&mut app, &cw20_token_instance, &vesting_instance, 300u128); + + let msg = ExecuteMsg::Claim { + recipient: None, + amount: None, + }; + let _res = app + .execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = QueryMsg::VestingAccount { + address: user1.to_string(), + }; + + let vesting_res: VestingAccountResponse = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + assert_eq!(vesting_res.info.released_amount, Uint128::from(300u128)); + + // Check vesting balance + check_token_balance(&mut app, &cw20_token_instance, &vesting_instance, 0u128); + + // Check user balance + check_token_balance(&mut app, &cw20_token_instance, &user1, 300u128); + + // Owner balance mustn't change after claim + check_token_balance( + &mut app, + &cw20_token_instance, + &owner.clone(), + TOKEN_INITIAL_AMOUNT - 300u128, + ); + + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + // Check user balance after claim + let user1_vesting_amount: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + + assert_eq!(user1_vesting_amount.clone(), Uint128::new(0u128)); +} + +#[test] +fn claim_native() { + let user1 = Addr::unchecked(USER1); + let owner = Addr::unchecked(OWNER1); + + let mut app = mock_app(&owner); + + let token_code_id = store_token_code(&mut app); + + let random_token_instance = + instantiate_token(&mut app, token_code_id, "RND", Some(1_000_000_000)); + + mint_tokens(&mut app, &random_token_instance, &owner, 1_000_000_000); + + let vesting_instance = instantiate_vesting_remote_chain(&mut app); + + let msg = Cw20ExecuteMsg::Send { + contract: vesting_instance.to_string(), + msg: to_binary(&Cw20HookMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(101).seconds(), + amount: Uint128::new(200), + }), + }], + }], + }) + .unwrap(), + amount: Uint128::from(300u128), + }; + + let err = app + .execute_contract(owner.clone(), random_token_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let msg = ExecuteMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![ + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(101).seconds(), + amount: Uint128::new(100), + }), + }, + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(110).seconds(), + amount: Uint128::new(100), + }), + }, + VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(200).seconds(), + amount: Uint128::new(100), + }), + }, + ], + }], + }; + + app.execute_contract( + owner.clone(), + vesting_instance.clone(), + &msg, + &coins(300, VESTING_TOKEN), + ) + .unwrap(); + + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + let user1_vesting_amount: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + assert_eq!(user1_vesting_amount.clone(), Uint128::new(300u128)); + + // Check owner balance + let bal = query_balance(&app.wrap(), &owner, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 300u128); + + // Check vesting balance + let bal = query_balance(&app.wrap(), &vesting_instance, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, 300u128); + + let msg = ExecuteMsg::Claim { + recipient: None, + amount: None, + }; + app.execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) + .unwrap(); + + let vesting_res: VestingAccountResponse = app + .wrap() + .query_wasm_smart( + vesting_instance.clone(), + &QueryMsg::VestingAccount { + address: user1.to_string(), + }, + ) + .unwrap(); + assert_eq!(vesting_res.info.released_amount, Uint128::from(300u128)); + + // Check vesting balance + let bal = query_balance(&app.wrap(), &vesting_instance, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, 0); + + // Check user balance + let bal = query_balance(&app.wrap(), &user1, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, 300); + + // Owner balance mustn't change after claim + let bal = query_balance(&app.wrap(), &owner, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 300u128); + + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + // Check user balance after claim + let user1_vesting_amount: Uint128 = + app.wrap().query_wasm_smart(vesting_instance, &msg).unwrap(); + + assert_eq!(user1_vesting_amount.clone(), Uint128::new(0u128)); +} + +#[test] +fn register_vesting_accounts() { + let user1 = Addr::unchecked(USER1); + let user2 = Addr::unchecked(USER2); + let owner = Addr::unchecked(OWNER1); + + let mut app = mock_app(&owner); + + let token_code_id = store_token_code(&mut app); + + let cw20_token_instance = + instantiate_token(&mut app, token_code_id, "NTRN", Some(1_000_000_000_000_000)); + + let noname_token_instance = instantiate_token( + &mut app, + token_code_id, + "NONAME", + Some(1_000_000_000_000_000), + ); + + mint_tokens( + &mut app, + &noname_token_instance, + &owner, + TOKEN_INITIAL_AMOUNT, + ); + + let vesting_instance = instantiate_vesting(&mut app, &cw20_token_instance); + + let msg = Cw20ExecuteMsg::Send { + contract: vesting_instance.to_string(), + msg: to_binary(&Cw20HookMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(150).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::new(100), + }), + }], + }], + }) + .unwrap(), + amount: Uint128::from(100u128), + }; + + let res = app + .execute_contract(owner.clone(), cw20_token_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!(res.root_cause().to_string(), "Vesting schedule error on addr: user1. Should satisfy: (start < end and at_start < total) or (start = end and at_start = total)"); + + let msg = Cw20ExecuteMsg::Send { + contract: vesting_instance.to_string(), + msg: to_binary(&Cw20HookMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(150).seconds(), + amount: Uint128::new(100), + }), + }], + }], + }) + .unwrap(), + amount: Uint128::from(100u128), + }; + + let res = app + .execute_contract( + user1.clone(), + cw20_token_instance.clone(), + &msg.clone(), + &[], + ) + .unwrap_err(); + assert_eq!(res.root_cause().to_string(), "Cannot Sub with 0 and 100"); + + let res = app + .execute_contract(owner.clone(), noname_token_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!(res.root_cause().to_string(), "Unauthorized"); + + // Checking that execute endpoint with native coin is unreachable if the asset is a cw20 token + let native_msg = ExecuteMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(150).seconds(), + amount: Uint128::new(100), + }), + }], + }], + }; + + let err = app + .execute_contract( + owner.clone(), + vesting_instance.clone(), + &native_msg, + &coins(100u128, "random_coin"), + ) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let _res = app + .execute_contract(owner.clone(), cw20_token_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + let user1_vesting_amount: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + + assert_eq!(user1_vesting_amount.clone(), Uint128::new(100u128)); + check_token_balance( + &mut app, + &cw20_token_instance, + &owner.clone(), + TOKEN_INITIAL_AMOUNT - 100u128, + ); + check_token_balance(&mut app, &cw20_token_instance, &vesting_instance, 100u128); + + // Let's check user1's final vesting amount after add schedule for a new one + let msg = Cw20ExecuteMsg::Send { + contract: vesting_instance.to_string(), + msg: to_binary(&Cw20HookMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user2.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(150).seconds(), + amount: Uint128::new(200), + }), + }], + }], + }) + .unwrap(), + amount: Uint128::from(200u128), + }; + + let _res = app + .execute_contract(owner.clone(), cw20_token_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = QueryMsg::AvailableAmount { + address: user2.to_string(), + }; + + let user2_vesting_amount: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + + check_token_balance( + &mut app, + &cw20_token_instance, + &owner.clone(), + TOKEN_INITIAL_AMOUNT - 300u128, + ); + check_token_balance(&mut app, &cw20_token_instance, &vesting_instance, 300u128); + // A new schedule has been added successfully and an old one hasn't changed. + // The new schedule doesn't have the same value as the old one. + assert_eq!(user2_vesting_amount, Uint128::new(200u128)); + assert_eq!(user1_vesting_amount, Uint128::from(100u128)); + + // Add one more vesting schedule; final amount to vest must increase + let msg = Cw20ExecuteMsg::Send { + contract: vesting_instance.to_string(), + msg: to_binary(&Cw20HookMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(200).seconds(), + amount: Uint128::new(10), + }), + }], + }], + }) + .unwrap(), + amount: Uint128::from(10u128), + }; + + let _res = app + .execute_contract(owner.clone(), cw20_token_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + + assert_eq!(vesting_res, Uint128::new(110u128)); + check_token_balance( + &mut app, + &cw20_token_instance, + &owner.clone(), + TOKEN_INITIAL_AMOUNT - 310u128, + ); + check_token_balance(&mut app, &cw20_token_instance, &vesting_instance, 310u128); + + let msg = ExecuteMsg::Claim { + recipient: None, + amount: None, + }; + let _res = app + .execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = QueryMsg::VestingAccount { + address: user1.to_string(), + }; + + let vesting_res: VestingAccountResponse = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + assert_eq!(vesting_res.info.released_amount, Uint128::from(110u128)); + check_token_balance(&mut app, &cw20_token_instance, &vesting_instance, 200u128); + check_token_balance(&mut app, &cw20_token_instance, &user1, 110u128); + + // Owner balance mustn't change after claim + check_token_balance( + &mut app, + &cw20_token_instance, + &owner.clone(), + TOKEN_INITIAL_AMOUNT - 310u128, + ); +} + +#[test] +fn register_vesting_accounts_native() { + let user1 = Addr::unchecked(USER1); + let user2 = Addr::unchecked(USER2); + let owner = Addr::unchecked(OWNER1); + + let mut app = mock_app(&owner); + + let token_code_id = store_token_code(&mut app); + + let random_token_instance = + instantiate_token(&mut app, token_code_id, "RND", Some(1_000_000_000_000_000)); + + mint_tokens( + &mut app, + &random_token_instance, + &owner, + TOKEN_INITIAL_AMOUNT, + ); + + let vesting_instance = instantiate_vesting_remote_chain(&mut app); + + let msg = Cw20ExecuteMsg::Send { + contract: vesting_instance.to_string(), + msg: to_binary(&Cw20HookMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(150).seconds(), + amount: Uint128::new(100), + }), + }], + }], + }) + .unwrap(), + amount: Uint128::from(100u128), + }; + + let err = app + .execute_contract(owner.clone(), random_token_instance.clone(), &msg, &[]) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + // Checking that execute endpoint with random native coin is unreachable + let native_msg = ExecuteMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(150).seconds(), + amount: Uint128::new(100), + }), + }], + }], + }; + + let err = app + .execute_contract( + owner.clone(), + vesting_instance.clone(), + &native_msg, + &coins(100u128, "random_coin"), + ) + .unwrap_err(); + assert_eq!( + ContractError::PaymentError(PaymentError::MissingDenom("vesting_token".to_string())), + err.downcast().unwrap() + ); + + app.execute_contract( + owner.clone(), + vesting_instance.clone(), + &native_msg, + &coins(100u128, VESTING_TOKEN), + ) + .unwrap(); + + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + let user1_vesting_amount: Uint128 = app + .wrap() + .query_wasm_smart(&vesting_instance, &msg) + .unwrap(); + assert_eq!(user1_vesting_amount.u128(), 100u128); + + let bal = query_balance(&app.wrap(), &owner, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 100u128); + + let bal = query_balance(&app.wrap(), &vesting_instance, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, 100); + + // Let's check user1's final vesting amount after add schedule for a new one + let msg = ExecuteMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user2.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(150).seconds(), + amount: Uint128::new(200), + }), + }], + }], + }; + + app.execute_contract( + owner.clone(), + vesting_instance.clone(), + &msg, + &coins(200, VESTING_TOKEN), + ) + .unwrap(); + + let msg = QueryMsg::AvailableAmount { + address: user2.to_string(), + }; + + let user2_vesting_amount: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + + let bal = query_balance(&app.wrap(), &owner, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 300u128); + let bal = query_balance(&app.wrap(), &vesting_instance, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, 300u128); + + // A new schedule has been added successfully and an old one hasn't changed. + // The new schedule doesn't have the same value as the old one. + assert_eq!(user2_vesting_amount, Uint128::new(200u128)); + assert_eq!(user1_vesting_amount, Uint128::from(100u128)); + + // Add one more vesting schedule; final amount to vest must increase + let msg = ExecuteMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: Timestamp::from_seconds(100).seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: Timestamp::from_seconds(200).seconds(), + amount: Uint128::new(10), + }), + }], + }], + }; + + app.execute_contract( + owner.clone(), + vesting_instance.clone(), + &msg, + &coins(10, VESTING_TOKEN), + ) + .unwrap(); + + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + assert_eq!(vesting_res, Uint128::new(110u128)); + + let bal = query_balance(&app.wrap(), &owner, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 310u128); + let bal = query_balance(&app.wrap(), &vesting_instance, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, 310u128); + + let msg = ExecuteMsg::Claim { + recipient: None, + amount: None, + }; + let _res = app + .execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) + .unwrap(); + + let msg = QueryMsg::VestingAccount { + address: user1.to_string(), + }; + + let vesting_res: VestingAccountResponse = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &msg) + .unwrap(); + assert_eq!(vesting_res.info.released_amount, Uint128::from(110u128)); + + let bal = query_balance(&app.wrap(), &vesting_instance, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, 200); + let bal = query_balance(&app.wrap(), &user1, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, 110u128); + + let bal = query_balance(&app.wrap(), &owner, VESTING_TOKEN) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 310u128); +} + +#[test] +fn query_at_height() { + let user1 = Addr::unchecked(USER1); + let user2 = Addr::unchecked(USER2); + let owner = Addr::unchecked(OWNER1); + + let mut app = mock_app(&owner); + let start_block_height = app.block_info().height; + + let vesting_instance = instantiate_vesting_remote_chain(&mut app); + + let native_msg = ExecuteMsg::RegisterVestingAccounts { + vesting_accounts: vec![ + VestingAccount { + address: user1.to_string(), + schedules: vec![ + VestingSchedule { + start_point: VestingSchedulePoint { + time: app.block_info().time.seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: app + .block_info() + .time + .plus_seconds(100 * BLOCK_TIME) + .seconds(), + amount: Uint128::new(50), + }), + }, + VestingSchedule { + start_point: VestingSchedulePoint { + time: app.block_info().time.seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: app + .block_info() + .time + .plus_seconds(100 * BLOCK_TIME) + .seconds(), + amount: Uint128::new(150), + }), + }, + ], + }, + VestingAccount { + address: user2.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: app.block_info().time.seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: app + .block_info() + .time + .plus_seconds(100 * BLOCK_TIME) + .seconds(), + amount: Uint128::new(1000), + }), + }], + }, + ], + }; + + app.execute_contract( + owner, + vesting_instance.clone(), + &native_msg, + &coins(1200, VESTING_TOKEN), + ) + .unwrap(); + + let query = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + for _ in 1..=10 { + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query) + .unwrap(); + assert_eq!(vesting_res, Uint128::new(0u128)); + + app.update_block(|b| { + b.height += 10; + b.time = b.time.plus_seconds(10 * BLOCK_TIME) + }); + + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query) + .unwrap(); + assert_eq!(vesting_res, Uint128::new(20u128)); + + let msg = ExecuteMsg::Claim { + recipient: None, + amount: None, + }; + let _res = app + .execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) + .unwrap(); + + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query) + .unwrap(); + assert_eq!(vesting_res, Uint128::new(0u128)); + } + app.update_block(|b| { + b.height += 100; + b.time = b.time.plus_seconds(100 * BLOCK_TIME) + }); + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query) + .unwrap(); + assert_eq!(vesting_res, Uint128::new(0u128)); + + let query_user_unclamed = QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedAmountAtHeight { + address: user1.to_string(), + height: start_block_height - 1, + }, + }; + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query_user_unclamed) + .unwrap(); + assert_eq!(vesting_res, Uint128::new(0u128)); + + let query_total_unclamed = QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedTotalAmountAtHeight { + height: start_block_height - 1, + }, + }; + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query_total_unclamed) + .unwrap(); + assert_eq!(vesting_res, Uint128::new(0u128)); + let max_unclaimed_user1: u128 = 200; + let max_unclaimed_total: u128 = 1200; + for i in 0..=10 { + let query = QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedAmountAtHeight { + address: user1.to_string(), + height: start_block_height + 1 + i * 10, + }, + }; + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query) + .unwrap(); + assert_eq!( + vesting_res, + Uint128::new(max_unclaimed_user1 - (i as u128) * 20) + ); + + let query_total_unclamed = QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedTotalAmountAtHeight { + height: start_block_height + 1 + i * 10, + }, + }; + let vesting_res: Uint128 = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query_total_unclamed) + .unwrap(); + assert_eq!( + vesting_res, + Uint128::new(max_unclaimed_total - (i as u128) * 20) + ); + } +} + +#[test] +fn vesting_managers() { + let user1 = Addr::unchecked(USER1); + let user2 = Addr::unchecked(USER2); + let owner = Addr::unchecked(OWNER1); + + let mut app = mock_app(&owner); + let vesting_instance = instantiate_vesting_remote_chain(&mut app); + + let query = QueryMsg::WithManagersExtension { + msg: QueryMsgWithManagers::VestingManagers {}, + }; + let vesting_res: Vec = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query) + .unwrap(); + assert_eq!(vesting_res.len(), 0,); + + let native_msg = ExecuteMsg::RegisterVestingAccounts { + vesting_accounts: vec![VestingAccount { + address: user1.to_string(), + schedules: vec![VestingSchedule { + start_point: VestingSchedulePoint { + time: app.block_info().time.seconds(), + amount: Uint128::zero(), + }, + end_point: Some(VestingSchedulePoint { + time: app + .block_info() + .time + .plus_seconds(100 * BLOCK_TIME) + .seconds(), + amount: Uint128::new(50), + }), + }], + }], + }; + let err = app + .execute_contract(user1.clone(), vesting_instance.clone(), &native_msg, &[]) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let add_manager_msg = ExecuteMsg::WithManagersExtension { + msg: ExecuteMsgWithManagers::AddVestingManagers { + managers: vec![user1.to_string()], + }, + }; + + let err = app + .execute_contract( + user1.clone(), + vesting_instance.clone(), + &add_manager_msg, + &[], + ) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let _res = app + .execute_contract( + owner.clone(), + vesting_instance.clone(), + &add_manager_msg, + &[], + ) + .unwrap(); + + let vesting_res: Vec = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &query) + .unwrap(); + assert_eq!(vesting_res, vec![Addr::unchecked(user1.clone())]); + + app.send_tokens(owner.clone(), user1.clone(), &coins(50, VESTING_TOKEN)) + .unwrap(); + + let _res = app + .execute_contract( + user1.clone(), + vesting_instance.clone(), + &native_msg, + &coins(50, VESTING_TOKEN), + ) + .unwrap(); + let err = app + .execute_contract(user2, vesting_instance.clone(), &native_msg, &[]) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let remove_manager_msg = ExecuteMsg::WithManagersExtension { + msg: ExecuteMsgWithManagers::RemoveVestingManagers { + managers: vec![user1.to_string()], + }, + }; + let err = app + .execute_contract(user1, vesting_instance.clone(), &remove_manager_msg, &[]) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let _res = app + .execute_contract(owner, vesting_instance.clone(), &remove_manager_msg, &[]) + .unwrap(); + + let vesting_res: Vec = app + .wrap() + .query_wasm_smart(vesting_instance, &query) + .unwrap(); + assert_eq!(vesting_res.len(), 0); +} + +fn mock_app(owner: &Addr) -> App { + App::new(|app, _, storage| { + app.bank + .init_balance( + storage, + owner, + vec![ + coin(TOKEN_INITIAL_AMOUNT, VESTING_TOKEN), + coin(10_000_000_000u128, "random_coin"), + ], + ) + .unwrap() + }) +} + +fn store_token_code(app: &mut App) -> u64 { + let cw20_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + app.store_code(cw20_token_contract) +} + +fn instantiate_token(app: &mut App, token_code_id: u64, name: &str, cap: Option) -> Addr { + let name = String::from(name); + + let msg = TokenInstantiateMsg { + name: name.clone(), + symbol: name.clone(), + decimals: 6, + initial_balances: vec![], + mint: Some(MinterResponse { + minter: String::from(OWNER1), + cap: cap.map(Uint128::from), + }), + marketing: None, + }; + + app.instantiate_contract( + token_code_id, + Addr::unchecked(OWNER1), + &msg, + &[], + name, + None, + ) + .unwrap() +} + +fn instantiate_vesting(app: &mut App, cw20_token_instance: &Addr) -> Addr { + let vesting_contract = Box::new(ContractWrapper::new_with_empty( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + )); + let owner = Addr::unchecked(OWNER1); + let token_manager = Addr::unchecked(TOKEN_MANAGER); + let vesting_code_id = app.store_code(vesting_contract); + + let init_msg = InstantiateMsg { + owner: OWNER1.to_string(), + token_info_manager: TOKEN_MANAGER.to_string(), + vesting_managers: vec![], + }; + + let vesting_instance = app + .instantiate_contract( + vesting_code_id, + owner.clone(), + &init_msg, + &[], + "Vesting", + None, + ) + .unwrap(); + let set_vesting_token_msg = ExecuteMsg::SetVestingToken { + vesting_token: token_asset_info(cw20_token_instance.clone()), + }; + app.execute_contract( + token_manager, + vesting_instance.clone(), + &set_vesting_token_msg, + &[], + ) + .unwrap(); + + let res: Config = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + cw20_token_instance.to_string(), + res.vesting_token.unwrap().to_string() + ); + + mint_tokens(app, cw20_token_instance, &owner, TOKEN_INITIAL_AMOUNT); + + check_token_balance(app, cw20_token_instance, &owner, TOKEN_INITIAL_AMOUNT); + + vesting_instance +} + +fn instantiate_vesting_remote_chain(app: &mut App) -> Addr { + let vesting_contract = Box::new(ContractWrapper::new_with_empty( + crate::contract::execute, + crate::contract::instantiate, + crate::contract::query, + )); + let owner = Addr::unchecked(OWNER1); + let token_manager = Addr::unchecked(TOKEN_MANAGER); + let vesting_code_id = app.store_code(vesting_contract); + + let init_msg = InstantiateMsg { + owner: OWNER1.to_string(), + token_info_manager: TOKEN_MANAGER.to_string(), + vesting_managers: vec![], + }; + + let res = app + .instantiate_contract(vesting_code_id, owner, &init_msg, &[], "Vesting", None) + .unwrap(); + let msg = ExecuteMsg::SetVestingToken { + vesting_token: native_asset_info(VESTING_TOKEN.to_string()), + }; + app.execute_contract(token_manager, res.clone(), &msg, &[]) + .unwrap(); + res +} + +fn mint_tokens(app: &mut App, token: &Addr, recipient: &Addr, amount: u128) { + let msg = Cw20ExecuteMsg::Mint { + recipient: recipient.to_string(), + amount: Uint128::from(amount), + }; + + app.execute_contract(Addr::unchecked(OWNER1), token.to_owned(), &msg, &[]) + .unwrap(); +} + +fn check_token_balance(app: &mut App, token: &Addr, address: &Addr, expected: u128) { + let msg = Cw20QueryMsg::Balance { + address: address.to_string(), + }; + let res: StdResult = app.wrap().query_wasm_smart(token, &msg); + assert_eq!(res.unwrap().balance, Uint128::from(expected)); +} diff --git a/contracts/vesting-lp-pcl/src/tests/mod.rs b/contracts/vesting-lp-pcl/src/tests/mod.rs new file mode 100644 index 00000000..6d3bbe60 --- /dev/null +++ b/contracts/vesting-lp-pcl/src/tests/mod.rs @@ -0,0 +1 @@ +mod integration; diff --git a/contracts/vesting-lp/.cargo/config b/contracts/vesting-lp/.cargo/config index f2fbc6a9..a79b8fdb 100644 --- a/contracts/vesting-lp/.cargo/config +++ b/contracts/vesting-lp/.cargo/config @@ -3,4 +3,4 @@ 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 vesting-lp_schema" +schema = "run --example vesting_schema" diff --git a/contracts/vesting-lp/Cargo.toml b/contracts/vesting-lp/Cargo.toml index 31f70c7c..ad739e0e 100644 --- a/contracts/vesting-lp/Cargo.toml +++ b/contracts/vesting-lp/Cargo.toml @@ -9,20 +9,20 @@ description = "Vesting contract with a voting capabilities. Provides queries to crate-type = ["cdylib", "rlib"] [features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] # use library feature to disable all init/handle/query exports library = [] [dependencies] -cw2 = { workspace = true } -cw20 = { workspace = true } -astroport = { workspace = true } -vesting-base = { workspace = true } -cosmwasm-schema = { workspace = true } -cosmwasm-std = { workspace = true } +cw2 = { version = "0.15" } +vesting-base = {path = "../../packages/vesting-base"} +vesting-base-lp = {path = "../../packages/vesting-base-lp"} +astroport = { git = "https://github.com/astroport-fi/astroport-core.git", tag = "v2.8.0" } +cosmwasm-schema = { version = "1.1", default-features = false } +cosmwasm-std = { version = "1.1" } +cw-storage-plus = "0.15" [dev-dependencies] -cw-multi-test = { workspace = true } +cw-multi-test = "0.15" astroport-token = {git = "https://github.com/astroport-fi/astroport-core.git", rev = "65ce7d1879cc5d95b09fa14202f0423bba52ae0e" } +cw20 = { version = "0.15" } cw-utils = "0.15" diff --git a/contracts/vesting-lp/examples/vesting_schema.rs b/contracts/vesting-lp/examples/vesting_schema.rs new file mode 100644 index 00000000..90284134 --- /dev/null +++ b/contracts/vesting-lp/examples/vesting_schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; +use vesting_base::msg::{ExecuteMsg, MigrateMsg, QueryMsg}; +use vesting_lp::msg::InstantiateMsg; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/vesting-lp/src/contract.rs b/contracts/vesting-lp/src/contract.rs index 4b3a4659..bf18ed13 100644 --- a/contracts/vesting-lp/src/contract.rs +++ b/contracts/vesting-lp/src/contract.rs @@ -2,9 +2,12 @@ use crate::msg::InstantiateMsg; use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; use cw2::set_contract_version; use vesting_base::builder::VestingBaseBuilder; -use vesting_base::error::ContractError; -use vesting_base::handlers::{execute as base_execute, query as base_query}; -use vesting_base::msg::{ExecuteMsg, QueryMsg}; +use vesting_base_lp::error::ContractError; +use vesting_base_lp::handlers::execute as base_execute; +use vesting_base_lp::handlers::migrate as base_migrate; +use vesting_base_lp::handlers::query as base_query; +use vesting_base_lp::msg::QueryMsg; +use vesting_base_lp::msg::{ExecuteMsg, MigrateMsg}; /// Contract name that is used for migration. const CONTRACT_NAME: &str = "neutron-vesting-lp"; @@ -44,3 +47,9 @@ pub fn execute( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { base_query(deps, env, msg) } + +/// Exposes migrate functions available in the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result { + base_migrate(deps, env, msg) +} diff --git a/contracts/vesting-lp/src/lib.rs b/contracts/vesting-lp/src/lib.rs index 08d6d688..112ecadc 100644 --- a/contracts/vesting-lp/src/lib.rs +++ b/contracts/vesting-lp/src/lib.rs @@ -1,5 +1,2 @@ pub mod contract; pub mod msg; - -#[cfg(test)] -mod tests; diff --git a/packages/vesting-base-lp/Cargo.toml b/packages/vesting-base-lp/Cargo.toml new file mode 100644 index 00000000..f7c90a1e --- /dev/null +++ b/packages/vesting-base-lp/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "vesting-base-lp" +version = "1.1.0" +authors = ["Astroport"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +serde = { version = "1.0.145", default-features = false, features = ["derive"] } +cw2 = { version = "0.15" } +cw20 = { version = "0.15" } +cosmwasm-std = { version = "1.1" } +cw-storage-plus = "0.15" +thiserror = { version = "1.0" } +cw-utils = "0.15" +cosmwasm-schema = { version = "1.1", default-features = false } +vesting-base = {path = "../vesting-base"} +astroport = { git = "https://github.com/astroport-fi/astroport-core.git", tag = "v2.8.0" } diff --git a/packages/vesting-base-lp/NOTICE b/packages/vesting-base-lp/NOTICE new file mode 100644 index 00000000..84b1c210 --- /dev/null +++ b/packages/vesting-base-lp/NOTICE @@ -0,0 +1,14 @@ +CW20-Base: A reference implementation for fungible token on CosmWasm +Copyright (C) 2020 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/vesting-base-lp/README.md b/packages/vesting-base-lp/README.md new file mode 100644 index 00000000..57accc63 --- /dev/null +++ b/packages/vesting-base-lp/README.md @@ -0,0 +1,251 @@ +# Neutron Vesting Base + +This library contains basis for configuration and initialisation of vesting contracts. It also contains data models and handlers for interaction with vesting contracts. + +## Usage + +1. To use the library for initialisation of a simple vesting contract just build a default vesting base in its instantiate message: +```rust +use vesting_base::builder::VestingBaseBuilder; + +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + ... + VestingBaseBuilder::default().build(deps, msg.owner, msg.vesting_token)?; + ... +``` + +Read about more advanced building in the [Extensions](#extensions) section. + +2. Simply pass the execute and query requests to the vesting base's execute and query handlers: +```rust +use vesting_base::handlers::{execute as base_execute, query as base_query}; + +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + base_execute(deps, env, info, msg) +} + +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + base_query(deps, env, msg) +} +``` + +### Messages + +The default version exposes the following messages: + +#### ExecuteMsg + +```rust +/// This structure describes the execute messages available in a vesting contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Claim claims vested tokens and sends them to a recipient + Claim { + /// The address that receives the vested tokens + recipient: Option, + /// The amount of tokens to claim + amount: Option, + }, + /// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template + Receive(Cw20ReceiveMsg), + /// RegisterVestingAccounts registers vesting targets/accounts + RegisterVestingAccounts { + vesting_accounts: Vec, + }, + /// Creates a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the offer to change the owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + DropOwnershipProposal {}, + /// Claims contract ownership + /// ## Executor + /// Only the newly proposed owner can execute this + ClaimOwnership {}, + /// Sets vesting token + /// ## Executor + /// Only the current owner or token info manager can execute this + SetVestingToken { vesting_token: AssetInfo }, + /// Contains messages associated with the managed extension for vesting contracts. + ManagedExtension { msg: ExecuteMsgManaged }, + /// Contains messages associated with the with_managers extension for vesting contracts. + WithManagersExtension { msg: ExecuteMsgWithManagers }, + /// Contains messages associated with the historical extension for vesting contracts. + HistoricalExtension { msg: ExecuteMsgHistorical }, +} +``` + +The `ManagedExtension`, `WithManagersExtension`, and `HistoricalExtension` messages are extensiom messages. Read about them in the [Extensions](#extensions) section. + +#### QueryMsg + +```rust +/// This structure describes the query messages available in a vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the configuration for the contract using a [`ConfigResponse`] object. + #[returns(ConfigResponse)] + Config {}, + /// Returns information about an address vesting tokens using a [`VestingAccountResponse`] object. + #[returns(VestingAccountResponse)] + VestingAccount { address: String }, + /// Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object. + #[returns(VestingAccountsResponse)] + VestingAccounts { + start_after: Option, + limit: Option, + order_by: Option, + }, + /// Returns the total unvested amount of tokens for a specific address. + #[returns(Uint128)] + AvailableAmount { address: String }, + /// Timestamp returns the current timestamp + #[returns(u64)] + Timestamp {}, + /// VestingState returns the current vesting state. + #[returns(VestingState)] + VestingState {}, + /// Contains messages associated with the managed extension for vesting contracts. + #[returns(QueryMsgManaged)] + ManagedExtension { msg: QueryMsgManaged }, + /// Contains messages associated with the with_managers extension for vesting contracts. + #[returns(QueryMsgWithManagers)] + WithManagersExtension { msg: QueryMsgWithManagers }, + /// Contains messages associated with the historical extension for vesting contracts. + #[returns(QueryMsgHistorical)] + HistoricalExtension { msg: QueryMsgHistorical }, +} +``` + +The `ManagedExtension`, `WithManagersExtension`, and `HistoricalExtension` messages are extensiom messages. Read about them in the [Extensions](#extensions) section. + +## Extensions + +Created contracts can be extended with a number of features. + +### Managed + +The `managed` extension allows the owner of the vesting contract to remove registered vesting accounts and redeem the corresponding funds. + +```rust +/// This structure describes the execute messages available in a managed vesting contract. +#[cw_serde] +pub enum ExecuteMsgManaged { + /// Removes vesting targets/accounts. + /// ## Executor + /// Only the current owner can execute this + RemoveVestingAccounts { + vesting_accounts: Vec, + /// Specifies the account that will receive the funds taken from the vesting accounts. + clawback_account: String, + }, +} + +/// This structure describes the query messages available in a managed vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgManaged {} +``` + +### WithManagers + +The `with_managers` extension allows the owner of the vesting contract to add/remove vesting managers — addresses that just like the owner are capable of registering new vesting accounts. + +```rust +/// This structure describes the execute messages available in a with_managers vesting contract. +#[cw_serde] +pub enum ExecuteMsgWithManagers { + /// Adds vesting managers + /// ## Executor + /// Only the current owner can execute this + AddVestingManagers { managers: Vec }, + /// Removes vesting managers + /// ## Executor + /// Only the current owner can execute this + RemoveVestingManagers { managers: Vec }, +} + +/// This structure describes the query messages available in a with_managers vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgWithManagers { + /// Returns list of vesting managers + /// (the persons who are able to add/remove vesting schedules) + #[returns(Vec)] + VestingManagers {}, +} +``` + +### Historical + +The `historical` allows to query vesting accounts and total vesting state based on a given height. + +```rust +/// This structure describes the execute messages available in a historical vesting contract. +#[cw_serde] +pub enum ExecuteMsgHistorical {} + +/// This structure describes the query messages available in a historical vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgHistorical { + /// Returns the total unclaimed amount of tokens for a specific address at certain height. + #[returns(Uint128)] + UnclaimedAmountAtHeight { address: String, height: u64 }, + /// Returns the total unclaimed amount of tokens for all the users at certain height. + #[returns(Uint128)] + UnclaimedTotalAmountAtHeight { height: u64 }, +} +``` + +### Extensions usage + +The following example adds all three extensions to the contract, but it's allowed to combine them in any way. +```rust +use vesting_base::builder::VestingBaseBuilder; +use astroport::asset::AssetInfo; +use cosmwasm_schema::cw_serde; + +/// This structure describes the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Address allowed to change contract parameters + pub owner: String, + /// [`AssetInfo`] of the token that's being vested + pub vesting_token: AssetInfo, + /// Initial list of whitelisted vesting managers + pub vesting_managers: Vec, +} + +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + ... + VestingBaseBuilder::default() + .historical() + .managed() + .with_managers(msg.vesting_managers) + .build(deps, msg.owner, msg.vesting_token)?; + ... +``` diff --git a/packages/vesting-base-lp/src/builder.rs b/packages/vesting-base-lp/src/builder.rs new file mode 100644 index 00000000..85957245 --- /dev/null +++ b/packages/vesting-base-lp/src/builder.rs @@ -0,0 +1,60 @@ +use crate::state::{CONFIG, VESTING_MANAGERS}; +use crate::types::{Config, Extensions}; +use cosmwasm_std::{DepsMut, StdResult}; + +/// A builder for vesting contracts with different extensions. +#[derive(Default)] +pub struct VestingBaseBuilder { + vesting_managers: Vec, + historical: bool, + managed: bool, + with_managers: bool, +} + +impl VestingBaseBuilder { + /// Appends the `managed` extension to the created vesting contract. + pub fn managed(&mut self) -> &mut VestingBaseBuilder { + self.managed = true; + self + } + + /// Appends the `with_managers` extension to the created vesting contract. + pub fn with_managers(&mut self, managers: Vec) -> &mut VestingBaseBuilder { + self.vesting_managers.extend(managers); + self.with_managers = true; + self + } + + /// Appends the `historical` extension to the created vesting contract. + pub fn historical(&mut self) -> &mut VestingBaseBuilder { + self.historical = true; + self + } + + /// Validates the inputs and initialises the created contract state. + pub fn build(&self, deps: DepsMut, owner: String, token_info_manager: String) -> StdResult<()> { + let owner = deps.api.addr_validate(&owner)?; + CONFIG.save( + deps.storage, + &Config { + owner, + vesting_token: None, + token_info_manager: deps.api.addr_validate(&token_info_manager)?, + extensions: Extensions { + historical: self.historical, + managed: self.managed, + with_managers: self.with_managers, + }, + }, + )?; + + if self.with_managers { + for m in self.vesting_managers.iter() { + let ma = deps.api.addr_validate(m)?; + VESTING_MANAGERS.save(deps.storage, ma, &())?; + } + }; + + Ok(()) + } +} diff --git a/packages/vesting-base-lp/src/error.rs b/packages/vesting-base-lp/src/error.rs new file mode 100644 index 00000000..c394825b --- /dev/null +++ b/packages/vesting-base-lp/src/error.rs @@ -0,0 +1,65 @@ +use cosmwasm_std::{Decimal, OverflowError, StdError}; +use cw_utils::PaymentError; +use thiserror::Error; + +/// This enum describes generator vesting contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Amount is not available!")] + AmountIsNotAvailable {}, + + #[error("Vesting schedule error on addr: {0}. Should satisfy: (start < end and at_start < total) or (start = end and at_start = total)")] + VestingScheduleError(String), + + #[error("Vesting schedule amount error. The total amount should be equal to the CW20 receive amount.")] + VestingScheduleAmountError {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Vesting token is not set!")] + VestingTokenIsNotSet {}, + + #[error("Contract is in migration state. Please wait for migration to complete.")] + MigrationIncomplete {}, + + #[error( + "Provided slippage tolerance {slippage_tolerance} is more than the max allowed {max_slippage_tolerance}" + )] + MigrationSlippageToBig { + slippage_tolerance: Decimal, + max_slippage_tolerance: Decimal, + }, + + #[error("Migration is complete")] + MigrationComplete {}, +} + +#[allow(clippy::from_over_into)] +impl Into for ContractError { + fn into(self) -> StdError { + StdError::generic_err(self.to_string()) + } +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} + +pub fn ext_unsupported_err(extension: impl Into + std::fmt::Display) -> StdError { + StdError::generic_err(format!( + "Extension is not enabled for the contract: {}.", + extension + )) +} diff --git a/packages/vesting-base-lp/src/ext_historical.rs b/packages/vesting-base-lp/src/ext_historical.rs new file mode 100644 index 00000000..d494adf6 --- /dev/null +++ b/packages/vesting-base-lp/src/ext_historical.rs @@ -0,0 +1,101 @@ +use crate::error::{ext_unsupported_err, ContractError}; +use crate::msg::{ExecuteMsgHistorical, QueryMsgHistorical}; +use crate::state::{vesting_info, vesting_state, CONFIG}; +use crate::types::VestingInfo; +use cosmwasm_std::{ + to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, +}; + +/// Contains the historical extension check and routing of the message. +pub(crate) fn handle_execute_historical_msg( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsgHistorical, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.historical { + return Err(ext_unsupported_err("historical").into()); + } + + // empty handler kept for uniformity with other extensions + unimplemented!() +} + +/// Contains the historical extension check and routing of the message. +pub(crate) fn handle_query_historical_msg( + deps: Deps, + _env: Env, + msg: QueryMsgHistorical, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.historical { + return Err(ext_unsupported_err("historical")); + } + + match msg { + QueryMsgHistorical::UnclaimedAmountAtHeight { address, height } => { + to_binary(&query_unclaimed_amount_at_height(deps, address, height)?) + } + QueryMsgHistorical::UnclaimedTotalAmountAtHeight { height } => { + to_binary(&query_total_unclaimed_amount_at_height(deps, height)?) + } + } +} + +/// Returns the available amount of distributed and yet to be claimed tokens for a specific vesting recipient at certain height. +/// +/// * **address** vesting recipient for which to return the available amount of tokens to claim. +/// +/// * **height** the height we querying unclaimed amount for +fn query_unclaimed_amount_at_height( + deps: Deps, + address: String, + height: u64, +) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + let config = CONFIG.load(deps.storage)?; + let maybe_info = vesting_info(config.extensions.historical).may_load_at_height( + deps.storage, + address, + height, + )?; + match &maybe_info { + Some(info) => compute_unclaimed_amount(info), + None => Ok(Uint128::zero()), + } +} + +/// Returns the available amount of distributed and yet to be claimed tokens for all the recipients at certain height. +/// +/// * **height** the height we querying unclaimed amount for +fn query_total_unclaimed_amount_at_height(deps: Deps, height: u64) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let maybe_state = + vesting_state(config.extensions.historical).may_load_at_height(deps.storage, height)?; + match &maybe_state { + Some(info) => Ok(info.total_granted.checked_sub(info.total_released)?), + None => Ok(Uint128::zero()), + } +} + +/// Computes the amount of distributed and yet unclaimed tokens for a specific vesting recipient at certain height. +/// Returns the computed amount if the operation is successful. +/// +/// * **vesting_info** vesting schedules for which to compute the amount of tokens +/// that are vested and can be claimed by the recipient. +fn compute_unclaimed_amount(vesting_info: &VestingInfo) -> StdResult { + let mut available_amount: Uint128 = Uint128::zero(); + for sch in &vesting_info.schedules { + if let Some(end_point) = &sch.end_point { + available_amount = available_amount.checked_add(end_point.amount)?; + } else { + available_amount = available_amount.checked_add(sch.start_point.amount)?; + } + } + + available_amount + .checked_sub(vesting_info.released_amount) + .map_err(StdError::from) +} diff --git a/packages/vesting-base-lp/src/ext_managed.rs b/packages/vesting-base-lp/src/ext_managed.rs new file mode 100644 index 00000000..b11cc1b0 --- /dev/null +++ b/packages/vesting-base-lp/src/ext_managed.rs @@ -0,0 +1,121 @@ +use crate::error::{ext_unsupported_err, ContractError}; +use crate::handlers::get_vesting_token; +use crate::msg::{ExecuteMsgManaged, QueryMsgManaged}; +use crate::state::{vesting_info, vesting_state, CONFIG}; +use astroport::asset::AssetInfoExt; +use cosmwasm_std::{ + attr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, Uint128, +}; + +/// Contains the managed extension check and routing of the message. +pub(crate) fn handle_execute_managed_msg( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsgManaged, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.managed { + return Err(ext_unsupported_err("managed").into()); + } + + match msg { + ExecuteMsgManaged::RemoveVestingAccounts { + vesting_accounts, + clawback_account, + } => remove_vesting_accounts(deps, env, info, vesting_accounts, clawback_account), + } +} + +/// Contains the managed extension check and routing of the message. +pub(crate) fn handle_query_managed_msg( + deps: Deps, + _env: Env, + _msg: QueryMsgManaged, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.managed { + return Err(ext_unsupported_err("managed")); + } + + // empty handler kept for uniformity with other extensions + unimplemented!() +} + +#[allow(clippy::too_many_arguments)] +fn remove_vesting_accounts( + deps: DepsMut, + env: Env, + info: MessageInfo, + vesting_accounts: Vec, + clawback_account: String, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + let vesting_token = get_vesting_token(&config)?; + + let mut response = Response::new(); + + let clawback_address = deps.api.addr_validate(&clawback_account)?; + + // For each vesting account, calculate the amount of tokens to claw back (unclaimed + still + // vesting), transfer the required amount to the owner, remove the vesting information + // from the storage, and decrease the total granted metric. + for vesting_account in vesting_accounts { + let account_address = deps.api.addr_validate(&vesting_account)?; + + let config = CONFIG.load(deps.storage)?; + let vesting_info = vesting_info(config.extensions.historical); + if let Some(account_info) = vesting_info.may_load(deps.storage, account_address.clone())? { + let mut total_granted_for_user = Uint128::zero(); + for sch in account_info.schedules { + if let Some(end_point) = sch.end_point { + total_granted_for_user = + total_granted_for_user.checked_add(end_point.amount)?; + } else { + total_granted_for_user = + total_granted_for_user.checked_add(sch.start_point.amount)?; + } + } + + let amount_to_claw_back = + total_granted_for_user.checked_sub(account_info.released_amount)?; + + let transfer_msg = vesting_token + .with_balance(amount_to_claw_back) + .into_msg(clawback_address.clone())?; + response = response.add_submessage(SubMsg::new(transfer_msg)); + + vesting_state(config.extensions.historical).update::<_, ContractError>( + deps.storage, + env.block.height, + |s| { + // Here we choose the "forget about everything" strategy. E.g., if we granted a user + // 300 tokens, and they claimed 150 tokens, the vesting state is + // { total_granted: 300, total_released: 150 }. + // If after that we remove the user's vesting account, we set the vesting state to + // { total_granted: 0, total_released: 0 }. + // + // If we decided to set it to { total_granted: 150, total_released: 150 }., the + // .total_released value of the vesting state would not be equal to the sum of the + // .released_amount values of all registered accounts. + let mut state = s.ok_or(ContractError::AmountIsNotAvailable {})?; + state.total_granted = + state.total_granted.checked_sub(total_granted_for_user)?; + state.total_released = state + .total_released + .checked_sub(account_info.released_amount)?; + Ok(state) + }, + )?; + vesting_info.remove(deps.storage, account_address, env.block.height)?; + } + } + + Ok(response.add_attributes(vec![ + attr("action", "remove_vesting_accounts"), + attr("sender", &info.sender), + ])) +} diff --git a/packages/vesting-base-lp/src/ext_with_managers.rs b/packages/vesting-base-lp/src/ext_with_managers.rs new file mode 100644 index 00000000..56573188 --- /dev/null +++ b/packages/vesting-base-lp/src/ext_with_managers.rs @@ -0,0 +1,105 @@ +use crate::error::{ext_unsupported_err, ContractError}; +use crate::msg::{ExecuteMsgWithManagers, QueryMsgWithManagers}; +use crate::state::{CONFIG, VESTING_MANAGERS}; +use cosmwasm_std::{ + attr, to_binary, Addr, Attribute, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, + StdError, StdResult, +}; + +/// Contains the with_managers extension check and routing of the message. +pub(crate) fn handle_execute_with_managers_msg( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsgWithManagers, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.with_managers { + return Err(ext_unsupported_err("with_managers").into()); + } + + match msg { + ExecuteMsgWithManagers::AddVestingManagers { managers } => { + add_vesting_managers(deps, env, info, managers) + } + ExecuteMsgWithManagers::RemoveVestingManagers { managers } => { + remove_vesting_managers(deps, env, info, managers) + } + } +} + +/// Contains the with_managers extension check and routing of the message. +pub(crate) fn handle_query_managers_msg( + deps: Deps, + _env: Env, + msg: QueryMsgWithManagers, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.with_managers { + return Err(ext_unsupported_err("with_managers")); + } + + match msg { + QueryMsgWithManagers::VestingManagers {} => to_binary(&query_vesting_managers(deps)?), + } +} + +/// Adds new vesting managers, which have a permission to add/remove vesting schedule +/// +/// * **managers** list of accounts to be added to the whitelist. +fn add_vesting_managers( + deps: DepsMut, + _env: Env, + info: MessageInfo, + managers: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + let mut attrs: Vec = vec![]; + for m in managers { + let ma = deps.api.addr_validate(&m)?; + if !VESTING_MANAGERS.has(deps.storage, ma.clone()) { + VESTING_MANAGERS.save(deps.storage, ma, &())?; + attrs.push(attr("vesting_manager", &m)) + } + } + Ok(Response::new() + .add_attribute("action", "add_vesting_managers") + .add_attributes(attrs)) +} + +/// Removes new vesting managers from the whitelist +/// +/// * **managers** list of accounts to be removed from the whitelist. +fn remove_vesting_managers( + deps: DepsMut, + _env: Env, + info: MessageInfo, + managers: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + let mut attrs: Vec = vec![]; + for m in managers { + let ma = deps.api.addr_validate(&m)?; + if VESTING_MANAGERS.has(deps.storage, ma.clone()) { + VESTING_MANAGERS.remove(deps.storage, ma); + attrs.push(attr("vesting_manager", &m)) + } + } + Ok(Response::new() + .add_attribute("action", "remove_vesting_managers") + .add_attributes(attrs)) +} + +/// Returns a list of vesting schedules using a [`VestingAccountsResponse`] object. +fn query_vesting_managers(deps: Deps) -> StdResult> { + let managers = VESTING_MANAGERS + .keys(deps.storage, None, None, Order::Ascending) + .collect::, StdError>>()?; + Ok(managers) +} diff --git a/packages/vesting-base-lp/src/handlers.rs b/packages/vesting-base-lp/src/handlers.rs new file mode 100644 index 00000000..f798e2e8 --- /dev/null +++ b/packages/vesting-base-lp/src/handlers.rs @@ -0,0 +1,820 @@ +use crate::error::ContractError; +use crate::ext_historical::{handle_execute_historical_msg, handle_query_historical_msg}; +use crate::ext_managed::{handle_execute_managed_msg, handle_query_managed_msg}; +use crate::ext_with_managers::{handle_execute_with_managers_msg, handle_query_managers_msg}; +use crate::msg::{CallbackMsg, Cw20HookMsg, ExecuteMsg, MigrateMsg, QueryMsg}; +use crate::state::{ + read_vesting_infos, vesting_info, vesting_state, MIGRATION_STATUS, XYK_TO_CL_MIGRATION_CONFIG, +}; +use crate::state::{CONFIG, OWNERSHIP_PROPOSAL, VESTING_MANAGERS}; +use crate::types::{ + Config, MigrationState, OrderBy, VestingAccount, VestingAccountResponse, + VestingAccountsResponse, VestingInfo, VestingSchedule, VestingSchedulePoint, VestingState, + XykToClMigrationConfig, +}; +use astroport::asset::{ + addr_opt_validate, native_asset, token_asset_info, AssetInfo, AssetInfoExt, PairInfo, +}; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use astroport::pair::{ + Cw20HookMsg as PairCw20HookMsg, ExecuteMsg as PairExecuteMsg, QueryMsg as PairQueryMsg, +}; +use cosmwasm_std::{ + attr, from_binary, to_binary, Addr, Binary, Coin, CosmosMsg, Decimal, Deps, DepsMut, Env, + MessageInfo, Response, StdError, StdResult, Storage, SubMsg, Uint128, WasmMsg, +}; +use cw20::{BalanceResponse, Cw20ExecuteMsg, Cw20QueryMsg, Cw20ReceiveMsg}; +use cw_utils::must_pay; + +/// Exposes execute functions available in the contract. +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let migration_state: MigrationState = MIGRATION_STATUS.load(deps.storage)?; + if migration_state != MigrationState::Completed { + match msg { + ExecuteMsg::MigrateLiquidity {} => {} + ExecuteMsg::Callback(..) => {} + _ => return Err(ContractError::MigrationIncomplete {}), + } + } + match msg { + ExecuteMsg::Claim { recipient, amount } => claim(deps, env, info, recipient, amount), + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::RegisterVestingAccounts { vesting_accounts } => { + let config = CONFIG.load(deps.storage)?; + let vesting_token = get_vesting_token(&config)?; + + match &vesting_token { + AssetInfo::NativeToken { denom } + if is_sender_whitelisted(deps.storage, &config, &info.sender) => + { + let amount = must_pay(&info, denom)?; + register_vesting_accounts(deps, vesting_accounts, amount, env.block.height) + } + _ => Err(ContractError::Unauthorized {}), + } + } + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config: Config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config: Config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG.update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + })?; + + Ok(()) + }) + .map_err(Into::into) + } + ExecuteMsg::SetVestingToken { vesting_token } => { + set_vesting_token(deps, env, info, vesting_token) + } + ExecuteMsg::ManagedExtension { msg } => handle_execute_managed_msg(deps, env, info, msg), + ExecuteMsg::WithManagersExtension { msg } => { + handle_execute_with_managers_msg(deps, env, info, msg) + } + ExecuteMsg::HistoricalExtension { msg } => { + handle_execute_historical_msg(deps, env, info, msg) + } + ExecuteMsg::MigrateLiquidity {} => execute_migrate_liquidity(deps, env, None), + ExecuteMsg::Callback(msg) => _handle_callback(deps, env, info, msg), + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. +/// +/// * **cw20_msg** CW20 message to process. +fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + let vesting_token = get_vesting_token(&config)?; + + // Permission check + if !is_sender_whitelisted( + deps.storage, + &config, + &deps.api.addr_validate(&cw20_msg.sender)?, + ) || token_asset_info(info.sender) != vesting_token + { + return Err(ContractError::Unauthorized {}); + } + + match from_binary(&cw20_msg.msg)? { + Cw20HookMsg::RegisterVestingAccounts { vesting_accounts } => { + register_vesting_accounts(deps, vesting_accounts, cw20_msg.amount, env.block.height) + } + } +} + +/// Create new vesting schedules. +/// +/// * **vesting_accounts** list of accounts and associated vesting schedules to create. +/// +/// * **cw20_amount** sets the amount that confirms the total amount of all accounts to register. +fn register_vesting_accounts( + deps: DepsMut, + vesting_accounts: Vec, + amount: Uint128, + height: u64, +) -> Result { + let response = Response::new(); + let config = CONFIG.load(deps.storage)?; + let mut to_deposit = Uint128::zero(); + + for mut vesting_account in vesting_accounts { + let mut released_amount = Uint128::zero(); + let account_address = deps.api.addr_validate(&vesting_account.address)?; + + assert_vesting_schedules(&account_address, &vesting_account.schedules)?; + + for sch in &vesting_account.schedules { + let amount = if let Some(end_point) = &sch.end_point { + end_point.amount + } else { + sch.start_point.amount + }; + to_deposit = to_deposit.checked_add(amount)?; + } + + let vesting_info = vesting_info(config.extensions.historical); + if let Some(mut old_info) = vesting_info.may_load(deps.storage, account_address.clone())? { + released_amount = old_info.released_amount; + vesting_account.schedules.append(&mut old_info.schedules); + } + + vesting_info.save( + deps.storage, + account_address, + &VestingInfo { + schedules: vesting_account.schedules, + released_amount, + }, + height, + )?; + } + + if to_deposit != amount { + return Err(ContractError::VestingScheduleAmountError {}); + } + + vesting_state(config.extensions.historical).update::<_, ContractError>( + deps.storage, + height, + |s| { + let mut state = s.unwrap_or_default(); + state.total_granted = state.total_granted.checked_add(to_deposit)?; + Ok(state) + }, + )?; + + Ok(response.add_attributes({ + vec![ + attr("action", "register_vesting_accounts"), + attr("deposited", to_deposit), + ] + })) +} + +/// Claims vested tokens and transfers them to the vesting recipient. +/// +/// * **recipient** vesting recipient for which to claim tokens. +/// +/// * **amount** amount of vested tokens to claim. +fn claim( + deps: DepsMut, + env: Env, + info: MessageInfo, + recipient: Option, + amount: Option, +) -> Result { + let config = CONFIG.load(deps.storage)?; + let vesting_token = get_vesting_token(&config)?; + let vesting_info = vesting_info(config.extensions.historical); + let mut sender_vesting_info = vesting_info.load(deps.storage, info.sender.clone())?; + + let available_amount = + compute_available_amount(env.block.time.seconds(), &sender_vesting_info)?; + + let claim_amount = if let Some(a) = amount { + if a > available_amount { + return Err(ContractError::AmountIsNotAvailable {}); + }; + a + } else { + available_amount + }; + + let mut response = Response::new(); + + if !claim_amount.is_zero() { + let transfer_msg = vesting_token + .with_balance(claim_amount) + .into_msg(recipient.unwrap_or_else(|| info.sender.to_string()))?; + response = response.add_submessage(SubMsg::new(transfer_msg)); + + sender_vesting_info.released_amount = sender_vesting_info + .released_amount + .checked_add(claim_amount)?; + vesting_info.save( + deps.storage, + info.sender.clone(), + &sender_vesting_info, + env.block.height, + )?; + vesting_state(config.extensions.historical).update::<_, ContractError>( + deps.storage, + env.block.height, + |s| { + let mut state = s.ok_or(ContractError::AmountIsNotAvailable {})?; + state.total_released = state.total_released.checked_add(claim_amount)?; + Ok(state) + }, + )?; + }; + + Ok(response.add_attributes(vec![ + attr("action", "claim"), + attr("address", &info.sender), + attr("available_amount", available_amount), + attr("claimed_amount", claim_amount), + ])) +} + +pub(crate) fn set_vesting_token( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token: AssetInfo, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + if info.sender != config.owner && info.sender != config.token_info_manager { + return Err(ContractError::Unauthorized {}); + } + token.check(deps.api)?; + config.vesting_token = Some(token); + + CONFIG.save(deps.storage, &config)?; + Ok(Response::new()) +} + +pub(crate) fn get_vesting_token(config: &Config) -> Result { + config + .vesting_token + .clone() + .ok_or(ContractError::VestingTokenIsNotSet {}) +} + +fn execute_migrate_liquidity( + deps: DepsMut, + env: Env, + slippage_tolerance: Option, +) -> Result { + let migration_state: MigrationState = MIGRATION_STATUS.load(deps.storage)?; + if migration_state == MigrationState::Completed { + return Err(ContractError::MigrationComplete {}); + } + let migration_config: XykToClMigrationConfig = XYK_TO_CL_MIGRATION_CONFIG.load(deps.storage)?; + + let vesting_infos = read_vesting_infos( + deps.as_ref(), + migration_config.last_processed_user, + Some(migration_config.batch_size), + None, + )?; + + let vesting_accounts: Vec<_> = vesting_infos + .into_iter() + .map(|(address, info)| VestingAccountResponse { address, info }) + .collect(); + + if vesting_accounts.is_empty() { + MIGRATION_STATUS.save(deps.storage, &MigrationState::Completed)?; + } + let mut resp = Response::default(); + + // get pairs LP token addresses + let pair_info: PairInfo = deps + .querier + .query_wasm_smart(migration_config.xyk_pair.clone(), &PairQueryMsg::Pair {})?; + + // query max available amounts to be withdrawn from pool + let max_available_amount = { + let resp: BalanceResponse = deps.querier.query_wasm_smart( + pair_info.liquidity_token.clone(), + &Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + resp.balance + }; + + if max_available_amount.is_zero() { + return Ok(resp); + } + + for user in vesting_accounts.into_iter() { + let user_amount = compute_share(&user.info)?; + + if let Some(slippage_tolerance) = slippage_tolerance { + if slippage_tolerance.gt(&migration_config.max_slippage) { + return Err(ContractError::MigrationSlippageToBig { + slippage_tolerance, + max_slippage_tolerance: migration_config.max_slippage, + }); + } + } + + let slippage_tolerance = slippage_tolerance.unwrap_or(migration_config.max_slippage); + + resp = resp.add_message( + CallbackMsg::MigrateLiquidityToClPair { + xyk_pair: migration_config.xyk_pair.clone(), + xyk_lp_token: pair_info.liquidity_token.clone(), + amount: user_amount, + slippage_tolerance, + cl_pair: migration_config.cl_pair.clone(), + ntrn_denom: migration_config.ntrn_denom.clone(), + paired_asset_denom: migration_config.paired_denom.clone(), + user, + } + .to_cosmos_msg(&env)?, + ); + } + + Ok(resp) +} + +fn _handle_callback( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: CallbackMsg, +) -> Result { + // Only the contract itself can call callbacks + if info.sender != env.contract.address { + return Err(ContractError::Unauthorized {}); + } + match msg { + CallbackMsg::MigrateLiquidityToClPair { + xyk_pair, + xyk_lp_token, + amount, + slippage_tolerance, + cl_pair, + ntrn_denom, + paired_asset_denom, + user, + } => migrate_liquidity_to_cl_pair_callback( + deps, + env, + xyk_pair, + xyk_lp_token, + amount, + slippage_tolerance, + cl_pair, + ntrn_denom, + paired_asset_denom, + user, + ), + CallbackMsg::ProvideLiquidityToClPairAfterWithdrawal { + ntrn_denom, + ntrn_init_balance, + paired_asset_denom, + paired_asset_init_balance, + cl_pair, + slippage_tolerance, + user, + } => provide_liquidity_to_cl_pair_after_withdrawal_callback( + deps, + env, + ntrn_denom, + ntrn_init_balance, + paired_asset_denom, + paired_asset_init_balance, + cl_pair, + slippage_tolerance, + user, + ), + CallbackMsg::PostMigrationVestingReschedule { user } => { + post_migration_vesting_reschedule_callback(deps, env, &user) + } + } +} + +#[allow(clippy::too_many_arguments)] +fn migrate_liquidity_to_cl_pair_callback( + deps: DepsMut, + env: Env, + xyk_pair: Addr, + xyk_lp_token: Addr, + amount: Uint128, + slippage_tolerance: Decimal, + cl_pair: Addr, + ntrn_denom: String, + paired_asset_denom: String, + user: VestingAccountResponse, +) -> Result { + let ntrn_init_balance = deps + .querier + .query_balance(env.contract.address.to_string(), ntrn_denom.clone())? + .amount; + let paired_asset_init_balance = deps + .querier + .query_balance(env.contract.address.to_string(), paired_asset_denom.clone())? + .amount; + + let mut msgs: Vec = vec![]; + + // push message to withdraw liquidity from the xyk pair + if !amount.is_zero() { + msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: xyk_lp_token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Send { + contract: xyk_pair.to_string(), + amount, + msg: to_binary(&PairCw20HookMsg::WithdrawLiquidity { assets: vec![] })?, + })?, + funds: vec![], + })) + } + // push the next migration step as a callback message + msgs.push( + CallbackMsg::ProvideLiquidityToClPairAfterWithdrawal { + ntrn_denom, + ntrn_init_balance, + paired_asset_denom, + paired_asset_init_balance, + cl_pair, + slippage_tolerance, + user, + } + .to_cosmos_msg(&env)?, + ); + + Ok(Response::default().add_messages(msgs)) +} + +#[allow(clippy::too_many_arguments)] +fn provide_liquidity_to_cl_pair_after_withdrawal_callback( + deps: DepsMut, + env: Env, + ntrn_denom: String, + ntrn_init_balance: Uint128, + paired_asset_denom: String, + paired_asset_init_balance: Uint128, + cl_pair_address: Addr, + slippage_tolerance: Decimal, + user: VestingAccountResponse, +) -> Result { + let ntrn_balance_after_withdrawal = deps + .querier + .query_balance(env.contract.address.to_string(), ntrn_denom.clone())? + .amount; + let paired_asset_balance_after_withdrawal = deps + .querier + .query_balance(env.contract.address.to_string(), paired_asset_denom.clone())? + .amount; + + // calc amount of assets that's been withdrawn + let withdrawn_ntrn_amount = ntrn_balance_after_withdrawal.checked_sub(ntrn_init_balance)?; + let withdrawn_paired_asset_amount = + paired_asset_balance_after_withdrawal.checked_sub(paired_asset_init_balance)?; + + let mut msgs: Vec = vec![]; + + if !withdrawn_ntrn_amount.is_zero() && !withdrawn_paired_asset_amount.is_zero() { + msgs.push(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cl_pair_address.to_string(), + msg: to_binary(&PairExecuteMsg::ProvideLiquidity { + assets: vec![ + native_asset(ntrn_denom.clone(), withdrawn_ntrn_amount), + native_asset(paired_asset_denom.clone(), withdrawn_paired_asset_amount), + ], + slippage_tolerance: Some(slippage_tolerance), + auto_stake: None, + receiver: None, + })?, + funds: vec![ + Coin::new(withdrawn_ntrn_amount.into(), ntrn_denom), + Coin::new(withdrawn_paired_asset_amount.into(), paired_asset_denom), + ], + })) + } + + msgs.push(CallbackMsg::PostMigrationVestingReschedule { user }.to_cosmos_msg(&env)?); + + Ok(Response::default().add_messages(msgs)) +} + +fn post_migration_vesting_reschedule_callback( + deps: DepsMut, + env: Env, + user: &VestingAccountResponse, +) -> Result { + let config = CONFIG.load(deps.storage)?; + let mut migration_config: XykToClMigrationConfig = + XYK_TO_CL_MIGRATION_CONFIG.load(deps.storage)?; + let balance_response: BalanceResponse = deps.querier.query_wasm_smart( + &migration_config.new_lp_token, + &Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + let state = vesting_state(config.extensions.historical).load(deps.storage)?; + let current_balance = balance_response.balance; + + let balance_diff: Uint128 = if !current_balance.is_zero() { + current_balance.checked_sub(state.total_granted)? + } else { + Uint128::zero() + }; + + let schedule = user.info.schedules.last().unwrap(); + + let new_end_point; + if let Some(end_point) = &schedule.end_point { + new_end_point = Option::from(VestingSchedulePoint { + time: end_point.time, + amount: balance_diff, + }) + } else { + new_end_point = None + } + + let new_schedule = VestingSchedule { + start_point: VestingSchedulePoint { + time: schedule.start_point.time, + amount: Uint128::zero(), + }, + end_point: new_end_point, + }; + + let vesting_info = vesting_info(config.extensions.historical); + + vesting_info.save( + deps.storage, + user.address.clone(), + &VestingInfo { + schedules: vec![new_schedule], + released_amount: Uint128::zero(), + }, + env.block.height, + )?; + + vesting_state(config.extensions.historical).update::<_, ContractError>( + deps.storage, + env.block.height, + |s| { + let mut state = s.unwrap_or_default(); + state.total_granted = state.total_granted.checked_add(balance_diff)?; + Ok(state) + }, + )?; + + migration_config.last_processed_user = Some(user.address.clone()); + XYK_TO_CL_MIGRATION_CONFIG.save(deps.storage, &migration_config)?; + + Ok(Response::default()) +} + +/// Exposes all the queries available in the contract. +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + let migration_state: MigrationState = MIGRATION_STATUS.load(deps.storage)?; + if migration_state != MigrationState::Completed { + return Err(ContractError::MigrationIncomplete {}.into()); + } + + match msg { + QueryMsg::Config {} => Ok(to_binary(&query_config(deps)?)?), + QueryMsg::VestingAccount { address } => { + Ok(to_binary(&query_vesting_account(deps, address)?)?) + } + QueryMsg::VestingAccounts { + start_after, + limit, + order_by, + } => Ok(to_binary(&query_vesting_accounts( + deps, + start_after, + limit, + order_by, + )?)?), + QueryMsg::AvailableAmount { address } => Ok(to_binary(&query_vesting_available_amount( + deps, env, address, + )?)?), + QueryMsg::VestingState {} => Ok(to_binary(&query_vesting_state(deps)?)?), + QueryMsg::Timestamp {} => Ok(to_binary(&query_timestamp(env)?)?), + QueryMsg::ManagedExtension { msg } => handle_query_managed_msg(deps, env, msg), + QueryMsg::WithManagersExtension { msg } => handle_query_managers_msg(deps, env, msg), + QueryMsg::HistoricalExtension { msg } => handle_query_historical_msg(deps, env, msg), + } +} + +/// Returns the vesting contract configuration using a [`Config`] object. +fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(config) +} + +/// Returns the accumulated vesting information for all addresses using a [`VestingState`] object. +fn query_vesting_state(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let state = vesting_state(config.extensions.historical).load(deps.storage)?; + + Ok(state) +} + +/// Return the current block timestamp (in seconds) +/// * **env** is an object of type [`Env`]. +fn query_timestamp(env: Env) -> StdResult { + Ok(env.block.time.seconds()) +} + +/// Returns the vesting data for a specific vesting recipient using a [`VestingAccountResponse`] object. +/// +/// * **address** vesting recipient for which to return vesting data. +fn query_vesting_account(deps: Deps, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + let config = CONFIG.load(deps.storage)?; + let info = vesting_info(config.extensions.historical).load(deps.storage, address.clone())?; + + Ok(VestingAccountResponse { address, info }) +} + +/// Returns a list of vesting schedules using a [`VestingAccountsResponse`] object. +/// +/// * **start_after** index from which to start reading vesting schedules. +/// +/// * **limit** amount of vesting schedules to return. +/// +/// * **order_by** whether results should be returned in an ascending or descending order. +fn query_vesting_accounts( + deps: Deps, + start_after: Option, + limit: Option, + order_by: Option, +) -> StdResult { + let start_after = addr_opt_validate(deps.api, &start_after)?; + + let vesting_infos = read_vesting_infos(deps, start_after, limit, order_by)?; + + let vesting_accounts: Vec<_> = vesting_infos + .into_iter() + .map(|(address, info)| VestingAccountResponse { address, info }) + .collect(); + + Ok(VestingAccountsResponse { vesting_accounts }) +} + +/// Returns the available amount of vested and yet to be claimed tokens for a specific vesting recipient. +/// +/// * **address** vesting recipient for which to return the available amount of tokens to claim. +fn query_vesting_available_amount(deps: Deps, env: Env, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + let config = CONFIG.load(deps.storage)?; + let info = vesting_info(config.extensions.historical).load(deps.storage, address)?; + let available_amount = compute_available_amount(env.block.time.seconds(), &info)?; + Ok(available_amount) +} + +/// Manages contract migration. +pub fn migrate(deps: DepsMut, env: Env, msg: MigrateMsg) -> Result { + let mut config = CONFIG.load(deps.storage)?; + XYK_TO_CL_MIGRATION_CONFIG.save( + deps.storage, + &XykToClMigrationConfig { + max_slippage: msg.max_slippage, + ntrn_denom: msg.ntrn_denom, + xyk_pair: deps.api.addr_validate(msg.xyk_pair.as_str())?, + paired_denom: msg.paired_denom, + cl_pair: deps.api.addr_validate(msg.cl_pair.as_str())?, + batch_size: msg.batch_size, + last_processed_user: None, + new_lp_token: deps.api.addr_validate(msg.new_lp_token.as_str())?, + }, + )?; + config.vesting_token = Some(AssetInfo::Token { + contract_addr: deps.api.addr_validate(msg.new_lp_token.as_str())?, + }); + + CONFIG.save(deps.storage, &config)?; + + vesting_state(config.extensions.historical).update::<_, ContractError>( + deps.storage, + env.block.height, + |s| { + let mut state = s.unwrap_or_default(); + state.total_granted = Uint128::zero(); + Ok(state) + }, + )?; + + MIGRATION_STATUS.save(deps.storage, &MigrationState::Started)?; + + Ok(Response::default()) +} + +fn is_sender_whitelisted(store: &mut dyn Storage, config: &Config, sender: &Addr) -> bool { + if *sender == config.owner { + return true; + } + if VESTING_MANAGERS.has(store, sender.clone()) { + return true; + } + false +} + +/// Asserts the validity of a list of vesting schedules. +/// +/// * **addr** receiver of the vested tokens. +/// +/// * **vesting_schedules** vesting schedules to validate. +fn assert_vesting_schedules( + addr: &Addr, + vesting_schedules: &[VestingSchedule], +) -> Result<(), ContractError> { + for sch in vesting_schedules { + if let Some(end_point) = &sch.end_point { + if !(sch.start_point.time < end_point.time && sch.start_point.amount < end_point.amount) + { + return Err(ContractError::VestingScheduleError(addr.to_string())); + } + } + } + + Ok(()) +} + +/// Computes the amount of vested and yet unclaimed tokens for a specific vesting recipient. +/// Returns the computed amount if the operation is successful. +/// +/// * **current_time** timestamp from which to start querying for vesting schedules. +/// Schedules that started later than current_time will be omitted. +/// +/// * **vesting_info** vesting schedules for which to compute the amount of tokens +/// that are vested and can be claimed by the recipient. +fn compute_available_amount(current_time: u64, vesting_info: &VestingInfo) -> StdResult { + let mut available_amount: Uint128 = Uint128::zero(); + for sch in &vesting_info.schedules { + if sch.start_point.time > current_time { + continue; + } + + available_amount = available_amount.checked_add(sch.start_point.amount)?; + + if let Some(end_point) = &sch.end_point { + let passed_time = current_time.min(end_point.time) - sch.start_point.time; + let time_period = end_point.time - sch.start_point.time; + if passed_time != 0 && time_period != 0 { + let release_amount = Uint128::from(passed_time).multiply_ratio( + end_point.amount.checked_sub(sch.start_point.amount)?, + time_period, + ); + available_amount = available_amount.checked_add(release_amount)?; + } + } + } + + available_amount + .checked_sub(vesting_info.released_amount) + .map_err(StdError::from) +} + +fn compute_share(vesting_info: &VestingInfo) -> StdResult { + let mut available_amount: Uint128 = Uint128::zero(); + for sch in &vesting_info.schedules { + if let Some(end_point) = &sch.end_point { + available_amount = available_amount.checked_add(end_point.amount)? + } + } + + Ok(available_amount.checked_sub(vesting_info.released_amount)?) +} diff --git a/packages/vesting-base-lp/src/lib.rs b/packages/vesting-base-lp/src/lib.rs new file mode 100644 index 00000000..594d5e36 --- /dev/null +++ b/packages/vesting-base-lp/src/lib.rs @@ -0,0 +1,10 @@ +pub mod builder; +pub mod error; +pub mod handlers; +pub mod msg; +pub mod state; +pub mod types; + +pub(crate) mod ext_historical; +pub(crate) mod ext_managed; +pub(crate) mod ext_with_managers; diff --git a/packages/vesting-base-lp/src/msg.rs b/packages/vesting-base-lp/src/msg.rs new file mode 100644 index 00000000..b9824f69 --- /dev/null +++ b/packages/vesting-base-lp/src/msg.rs @@ -0,0 +1,209 @@ +use crate::types::{ + Config, OrderBy, VestingAccount, VestingAccountResponse, VestingAccountsResponse, VestingState, +}; +use astroport::asset::AssetInfo; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{to_binary, Addr, Binary, CosmosMsg, Decimal, Env, StdResult, Uint128, WasmMsg}; +use cw20::Cw20ReceiveMsg; + +/// This structure describes the execute messages available in a vesting contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Claim claims vested tokens and sends them to a recipient + Claim { + /// The address that receives the vested tokens + recipient: Option, + /// The amount of tokens to claim + amount: Option, + }, + /// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template + Receive(Cw20ReceiveMsg), + /// RegisterVestingAccounts registers vesting targets/accounts + RegisterVestingAccounts { + vesting_accounts: Vec, + }, + /// Creates a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the offer to change the owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + DropOwnershipProposal {}, + /// Claims contract ownership + /// ## Executor + /// Only the newly proposed owner can execute this + ClaimOwnership {}, + /// Sets vesting token + /// ## Executor + /// Only the current owner or token info manager can execute this + SetVestingToken { vesting_token: AssetInfo }, + /// Contains messages associated with the managed extension for vesting contracts. + ManagedExtension { msg: ExecuteMsgManaged }, + /// Contains messages associated with the with_managers extension for vesting contracts. + WithManagersExtension { msg: ExecuteMsgWithManagers }, + /// Contains messages associated with the historical extension for vesting contracts. + HistoricalExtension { msg: ExecuteMsgHistorical }, + /// + MigrateLiquidity {}, + /// Callbacks; only callable by the contract itself. + Callback(CallbackMsg), +} + +/// This structure describes the execute messages available in a managed vesting contract. +#[cw_serde] +pub enum ExecuteMsgManaged { + /// Removes vesting targets/accounts. + /// ## Executor + /// Only the current owner can execute this + RemoveVestingAccounts { + vesting_accounts: Vec, + /// Specifies the account that will receive the funds taken from the vesting accounts. + clawback_account: String, + }, +} + +/// This structure describes the execute messages available in a with_managers vesting contract. +#[cw_serde] +pub enum ExecuteMsgWithManagers { + /// Adds vesting managers + /// ## Executor + /// Only the current owner can execute this + AddVestingManagers { managers: Vec }, + /// Removes vesting managers + /// ## Executor + /// Only the current owner can execute this + RemoveVestingManagers { managers: Vec }, +} + +/// This structure describes the execute messages available in a historical vesting contract. +#[cw_serde] +pub enum ExecuteMsgHistorical {} + +/// This structure describes the query messages available in a vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the configuration for the contract using a [`ConfigResponse`] object. + #[returns(Config)] + Config {}, + /// Returns information about an address vesting tokens using a [`VestingAccountResponse`] object. + #[returns(VestingAccountResponse)] + VestingAccount { address: String }, + /// Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object. + #[returns(VestingAccountsResponse)] + VestingAccounts { + start_after: Option, + limit: Option, + order_by: Option, + }, + /// Returns the total unvested amount of tokens for a specific address. + #[returns(Uint128)] + AvailableAmount { address: String }, + /// Timestamp returns the current timestamp + #[returns(u64)] + Timestamp {}, + /// VestingState returns the current vesting state. + #[returns(VestingState)] + VestingState {}, + /// Contains messages associated with the managed extension for vesting contracts. + #[returns(Binary)] + ManagedExtension { msg: QueryMsgManaged }, + /// Contains messages associated with the with_managers extension for vesting contracts. + #[returns(QueryMsgWithManagers)] + WithManagersExtension { msg: QueryMsgWithManagers }, + /// Contains messages associated with the historical extension for vesting contracts. + #[returns(QueryMsgHistorical)] + HistoricalExtension { msg: QueryMsgHistorical }, +} + +/// This structure describes the query messages available in a managed vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgManaged {} + +/// This structure describes the query messages available in a with_managers vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgWithManagers { + /// Returns list of vesting managers + /// (the persons who are able to add/remove vesting schedules) + #[returns(Vec)] + VestingManagers {}, +} + +/// This structure describes the query messages available in a historical vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgHistorical { + /// Returns the total unclaimed amount of tokens for a specific address at certain height. + #[returns(Uint128)] + UnclaimedAmountAtHeight { address: String, height: u64 }, + /// Returns the total unclaimed amount of tokens for all the users at certain height. + #[returns(Uint128)] + UnclaimedTotalAmountAtHeight { height: u64 }, +} + +/// This structure describes a migration message. +/// We currently take no arguments for migrations. +#[cw_serde] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg { + pub max_slippage: Decimal, + pub ntrn_denom: String, + pub paired_denom: String, + pub xyk_pair: String, + pub cl_pair: String, + pub new_lp_token: String, + pub batch_size: u32, +} +/// This structure describes a CW20 hook message. +#[cw_serde] +pub enum Cw20HookMsg { + /// RegisterVestingAccounts registers vesting targets/accounts + RegisterVestingAccounts { + vesting_accounts: Vec, + }, +} +#[cw_serde] +pub enum CallbackMsg { + MigrateLiquidityToClPair { + xyk_pair: Addr, + xyk_lp_token: Addr, + amount: Uint128, + slippage_tolerance: Decimal, + cl_pair: Addr, + ntrn_denom: String, + paired_asset_denom: String, + user: VestingAccountResponse, + }, + ProvideLiquidityToClPairAfterWithdrawal { + ntrn_denom: String, + ntrn_init_balance: Uint128, + paired_asset_denom: String, + paired_asset_init_balance: Uint128, + cl_pair: Addr, + slippage_tolerance: Decimal, + user: VestingAccountResponse, + }, + PostMigrationVestingReschedule { + user: VestingAccountResponse, + }, +} + +// Modified from +// https://github.com/CosmWasm/cosmwasm-plus/blob/v0.2.3/packages/cw20/src/receiver.rs#L15 +impl CallbackMsg { + pub fn to_cosmos_msg(self, env: &Env) -> StdResult { + Ok(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: env.contract.address.to_string(), + msg: to_binary(&ExecuteMsg::Callback(self))?, + funds: vec![], + })) + } +} diff --git a/packages/vesting-base-lp/src/state.rs b/packages/vesting-base-lp/src/state.rs new file mode 100644 index 00000000..28017fc0 --- /dev/null +++ b/packages/vesting-base-lp/src/state.rs @@ -0,0 +1,166 @@ +use crate::types::{ + Config, MigrationState, OrderBy, VestingInfo, VestingState, XykToClMigrationConfig, +}; +use astroport::common::OwnershipProposal; +use cosmwasm_std::{Addr, Deps, StdResult}; +use cw_storage_plus::{Bound, Item, Map, SnapshotItem, SnapshotMap, Strategy}; + +pub(crate) const CONFIG: Item = Item::new("config"); +/// Migration status +pub(crate) const MIGRATION_STATUS: Item = Item::new("migration_status"); +pub(crate) const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); +pub(crate) const VESTING_MANAGERS: Map = Map::new("vesting_managers"); +pub(crate) const VESTING_STATE: SnapshotItem = SnapshotItem::new( + "vesting_state", + "vesting_state__checkpoints", + "vesting_state__changelog", + Strategy::Never, +); +pub(crate) const VESTING_INFO: SnapshotMap = SnapshotMap::new( + "vesting_info", + "vesting_info__checkpoints", + "vesting_info__changelog", + Strategy::Never, +); +pub(crate) const VESTING_STATE_HISTORICAL: SnapshotItem = SnapshotItem::new( + "vesting_state", + "vesting_state__checkpoints", + "vesting_state__changelog", + Strategy::EveryBlock, +); +pub(crate) const VESTING_INFO_HISTORICAL: SnapshotMap = SnapshotMap::new( + "vesting_info", + "vesting_info__checkpoints", + "vesting_info__changelog", + Strategy::EveryBlock, +); + +pub(crate) fn vesting_state(historical: bool) -> SnapshotItem<'static, VestingState> { + if historical { + return VESTING_STATE_HISTORICAL; + } + VESTING_STATE +} + +pub(crate) fn vesting_info(historical: bool) -> SnapshotMap<'static, Addr, VestingInfo> { + if historical { + return VESTING_INFO_HISTORICAL; + } + VESTING_INFO +} + +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +/// Returns an empty vector if it does not find data, otherwise returns a vector that +/// contains objects of type [`VESTING_INFO`]. +/// ## Params +/// +/// * **start_after** index from which to start reading vesting schedules. +/// +/// * **limit** amount of vesting schedules to read. +/// +/// * **order_by** whether results should be returned in an ascending or descending order. +pub(crate) fn read_vesting_infos( + deps: Deps, + start_after: Option, + limit: Option, + order_by: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start_after = start_after.map(Bound::exclusive); + + let (start, end) = match &order_by { + Some(OrderBy::Asc) => (start_after, None), + _ => (None, start_after), + }; + + let info: Vec<(Addr, VestingInfo)> = VESTING_INFO + .range( + deps.storage, + start, + end, + order_by.unwrap_or(OrderBy::Desc).into(), + ) + .take(limit) + .filter_map(|v| v.ok()) + .collect(); + + Ok(info) +} + +#[cfg(test)] +mod testing { + use super::*; + + #[test] + fn read_vesting_infos_as_expected() { + use cosmwasm_std::{testing::mock_dependencies, Uint128}; + let mut deps = mock_dependencies(); + let historical = false; + + let vi_mock = VestingInfo { + released_amount: Uint128::zero(), + schedules: vec![], + }; + + for i in 1..5 { + let key = Addr::unchecked(format! {"address{}", i}); + + vesting_info(historical) + .save(&mut deps.storage, key, &vi_mock, 1) + .unwrap(); + } + + let res = read_vesting_infos( + deps.as_ref(), + Some(Addr::unchecked("address2")), + None, + Some(OrderBy::Asc), + ) + .unwrap(); + assert_eq!( + res, + vec![ + (Addr::unchecked("address3"), vi_mock.clone()), + (Addr::unchecked("address4"), vi_mock.clone()), + ] + ); + + let res = read_vesting_infos( + deps.as_ref(), + Some(Addr::unchecked("address2")), + Some(1), + Some(OrderBy::Asc), + ) + .unwrap(); + assert_eq!(res, vec![(Addr::unchecked("address3"), vi_mock.clone())]); + + let res = read_vesting_infos( + deps.as_ref(), + Some(Addr::unchecked("address3")), + None, + Some(OrderBy::Desc), + ) + .unwrap(); + assert_eq!( + res, + vec![ + (Addr::unchecked("address2"), vi_mock.clone()), + (Addr::unchecked("address1"), vi_mock.clone()), + ] + ); + + let res = read_vesting_infos( + deps.as_ref(), + Some(Addr::unchecked("address3")), + Some(1), + Some(OrderBy::Desc), + ) + .unwrap(); + assert_eq!(res, vec![(Addr::unchecked("address2"), vi_mock.clone())]); + } +} + +pub const XYK_TO_CL_MIGRATION_CONFIG: Item = + Item::new("xyk_to_cl_migration_config"); diff --git a/packages/vesting-base-lp/src/types.rs b/packages/vesting-base-lp/src/types.rs new file mode 100644 index 00000000..40147ed9 --- /dev/null +++ b/packages/vesting-base-lp/src/types.rs @@ -0,0 +1,131 @@ +use astroport::asset::AssetInfo; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Decimal, Order, Uint128}; + +/// This structure stores the main parameters for the generator vesting contract. +#[cw_serde] +pub struct Config { + /// Address that's allowed to change contract parameters + pub owner: Addr, + /// [`AssetInfo`] of the vested token + pub vesting_token: Option, + /// Address that's allowed to change vesting token + pub token_info_manager: Addr, + /// Contains extensions information of the contract + pub extensions: Extensions, +} + +/// Contains extensions information for the contract. +#[cw_serde] +pub struct Extensions { + /// Whether the historical extension is enabled for the contract. + pub historical: bool, + /// Whether the managed extension is enabled for the contract. + pub managed: bool, + /// Whether the with_managers extension is enabled for the contract. + pub with_managers: bool, +} + +/// This structure stores the accumulated vesting information for all addresses. +#[cw_serde] +#[derive(Default)] +pub struct VestingState { + /// The total amount of tokens granted to the users + pub total_granted: Uint128, + /// The total amount of tokens already claimed + pub total_released: Uint128, +} + +/// This structure stores vesting information for a specific address that is getting tokens. +#[cw_serde] +pub struct VestingAccount { + /// The address that is getting tokens + pub address: String, + /// The vesting schedules targeted at the `address` + pub schedules: Vec, +} + +/// This structure stores parameters for a batch of vesting schedules. +#[cw_serde] +pub struct VestingInfo { + /// The vesting schedules + pub schedules: Vec, + /// The total amount of vested tokens already claimed + pub released_amount: Uint128, +} + +/// This structure stores parameters for a specific vesting schedule +#[cw_serde] +pub struct VestingSchedule { + /// The start date for the vesting schedule + pub start_point: VestingSchedulePoint, + /// The end point for the vesting schedule + pub end_point: Option, +} + +/// This structure stores the parameters used to create a vesting schedule. +#[cw_serde] +pub struct VestingSchedulePoint { + /// The start time for the vesting schedule + pub time: u64, + /// The amount of tokens being vested + pub amount: Uint128, +} + +/// This structure describes a custom struct used to return vesting data about a specific vesting target. +#[cw_serde] +pub struct VestingAccountResponse { + /// The address that's vesting tokens + pub address: Addr, + /// Vesting information + pub info: VestingInfo, +} + +/// This structure describes a custom struct used to return vesting data for multiple vesting targets. +#[cw_serde] +pub struct VestingAccountsResponse { + /// A list of accounts that are vesting tokens + pub vesting_accounts: Vec, +} + +/// Config for xyk->CL liquidity migration. +#[cw_serde] +pub struct XykToClMigrationConfig { + /// The maximum allowed slippage tolerance for xyk to CL liquidity migration calls. + pub max_slippage: Decimal, + pub ntrn_denom: String, + pub xyk_pair: Addr, + pub paired_denom: String, + pub cl_pair: Addr, + pub new_lp_token: Addr, + pub last_processed_user: Option, + pub batch_size: u32, +} + +#[cw_serde] +pub enum MigrationState { + /// Migration is started + Started, + + Completed, +} + +/// This enum describes the types of sorting that can be applied to some piece of data +#[cw_serde] +pub enum OrderBy { + Asc, + Desc, +} + +// We suppress this clippy warning because Order in cosmwasm doesn't implement Debug and +// PartialEq for usage in QueryMsg. We need to use our own OrderBy and convert the result to cosmwasm's Order +#[allow(clippy::from_over_into)] +impl Into for OrderBy { + fn into(self) -> Order { + if self == OrderBy::Asc { + Order::Ascending + } else { + Order::Descending + } + } +} diff --git a/packages/vesting-base-pcl/Cargo.toml b/packages/vesting-base-pcl/Cargo.toml new file mode 100644 index 00000000..ffe8e644 --- /dev/null +++ b/packages/vesting-base-pcl/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "vesting-base" +version = "1.1.0" +authors = ["Astroport"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +cw20 = { workspace = true } +cosmwasm-std = { workspace = true } +cw-storage-plus = { workspace = true } +astroport = { workspace = true } +thiserror = { workspace = true } +# we keep it at 0.15 instead of latest version just for vesting investors contract +cw-utils = "0.15" +cosmwasm-schema = { workspace = true } diff --git a/packages/vesting-base-pcl/NOTICE b/packages/vesting-base-pcl/NOTICE new file mode 100644 index 00000000..84b1c210 --- /dev/null +++ b/packages/vesting-base-pcl/NOTICE @@ -0,0 +1,14 @@ +CW20-Base: A reference implementation for fungible token on CosmWasm +Copyright (C) 2020 Confio OÜ + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/vesting-base-pcl/README.md b/packages/vesting-base-pcl/README.md new file mode 100644 index 00000000..57accc63 --- /dev/null +++ b/packages/vesting-base-pcl/README.md @@ -0,0 +1,251 @@ +# Neutron Vesting Base + +This library contains basis for configuration and initialisation of vesting contracts. It also contains data models and handlers for interaction with vesting contracts. + +## Usage + +1. To use the library for initialisation of a simple vesting contract just build a default vesting base in its instantiate message: +```rust +use vesting_base::builder::VestingBaseBuilder; + +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + ... + VestingBaseBuilder::default().build(deps, msg.owner, msg.vesting_token)?; + ... +``` + +Read about more advanced building in the [Extensions](#extensions) section. + +2. Simply pass the execute and query requests to the vesting base's execute and query handlers: +```rust +use vesting_base::handlers::{execute as base_execute, query as base_query}; + +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + base_execute(deps, env, info, msg) +} + +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + base_query(deps, env, msg) +} +``` + +### Messages + +The default version exposes the following messages: + +#### ExecuteMsg + +```rust +/// This structure describes the execute messages available in a vesting contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Claim claims vested tokens and sends them to a recipient + Claim { + /// The address that receives the vested tokens + recipient: Option, + /// The amount of tokens to claim + amount: Option, + }, + /// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template + Receive(Cw20ReceiveMsg), + /// RegisterVestingAccounts registers vesting targets/accounts + RegisterVestingAccounts { + vesting_accounts: Vec, + }, + /// Creates a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the offer to change the owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + DropOwnershipProposal {}, + /// Claims contract ownership + /// ## Executor + /// Only the newly proposed owner can execute this + ClaimOwnership {}, + /// Sets vesting token + /// ## Executor + /// Only the current owner or token info manager can execute this + SetVestingToken { vesting_token: AssetInfo }, + /// Contains messages associated with the managed extension for vesting contracts. + ManagedExtension { msg: ExecuteMsgManaged }, + /// Contains messages associated with the with_managers extension for vesting contracts. + WithManagersExtension { msg: ExecuteMsgWithManagers }, + /// Contains messages associated with the historical extension for vesting contracts. + HistoricalExtension { msg: ExecuteMsgHistorical }, +} +``` + +The `ManagedExtension`, `WithManagersExtension`, and `HistoricalExtension` messages are extensiom messages. Read about them in the [Extensions](#extensions) section. + +#### QueryMsg + +```rust +/// This structure describes the query messages available in a vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the configuration for the contract using a [`ConfigResponse`] object. + #[returns(ConfigResponse)] + Config {}, + /// Returns information about an address vesting tokens using a [`VestingAccountResponse`] object. + #[returns(VestingAccountResponse)] + VestingAccount { address: String }, + /// Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object. + #[returns(VestingAccountsResponse)] + VestingAccounts { + start_after: Option, + limit: Option, + order_by: Option, + }, + /// Returns the total unvested amount of tokens for a specific address. + #[returns(Uint128)] + AvailableAmount { address: String }, + /// Timestamp returns the current timestamp + #[returns(u64)] + Timestamp {}, + /// VestingState returns the current vesting state. + #[returns(VestingState)] + VestingState {}, + /// Contains messages associated with the managed extension for vesting contracts. + #[returns(QueryMsgManaged)] + ManagedExtension { msg: QueryMsgManaged }, + /// Contains messages associated with the with_managers extension for vesting contracts. + #[returns(QueryMsgWithManagers)] + WithManagersExtension { msg: QueryMsgWithManagers }, + /// Contains messages associated with the historical extension for vesting contracts. + #[returns(QueryMsgHistorical)] + HistoricalExtension { msg: QueryMsgHistorical }, +} +``` + +The `ManagedExtension`, `WithManagersExtension`, and `HistoricalExtension` messages are extensiom messages. Read about them in the [Extensions](#extensions) section. + +## Extensions + +Created contracts can be extended with a number of features. + +### Managed + +The `managed` extension allows the owner of the vesting contract to remove registered vesting accounts and redeem the corresponding funds. + +```rust +/// This structure describes the execute messages available in a managed vesting contract. +#[cw_serde] +pub enum ExecuteMsgManaged { + /// Removes vesting targets/accounts. + /// ## Executor + /// Only the current owner can execute this + RemoveVestingAccounts { + vesting_accounts: Vec, + /// Specifies the account that will receive the funds taken from the vesting accounts. + clawback_account: String, + }, +} + +/// This structure describes the query messages available in a managed vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgManaged {} +``` + +### WithManagers + +The `with_managers` extension allows the owner of the vesting contract to add/remove vesting managers — addresses that just like the owner are capable of registering new vesting accounts. + +```rust +/// This structure describes the execute messages available in a with_managers vesting contract. +#[cw_serde] +pub enum ExecuteMsgWithManagers { + /// Adds vesting managers + /// ## Executor + /// Only the current owner can execute this + AddVestingManagers { managers: Vec }, + /// Removes vesting managers + /// ## Executor + /// Only the current owner can execute this + RemoveVestingManagers { managers: Vec }, +} + +/// This structure describes the query messages available in a with_managers vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgWithManagers { + /// Returns list of vesting managers + /// (the persons who are able to add/remove vesting schedules) + #[returns(Vec)] + VestingManagers {}, +} +``` + +### Historical + +The `historical` allows to query vesting accounts and total vesting state based on a given height. + +```rust +/// This structure describes the execute messages available in a historical vesting contract. +#[cw_serde] +pub enum ExecuteMsgHistorical {} + +/// This structure describes the query messages available in a historical vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgHistorical { + /// Returns the total unclaimed amount of tokens for a specific address at certain height. + #[returns(Uint128)] + UnclaimedAmountAtHeight { address: String, height: u64 }, + /// Returns the total unclaimed amount of tokens for all the users at certain height. + #[returns(Uint128)] + UnclaimedTotalAmountAtHeight { height: u64 }, +} +``` + +### Extensions usage + +The following example adds all three extensions to the contract, but it's allowed to combine them in any way. +```rust +use vesting_base::builder::VestingBaseBuilder; +use astroport::asset::AssetInfo; +use cosmwasm_schema::cw_serde; + +/// This structure describes the parameters used for creating a contract. +#[cw_serde] +pub struct InstantiateMsg { + /// Address allowed to change contract parameters + pub owner: String, + /// [`AssetInfo`] of the token that's being vested + pub vesting_token: AssetInfo, + /// Initial list of whitelisted vesting managers + pub vesting_managers: Vec, +} + +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + ... + VestingBaseBuilder::default() + .historical() + .managed() + .with_managers(msg.vesting_managers) + .build(deps, msg.owner, msg.vesting_token)?; + ... +``` diff --git a/packages/vesting-base-pcl/src/builder.rs b/packages/vesting-base-pcl/src/builder.rs new file mode 100644 index 00000000..85957245 --- /dev/null +++ b/packages/vesting-base-pcl/src/builder.rs @@ -0,0 +1,60 @@ +use crate::state::{CONFIG, VESTING_MANAGERS}; +use crate::types::{Config, Extensions}; +use cosmwasm_std::{DepsMut, StdResult}; + +/// A builder for vesting contracts with different extensions. +#[derive(Default)] +pub struct VestingBaseBuilder { + vesting_managers: Vec, + historical: bool, + managed: bool, + with_managers: bool, +} + +impl VestingBaseBuilder { + /// Appends the `managed` extension to the created vesting contract. + pub fn managed(&mut self) -> &mut VestingBaseBuilder { + self.managed = true; + self + } + + /// Appends the `with_managers` extension to the created vesting contract. + pub fn with_managers(&mut self, managers: Vec) -> &mut VestingBaseBuilder { + self.vesting_managers.extend(managers); + self.with_managers = true; + self + } + + /// Appends the `historical` extension to the created vesting contract. + pub fn historical(&mut self) -> &mut VestingBaseBuilder { + self.historical = true; + self + } + + /// Validates the inputs and initialises the created contract state. + pub fn build(&self, deps: DepsMut, owner: String, token_info_manager: String) -> StdResult<()> { + let owner = deps.api.addr_validate(&owner)?; + CONFIG.save( + deps.storage, + &Config { + owner, + vesting_token: None, + token_info_manager: deps.api.addr_validate(&token_info_manager)?, + extensions: Extensions { + historical: self.historical, + managed: self.managed, + with_managers: self.with_managers, + }, + }, + )?; + + if self.with_managers { + for m in self.vesting_managers.iter() { + let ma = deps.api.addr_validate(m)?; + VESTING_MANAGERS.save(deps.storage, ma, &())?; + } + }; + + Ok(()) + } +} diff --git a/packages/vesting-base-pcl/src/error.rs b/packages/vesting-base-pcl/src/error.rs new file mode 100644 index 00000000..a1859ac0 --- /dev/null +++ b/packages/vesting-base-pcl/src/error.rs @@ -0,0 +1,47 @@ +use cosmwasm_std::{OverflowError, StdError}; +use cw_utils::PaymentError; +use thiserror::Error; + +/// This enum describes generator vesting contract errors +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + PaymentError(#[from] PaymentError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Amount is not available!")] + AmountIsNotAvailable {}, + + #[error("Vesting schedule error on addr: {0}. Should satisfy: (start < end and at_start < total) or (start = end and at_start = total)")] + VestingScheduleError(String), + + #[error("Vesting schedule amount error. The total amount should be equal to the CW20 receive amount.")] + VestingScheduleAmountError {}, + + #[error("Contract can't be migrated!")] + MigrationError {}, + + #[error("Vesting token is not set!")] + VestingTokenIsNotSet {}, + + #[error("Vesting token is already set!")] + VestingTokenAlreadySet {}, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} + +pub fn ext_unsupported_err(extension: impl Into + std::fmt::Display) -> StdError { + StdError::generic_err(format!( + "Extension is not enabled for the contract: {}.", + extension + )) +} diff --git a/packages/vesting-base-pcl/src/ext_historical.rs b/packages/vesting-base-pcl/src/ext_historical.rs new file mode 100644 index 00000000..d494adf6 --- /dev/null +++ b/packages/vesting-base-pcl/src/ext_historical.rs @@ -0,0 +1,101 @@ +use crate::error::{ext_unsupported_err, ContractError}; +use crate::msg::{ExecuteMsgHistorical, QueryMsgHistorical}; +use crate::state::{vesting_info, vesting_state, CONFIG}; +use crate::types::VestingInfo; +use cosmwasm_std::{ + to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, Uint128, +}; + +/// Contains the historical extension check and routing of the message. +pub(crate) fn handle_execute_historical_msg( + deps: DepsMut, + _env: Env, + _info: MessageInfo, + _msg: ExecuteMsgHistorical, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.historical { + return Err(ext_unsupported_err("historical").into()); + } + + // empty handler kept for uniformity with other extensions + unimplemented!() +} + +/// Contains the historical extension check and routing of the message. +pub(crate) fn handle_query_historical_msg( + deps: Deps, + _env: Env, + msg: QueryMsgHistorical, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.historical { + return Err(ext_unsupported_err("historical")); + } + + match msg { + QueryMsgHistorical::UnclaimedAmountAtHeight { address, height } => { + to_binary(&query_unclaimed_amount_at_height(deps, address, height)?) + } + QueryMsgHistorical::UnclaimedTotalAmountAtHeight { height } => { + to_binary(&query_total_unclaimed_amount_at_height(deps, height)?) + } + } +} + +/// Returns the available amount of distributed and yet to be claimed tokens for a specific vesting recipient at certain height. +/// +/// * **address** vesting recipient for which to return the available amount of tokens to claim. +/// +/// * **height** the height we querying unclaimed amount for +fn query_unclaimed_amount_at_height( + deps: Deps, + address: String, + height: u64, +) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + let config = CONFIG.load(deps.storage)?; + let maybe_info = vesting_info(config.extensions.historical).may_load_at_height( + deps.storage, + address, + height, + )?; + match &maybe_info { + Some(info) => compute_unclaimed_amount(info), + None => Ok(Uint128::zero()), + } +} + +/// Returns the available amount of distributed and yet to be claimed tokens for all the recipients at certain height. +/// +/// * **height** the height we querying unclaimed amount for +fn query_total_unclaimed_amount_at_height(deps: Deps, height: u64) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let maybe_state = + vesting_state(config.extensions.historical).may_load_at_height(deps.storage, height)?; + match &maybe_state { + Some(info) => Ok(info.total_granted.checked_sub(info.total_released)?), + None => Ok(Uint128::zero()), + } +} + +/// Computes the amount of distributed and yet unclaimed tokens for a specific vesting recipient at certain height. +/// Returns the computed amount if the operation is successful. +/// +/// * **vesting_info** vesting schedules for which to compute the amount of tokens +/// that are vested and can be claimed by the recipient. +fn compute_unclaimed_amount(vesting_info: &VestingInfo) -> StdResult { + let mut available_amount: Uint128 = Uint128::zero(); + for sch in &vesting_info.schedules { + if let Some(end_point) = &sch.end_point { + available_amount = available_amount.checked_add(end_point.amount)?; + } else { + available_amount = available_amount.checked_add(sch.start_point.amount)?; + } + } + + available_amount + .checked_sub(vesting_info.released_amount) + .map_err(StdError::from) +} diff --git a/packages/vesting-base-pcl/src/ext_managed.rs b/packages/vesting-base-pcl/src/ext_managed.rs new file mode 100644 index 00000000..94b68d20 --- /dev/null +++ b/packages/vesting-base-pcl/src/ext_managed.rs @@ -0,0 +1,121 @@ +use crate::error::{ext_unsupported_err, ContractError}; +use crate::handlers::get_vesting_token; +use crate::msg::{ExecuteMsgManaged, QueryMsgManaged}; +use crate::state::{vesting_info, vesting_state, CONFIG}; +use astroport::asset::AssetInfoExt; +use cosmwasm_std::{ + attr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, SubMsg, Uint128, +}; + +/// Contains the managed extension check and routing of the message. +pub(crate) fn handle_execute_managed_msg( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsgManaged, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.managed { + return Err(ext_unsupported_err("managed").into()); + } + + match msg { + ExecuteMsgManaged::RemoveVestingAccounts { + vesting_accounts, + clawback_account, + } => remove_vesting_accounts(deps, env, info, vesting_accounts, clawback_account), + } +} + +/// Contains the managed extension check and routing of the message. +pub(crate) fn handle_query_managed_msg( + deps: Deps, + _env: Env, + _msg: QueryMsgManaged, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.managed { + return Err(ext_unsupported_err("managed")); + } + + // empty handler kept for uniformity with other extensions + unimplemented!() +} + +#[allow(clippy::too_many_arguments)] +fn remove_vesting_accounts( + deps: DepsMut, + env: Env, + info: MessageInfo, + vesting_accounts: Vec, + clawback_account: String, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + let vesting_token = get_vesting_token(&config)?; + + let mut response = Response::new(); + + let clawback_address = deps.api.addr_validate(&clawback_account)?; + + // For each vesting account, calculate the amount of tokens to claw back (unclaimed + still + // vesting), transfer the required amount to the owner, remove the vesting information + // from the storage, and decrease the total granted metric. + for vesting_account in vesting_accounts { + let account_address = deps.api.addr_validate(&vesting_account)?; + + let config = CONFIG.load(deps.storage)?; + let vesting_info = vesting_info(config.extensions.historical); + if let Some(account_info) = vesting_info.may_load(deps.storage, account_address.clone())? { + let mut total_granted_for_user = Uint128::zero(); + for sch in account_info.schedules { + if let Some(end_point) = sch.end_point { + total_granted_for_user = + total_granted_for_user.checked_add(end_point.amount)?; + } else { + total_granted_for_user = + total_granted_for_user.checked_add(sch.start_point.amount)?; + } + } + + let amount_to_claw_back = + total_granted_for_user.checked_sub(account_info.released_amount)?; + + let transfer_msg = vesting_token + .with_balance(amount_to_claw_back) + .into_msg(&deps.querier, clawback_address.clone())?; + response = response.add_submessage(SubMsg::new(transfer_msg)); + + vesting_state(config.extensions.historical).update::<_, ContractError>( + deps.storage, + env.block.height, + |s| { + // Here we choose the "forget about everything" strategy. E.g., if we granted a user + // 300 tokens, and they claimed 150 tokens, the vesting state is + // { total_granted: 300, total_released: 150 }. + // If after that we remove the user's vesting account, we set the vesting state to + // { total_granted: 0, total_released: 0 }. + // + // If we decided to set it to { total_granted: 150, total_released: 150 }., the + // .total_released value of the vesting state would not be equal to the sum of the + // .released_amount values of all registered accounts. + let mut state = s.ok_or(ContractError::AmountIsNotAvailable {})?; + state.total_granted = + state.total_granted.checked_sub(total_granted_for_user)?; + state.total_released = state + .total_released + .checked_sub(account_info.released_amount)?; + Ok(state) + }, + )?; + vesting_info.remove(deps.storage, account_address, env.block.height)?; + } + } + + Ok(response.add_attributes(vec![ + attr("action", "remove_vesting_accounts"), + attr("sender", &info.sender), + ])) +} diff --git a/packages/vesting-base-pcl/src/ext_with_managers.rs b/packages/vesting-base-pcl/src/ext_with_managers.rs new file mode 100644 index 00000000..56573188 --- /dev/null +++ b/packages/vesting-base-pcl/src/ext_with_managers.rs @@ -0,0 +1,105 @@ +use crate::error::{ext_unsupported_err, ContractError}; +use crate::msg::{ExecuteMsgWithManagers, QueryMsgWithManagers}; +use crate::state::{CONFIG, VESTING_MANAGERS}; +use cosmwasm_std::{ + attr, to_binary, Addr, Attribute, Binary, Deps, DepsMut, Env, MessageInfo, Order, Response, + StdError, StdResult, +}; + +/// Contains the with_managers extension check and routing of the message. +pub(crate) fn handle_execute_with_managers_msg( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsgWithManagers, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.with_managers { + return Err(ext_unsupported_err("with_managers").into()); + } + + match msg { + ExecuteMsgWithManagers::AddVestingManagers { managers } => { + add_vesting_managers(deps, env, info, managers) + } + ExecuteMsgWithManagers::RemoveVestingManagers { managers } => { + remove_vesting_managers(deps, env, info, managers) + } + } +} + +/// Contains the with_managers extension check and routing of the message. +pub(crate) fn handle_query_managers_msg( + deps: Deps, + _env: Env, + msg: QueryMsgWithManagers, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + if !config.extensions.with_managers { + return Err(ext_unsupported_err("with_managers")); + } + + match msg { + QueryMsgWithManagers::VestingManagers {} => to_binary(&query_vesting_managers(deps)?), + } +} + +/// Adds new vesting managers, which have a permission to add/remove vesting schedule +/// +/// * **managers** list of accounts to be added to the whitelist. +fn add_vesting_managers( + deps: DepsMut, + _env: Env, + info: MessageInfo, + managers: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + let mut attrs: Vec = vec![]; + for m in managers { + let ma = deps.api.addr_validate(&m)?; + if !VESTING_MANAGERS.has(deps.storage, ma.clone()) { + VESTING_MANAGERS.save(deps.storage, ma, &())?; + attrs.push(attr("vesting_manager", &m)) + } + } + Ok(Response::new() + .add_attribute("action", "add_vesting_managers") + .add_attributes(attrs)) +} + +/// Removes new vesting managers from the whitelist +/// +/// * **managers** list of accounts to be removed from the whitelist. +fn remove_vesting_managers( + deps: DepsMut, + _env: Env, + info: MessageInfo, + managers: Vec, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + let mut attrs: Vec = vec![]; + for m in managers { + let ma = deps.api.addr_validate(&m)?; + if VESTING_MANAGERS.has(deps.storage, ma.clone()) { + VESTING_MANAGERS.remove(deps.storage, ma); + attrs.push(attr("vesting_manager", &m)) + } + } + Ok(Response::new() + .add_attribute("action", "remove_vesting_managers") + .add_attributes(attrs)) +} + +/// Returns a list of vesting schedules using a [`VestingAccountsResponse`] object. +fn query_vesting_managers(deps: Deps) -> StdResult> { + let managers = VESTING_MANAGERS + .keys(deps.storage, None, None, Order::Ascending) + .collect::, StdError>>()?; + Ok(managers) +} diff --git a/packages/vesting-base-pcl/src/handlers.rs b/packages/vesting-base-pcl/src/handlers.rs new file mode 100644 index 00000000..7797db28 --- /dev/null +++ b/packages/vesting-base-pcl/src/handlers.rs @@ -0,0 +1,450 @@ +use crate::error::ContractError; +use crate::ext_historical::{handle_execute_historical_msg, handle_query_historical_msg}; +use crate::ext_managed::{handle_execute_managed_msg, handle_query_managed_msg}; +use crate::ext_with_managers::{handle_execute_with_managers_msg, handle_query_managers_msg}; +use crate::msg::{Cw20HookMsg, ExecuteMsg, MigrateMsg, QueryMsg}; +use crate::state::{read_vesting_infos, vesting_info, vesting_state}; +use crate::state::{CONFIG, OWNERSHIP_PROPOSAL, VESTING_MANAGERS}; +use crate::types::{ + Config, OrderBy, VestingAccount, VestingAccountResponse, VestingAccountsResponse, VestingInfo, + VestingSchedule, VestingState, +}; +use astroport::asset::{addr_opt_validate, token_asset_info, AssetInfo, AssetInfoExt}; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use cosmwasm_std::{ + attr, from_binary, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, + StdError, StdResult, Storage, SubMsg, Uint128, +}; +use cw20::Cw20ReceiveMsg; +use cw_utils::must_pay; + +/// Exposes execute functions available in the contract. +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::Claim { recipient, amount } => claim(deps, env, info, recipient, amount), + ExecuteMsg::Receive(msg) => receive_cw20(deps, env, info, msg), + ExecuteMsg::RegisterVestingAccounts { vesting_accounts } => { + let config = CONFIG.load(deps.storage)?; + let vesting_token = get_vesting_token(&config)?; + + match &vesting_token { + AssetInfo::NativeToken { denom } + if is_sender_whitelisted(deps.storage, &config, &info.sender) => + { + let amount = must_pay(&info, denom)?; + register_vesting_accounts(deps, vesting_accounts, amount, env.block.height) + } + _ => Err(ContractError::Unauthorized {}), + } + } + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config: Config = CONFIG.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + &OWNERSHIP_PROPOSAL, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config: Config = CONFIG.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, &OWNERSHIP_PROPOSAL) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => { + claim_ownership(deps, info, env, &OWNERSHIP_PROPOSAL, |deps, new_owner| { + CONFIG.update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + })?; + + Ok(()) + }) + .map_err(Into::into) + } + ExecuteMsg::SetVestingToken { vesting_token } => { + set_vesting_token(deps, env, info, vesting_token) + } + ExecuteMsg::ManagedExtension { msg } => handle_execute_managed_msg(deps, env, info, msg), + ExecuteMsg::WithManagersExtension { msg } => { + handle_execute_with_managers_msg(deps, env, info, msg) + } + ExecuteMsg::HistoricalExtension { msg } => { + handle_execute_historical_msg(deps, env, info, msg) + } + } +} + +/// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. +/// +/// * **cw20_msg** CW20 message to process. +fn receive_cw20( + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + let vesting_token = get_vesting_token(&config)?; + + // Permission check + if !is_sender_whitelisted( + deps.storage, + &config, + &deps.api.addr_validate(&cw20_msg.sender)?, + ) || token_asset_info(info.sender) != vesting_token + { + return Err(ContractError::Unauthorized {}); + } + + match from_binary(&cw20_msg.msg)? { + Cw20HookMsg::RegisterVestingAccounts { vesting_accounts } => { + register_vesting_accounts(deps, vesting_accounts, cw20_msg.amount, env.block.height) + } + } +} + +/// Create new vesting schedules. +/// +/// * **vesting_accounts** list of accounts and associated vesting schedules to create. +/// +/// * **cw20_amount** sets the amount that confirms the total amount of all accounts to register. +fn register_vesting_accounts( + deps: DepsMut, + vesting_accounts: Vec, + amount: Uint128, + height: u64, +) -> Result { + let response = Response::new(); + let config = CONFIG.load(deps.storage)?; + let mut to_deposit = Uint128::zero(); + + for mut vesting_account in vesting_accounts { + let mut released_amount = Uint128::zero(); + let account_address = deps.api.addr_validate(&vesting_account.address)?; + + assert_vesting_schedules(&account_address, &vesting_account.schedules)?; + + for sch in &vesting_account.schedules { + let amount = if let Some(end_point) = &sch.end_point { + end_point.amount + } else { + sch.start_point.amount + }; + to_deposit = to_deposit.checked_add(amount)?; + } + + let vesting_info = vesting_info(config.extensions.historical); + if let Some(mut old_info) = vesting_info.may_load(deps.storage, account_address.clone())? { + released_amount = old_info.released_amount; + vesting_account.schedules.append(&mut old_info.schedules); + } + + vesting_info.save( + deps.storage, + account_address, + &VestingInfo { + schedules: vesting_account.schedules, + released_amount, + }, + height, + )?; + } + + if to_deposit != amount { + return Err(ContractError::VestingScheduleAmountError {}); + } + + vesting_state(config.extensions.historical).update::<_, ContractError>( + deps.storage, + height, + |s| { + let mut state = s.unwrap_or_default(); + state.total_granted = state.total_granted.checked_add(to_deposit)?; + Ok(state) + }, + )?; + + Ok(response.add_attributes({ + vec![ + attr("action", "register_vesting_accounts"), + attr("deposited", to_deposit), + ] + })) +} + +/// Claims vested tokens and transfers them to the vesting recipient. +/// +/// * **recipient** vesting recipient for which to claim tokens. +/// +/// * **amount** amount of vested tokens to claim. +fn claim( + deps: DepsMut, + env: Env, + info: MessageInfo, + recipient: Option, + amount: Option, +) -> Result { + let config = CONFIG.load(deps.storage)?; + let vesting_token = get_vesting_token(&config)?; + let vesting_info = vesting_info(config.extensions.historical); + let mut sender_vesting_info = vesting_info.load(deps.storage, info.sender.clone())?; + + let available_amount = + compute_available_amount(env.block.time.seconds(), &sender_vesting_info)?; + + let claim_amount = if let Some(a) = amount { + if a > available_amount { + return Err(ContractError::AmountIsNotAvailable {}); + }; + a + } else { + available_amount + }; + + let mut response = Response::new(); + + if !claim_amount.is_zero() { + let transfer_msg = vesting_token.with_balance(claim_amount).into_msg( + &deps.querier, + recipient.unwrap_or_else(|| info.sender.to_string()), + )?; + response = response.add_submessage(SubMsg::new(transfer_msg)); + + sender_vesting_info.released_amount = sender_vesting_info + .released_amount + .checked_add(claim_amount)?; + vesting_info.save( + deps.storage, + info.sender.clone(), + &sender_vesting_info, + env.block.height, + )?; + vesting_state(config.extensions.historical).update::<_, ContractError>( + deps.storage, + env.block.height, + |s| { + let mut state = s.ok_or(ContractError::AmountIsNotAvailable {})?; + state.total_released = state.total_released.checked_add(claim_amount)?; + Ok(state) + }, + )?; + }; + + Ok(response.add_attributes(vec![ + attr("action", "claim"), + attr("address", &info.sender), + attr("available_amount", available_amount), + attr("claimed_amount", claim_amount), + ])) +} + +pub(crate) fn set_vesting_token( + deps: DepsMut, + _env: Env, + info: MessageInfo, + token: AssetInfo, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + if info.sender != config.owner && info.sender != config.token_info_manager { + return Err(ContractError::Unauthorized {}); + } + if config.vesting_token.is_some() { + return Err(ContractError::VestingTokenAlreadySet {}); + } + + token.check(deps.api)?; + config.vesting_token = Some(token.clone()); + CONFIG.save(deps.storage, &config)?; + + let response = Response::new(); + Ok(response.add_attributes(vec![ + attr("action", "set_vesting_token"), + attr("vesting_token", token.to_string()), + ])) +} + +pub(crate) fn get_vesting_token(config: &Config) -> Result { + config + .vesting_token + .clone() + .ok_or(ContractError::VestingTokenIsNotSet {}) +} + +/// Exposes all the queries available in the contract. +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => Ok(to_binary(&query_config(deps)?)?), + QueryMsg::VestingAccount { address } => { + Ok(to_binary(&query_vesting_account(deps, address)?)?) + } + QueryMsg::VestingAccounts { + start_after, + limit, + order_by, + } => Ok(to_binary(&query_vesting_accounts( + deps, + start_after, + limit, + order_by, + )?)?), + QueryMsg::AvailableAmount { address } => Ok(to_binary(&query_vesting_available_amount( + deps, env, address, + )?)?), + QueryMsg::VestingState {} => Ok(to_binary(&query_vesting_state(deps)?)?), + QueryMsg::Timestamp {} => Ok(to_binary(&query_timestamp(env)?)?), + QueryMsg::ManagedExtension { msg } => handle_query_managed_msg(deps, env, msg), + QueryMsg::WithManagersExtension { msg } => handle_query_managers_msg(deps, env, msg), + QueryMsg::HistoricalExtension { msg } => handle_query_historical_msg(deps, env, msg), + } +} + +/// Returns the vesting contract configuration using a [`Config`] object. +fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(config) +} + +/// Returns the accumulated vesting information for all addresses using a [`VestingState`] object. +fn query_vesting_state(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + let state = vesting_state(config.extensions.historical).load(deps.storage)?; + + Ok(state) +} + +/// Return the current block timestamp (in seconds) +/// * **env** is an object of type [`Env`]. +fn query_timestamp(env: Env) -> StdResult { + Ok(env.block.time.seconds()) +} + +/// Returns the vesting data for a specific vesting recipient using a [`VestingAccountResponse`] object. +/// +/// * **address** vesting recipient for which to return vesting data. +fn query_vesting_account(deps: Deps, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + let config = CONFIG.load(deps.storage)?; + let info = vesting_info(config.extensions.historical).load(deps.storage, address.clone())?; + + Ok(VestingAccountResponse { address, info }) +} + +/// Returns a list of vesting schedules using a [`VestingAccountsResponse`] object. +/// +/// * **start_after** index from which to start reading vesting schedules. +/// +/// * **limit** amount of vesting schedules to return. +/// +/// * **order_by** whether results should be returned in an ascending or descending order. +fn query_vesting_accounts( + deps: Deps, + start_after: Option, + limit: Option, + order_by: Option, +) -> StdResult { + let start_after = addr_opt_validate(deps.api, &start_after)?; + + let vesting_infos = read_vesting_infos(deps, start_after, limit, order_by)?; + + let vesting_accounts: Vec<_> = vesting_infos + .into_iter() + .map(|(address, info)| VestingAccountResponse { address, info }) + .collect(); + + Ok(VestingAccountsResponse { vesting_accounts }) +} + +/// Returns the available amount of vested and yet to be claimed tokens for a specific vesting recipient. +/// +/// * **address** vesting recipient for which to return the available amount of tokens to claim. +fn query_vesting_available_amount(deps: Deps, env: Env, address: String) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + let config = CONFIG.load(deps.storage)?; + let info = vesting_info(config.extensions.historical).load(deps.storage, address)?; + let available_amount = compute_available_amount(env.block.time.seconds(), &info)?; + Ok(available_amount) +} + +/// Manages contract migration. +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Response::default()) +} + +fn is_sender_whitelisted(store: &mut dyn Storage, config: &Config, sender: &Addr) -> bool { + if *sender == config.owner { + return true; + } + if VESTING_MANAGERS.has(store, sender.clone()) { + return true; + } + false +} + +/// Asserts the validity of a list of vesting schedules. +/// +/// * **addr** receiver of the vested tokens. +/// +/// * **vesting_schedules** vesting schedules to validate. +fn assert_vesting_schedules( + addr: &Addr, + vesting_schedules: &[VestingSchedule], +) -> Result<(), ContractError> { + for sch in vesting_schedules { + if let Some(end_point) = &sch.end_point { + if !(sch.start_point.time < end_point.time && sch.start_point.amount < end_point.amount) + { + return Err(ContractError::VestingScheduleError(addr.to_string())); + } + } + } + + Ok(()) +} + +/// Computes the amount of vested and yet unclaimed tokens for a specific vesting recipient. +/// Returns the computed amount if the operation is successful. +/// +/// * **current_time** timestamp from which to start querying for vesting schedules. +/// Schedules that started later than current_time will be omitted. +/// +/// * **vesting_info** vesting schedules for which to compute the amount of tokens +/// that are vested and can be claimed by the recipient. +fn compute_available_amount(current_time: u64, vesting_info: &VestingInfo) -> StdResult { + let mut available_amount: Uint128 = Uint128::zero(); + for sch in &vesting_info.schedules { + if sch.start_point.time > current_time { + continue; + } + + available_amount = available_amount.checked_add(sch.start_point.amount)?; + + if let Some(end_point) = &sch.end_point { + let passed_time = current_time.min(end_point.time) - sch.start_point.time; + let time_period = end_point.time - sch.start_point.time; + if passed_time != 0 && time_period != 0 { + let release_amount = Uint128::from(passed_time).multiply_ratio( + end_point.amount.checked_sub(sch.start_point.amount)?, + time_period, + ); + available_amount = available_amount.checked_add(release_amount)?; + } + } + } + + available_amount + .checked_sub(vesting_info.released_amount) + .map_err(StdError::from) +} diff --git a/packages/vesting-base-pcl/src/lib.rs b/packages/vesting-base-pcl/src/lib.rs new file mode 100644 index 00000000..2de7b374 --- /dev/null +++ b/packages/vesting-base-pcl/src/lib.rs @@ -0,0 +1,13 @@ +pub mod builder; +pub mod error; +pub mod handlers; +pub mod msg; +pub mod state; +pub mod types; + +pub(crate) mod ext_historical; +pub(crate) mod ext_managed; +pub(crate) mod ext_with_managers; + +#[cfg(test)] +mod testing; diff --git a/packages/vesting-base-pcl/src/msg.rs b/packages/vesting-base-pcl/src/msg.rs new file mode 100644 index 00000000..f3c16607 --- /dev/null +++ b/packages/vesting-base-pcl/src/msg.rs @@ -0,0 +1,160 @@ +use crate::types::{ + Config, OrderBy, VestingAccount, VestingAccountResponse, VestingAccountsResponse, VestingState, +}; +use astroport::asset::AssetInfo; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Binary, Uint128}; +use cw20::Cw20ReceiveMsg; + +/// This structure describes the execute messages available in a vesting contract. +#[cw_serde] +pub enum ExecuteMsg { + /// Claim claims vested tokens and sends them to a recipient + Claim { + /// The address that receives the vested tokens + recipient: Option, + /// The amount of tokens to claim + amount: Option, + }, + /// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template + Receive(Cw20ReceiveMsg), + /// RegisterVestingAccounts registers vesting targets/accounts + RegisterVestingAccounts { + vesting_accounts: Vec, + }, + /// Creates a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + ProposeNewOwner { + /// The newly proposed owner + owner: String, + /// The validity period of the offer to change the owner + expires_in: u64, + }, + /// Removes a request to change contract ownership + /// ## Executor + /// Only the current owner can execute this + DropOwnershipProposal {}, + /// Claims contract ownership + /// ## Executor + /// Only the newly proposed owner can execute this + ClaimOwnership {}, + /// Sets vesting token + /// ## Executor + /// Only the current owner or token info manager can execute this + SetVestingToken { vesting_token: AssetInfo }, + /// Contains messages associated with the managed extension for vesting contracts. + ManagedExtension { msg: ExecuteMsgManaged }, + /// Contains messages associated with the with_managers extension for vesting contracts. + WithManagersExtension { msg: ExecuteMsgWithManagers }, + /// Contains messages associated with the historical extension for vesting contracts. + HistoricalExtension { msg: ExecuteMsgHistorical }, +} + +/// This structure describes the execute messages available in a managed vesting contract. +#[cw_serde] +pub enum ExecuteMsgManaged { + /// Removes vesting targets/accounts. + /// ## Executor + /// Only the current owner can execute this + RemoveVestingAccounts { + vesting_accounts: Vec, + /// Specifies the account that will receive the funds taken from the vesting accounts. + clawback_account: String, + }, +} + +/// This structure describes the execute messages available in a with_managers vesting contract. +#[cw_serde] +pub enum ExecuteMsgWithManagers { + /// Adds vesting managers + /// ## Executor + /// Only the current owner can execute this + AddVestingManagers { managers: Vec }, + /// Removes vesting managers + /// ## Executor + /// Only the current owner can execute this + RemoveVestingManagers { managers: Vec }, +} + +/// This structure describes the execute messages available in a historical vesting contract. +#[cw_serde] +pub enum ExecuteMsgHistorical {} + +/// This structure describes the query messages available in a vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsg { + /// Returns the configuration for the contract using a [`ConfigResponse`] object. + #[returns(Config)] + Config {}, + /// Returns information about an address vesting tokens using a [`VestingAccountResponse`] object. + #[returns(VestingAccountResponse)] + VestingAccount { address: String }, + /// Returns a list of addresses that are vesting tokens using a [`VestingAccountsResponse`] object. + #[returns(VestingAccountsResponse)] + VestingAccounts { + start_after: Option, + limit: Option, + order_by: Option, + }, + /// Returns the total unvested amount of tokens for a specific address. + #[returns(Uint128)] + AvailableAmount { address: String }, + /// Timestamp returns the current timestamp + #[returns(u64)] + Timestamp {}, + /// VestingState returns the current vesting state. + #[returns(VestingState)] + VestingState {}, + /// Contains messages associated with the managed extension for vesting contracts. + #[returns(Binary)] + ManagedExtension { msg: QueryMsgManaged }, + /// Contains messages associated with the with_managers extension for vesting contracts. + #[returns(QueryMsgWithManagers)] + WithManagersExtension { msg: QueryMsgWithManagers }, + /// Contains messages associated with the historical extension for vesting contracts. + #[returns(QueryMsgHistorical)] + HistoricalExtension { msg: QueryMsgHistorical }, +} + +/// This structure describes the query messages available in a managed vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgManaged {} + +/// This structure describes the query messages available in a with_managers vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgWithManagers { + /// Returns list of vesting managers + /// (the persons who are able to add/remove vesting schedules) + #[returns(Vec)] + VestingManagers {}, +} + +/// This structure describes the query messages available in a historical vesting contract. +#[cw_serde] +#[derive(QueryResponses)] +pub enum QueryMsgHistorical { + /// Returns the total unclaimed amount of tokens for a specific address at certain height. + #[returns(Uint128)] + UnclaimedAmountAtHeight { address: String, height: u64 }, + /// Returns the total unclaimed amount of tokens for all the users at certain height. + #[returns(Uint128)] + UnclaimedTotalAmountAtHeight { height: u64 }, +} + +/// This structure describes a migration message. +/// We currently take no arguments for migrations. +#[cw_serde] +pub struct MigrateMsg {} + +/// This structure describes a CW20 hook message. +#[cw_serde] +pub enum Cw20HookMsg { + /// RegisterVestingAccounts registers vesting targets/accounts + RegisterVestingAccounts { + vesting_accounts: Vec, + }, +} diff --git a/packages/vesting-base-pcl/src/state.rs b/packages/vesting-base-pcl/src/state.rs new file mode 100644 index 00000000..978fd8c6 --- /dev/null +++ b/packages/vesting-base-pcl/src/state.rs @@ -0,0 +1,159 @@ +use crate::types::{Config, OrderBy, VestingInfo, VestingState}; +use astroport::common::OwnershipProposal; +use cosmwasm_std::{Addr, Deps, StdResult}; +use cw_storage_plus::{Bound, Item, Map, SnapshotItem, SnapshotMap, Strategy}; + +pub(crate) const CONFIG: Item = Item::new("config"); +pub(crate) const OWNERSHIP_PROPOSAL: Item = Item::new("ownership_proposal"); +pub(crate) const VESTING_MANAGERS: Map = Map::new("vesting_managers"); +pub(crate) const VESTING_STATE: SnapshotItem = SnapshotItem::new( + "vesting_state", + "vesting_state__checkpoints", + "vesting_state__changelog", + Strategy::Never, +); +pub(crate) const VESTING_INFO: SnapshotMap = SnapshotMap::new( + "vesting_info", + "vesting_info__checkpoints", + "vesting_info__changelog", + Strategy::Never, +); +pub(crate) const VESTING_STATE_HISTORICAL: SnapshotItem = SnapshotItem::new( + "vesting_state", + "vesting_state__checkpoints", + "vesting_state__changelog", + Strategy::EveryBlock, +); +pub(crate) const VESTING_INFO_HISTORICAL: SnapshotMap = SnapshotMap::new( + "vesting_info", + "vesting_info__checkpoints", + "vesting_info__changelog", + Strategy::EveryBlock, +); + +pub(crate) fn vesting_state(historical: bool) -> SnapshotItem<'static, VestingState> { + if historical { + return VESTING_STATE_HISTORICAL; + } + VESTING_STATE +} + +pub(crate) fn vesting_info(historical: bool) -> SnapshotMap<'static, Addr, VestingInfo> { + if historical { + return VESTING_INFO_HISTORICAL; + } + VESTING_INFO +} + +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +/// Returns an empty vector if it does not find data, otherwise returns a vector that +/// contains objects of type [`VESTING_INFO`]. +/// ## Params +/// +/// * **start_after** index from which to start reading vesting schedules. +/// +/// * **limit** amount of vesting schedules to read. +/// +/// * **order_by** whether results should be returned in an ascending or descending order. +pub(crate) fn read_vesting_infos( + deps: Deps, + start_after: Option, + limit: Option, + order_by: Option, +) -> StdResult> { + let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let start_after = start_after.map(Bound::exclusive); + + let (start, end) = match &order_by { + Some(OrderBy::Asc) => (start_after, None), + _ => (None, start_after), + }; + + let info: Vec<(Addr, VestingInfo)> = VESTING_INFO + .range( + deps.storage, + start, + end, + order_by.unwrap_or(OrderBy::Desc).into(), + ) + .take(limit) + .filter_map(|v| v.ok()) + .collect(); + + Ok(info) +} + +#[cfg(test)] +mod testing { + use super::*; + + #[test] + fn read_vesting_infos_as_expected() { + use cosmwasm_std::{testing::mock_dependencies, Uint128}; + let mut deps = mock_dependencies(); + let historical = false; + + let vi_mock = VestingInfo { + released_amount: Uint128::zero(), + schedules: vec![], + }; + + for i in 1..5 { + let key = Addr::unchecked(format! {"address{}", i}); + + vesting_info(historical) + .save(&mut deps.storage, key, &vi_mock, 1) + .unwrap(); + } + + let res = read_vesting_infos( + deps.as_ref(), + Some(Addr::unchecked("address2")), + None, + Some(OrderBy::Asc), + ) + .unwrap(); + assert_eq!( + res, + vec![ + (Addr::unchecked("address3"), vi_mock.clone()), + (Addr::unchecked("address4"), vi_mock.clone()), + ] + ); + + let res = read_vesting_infos( + deps.as_ref(), + Some(Addr::unchecked("address2")), + Some(1), + Some(OrderBy::Asc), + ) + .unwrap(); + assert_eq!(res, vec![(Addr::unchecked("address3"), vi_mock.clone())]); + + let res = read_vesting_infos( + deps.as_ref(), + Some(Addr::unchecked("address3")), + None, + Some(OrderBy::Desc), + ) + .unwrap(); + assert_eq!( + res, + vec![ + (Addr::unchecked("address2"), vi_mock.clone()), + (Addr::unchecked("address1"), vi_mock.clone()), + ] + ); + + let res = read_vesting_infos( + deps.as_ref(), + Some(Addr::unchecked("address3")), + Some(1), + Some(OrderBy::Desc), + ) + .unwrap(); + assert_eq!(res, vec![(Addr::unchecked("address2"), vi_mock.clone())]); + } +} diff --git a/packages/vesting-base-pcl/src/testing.rs b/packages/vesting-base-pcl/src/testing.rs new file mode 100644 index 00000000..3d11af41 --- /dev/null +++ b/packages/vesting-base-pcl/src/testing.rs @@ -0,0 +1,484 @@ +use crate::builder::VestingBaseBuilder; +use crate::error::{ext_unsupported_err, ContractError}; +use crate::handlers::{execute, query}; +use crate::msg::{ + ExecuteMsg, ExecuteMsgManaged, QueryMsg, QueryMsgHistorical, QueryMsgWithManagers, +}; +use crate::types::{Config, Extensions}; +use astroport::asset::token_asset_info; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{from_binary, Addr}; + +#[test] +fn set_vesting_token() { + let mut deps = mock_dependencies(); + let owner = String::from("owner"); + let token_info_manager = "token_info_manager"; + let env = mock_env(); + VestingBaseBuilder::default() + .build(deps.as_mut(), owner, String::from(token_info_manager)) + .unwrap(); + + // check initialisation + assert_eq!( + from_binary::(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()) + .unwrap(), + Config { + owner: Addr::unchecked("owner"), + token_info_manager: Addr::unchecked(token_info_manager), + vesting_token: None, + extensions: Extensions { + historical: false, + managed: false, + with_managers: false + } + } + ); + + let info = mock_info("stranger", &[]); + // set vesting token by a stranger -> Unauthorized + assert_eq!( + execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::SetVestingToken { + vesting_token: token_asset_info(Addr::unchecked("ntrn_token")), + }, + ) + .unwrap_err(), + ContractError::Unauthorized {}, + ); + + // set vesting token by the manager -> Success + let info = mock_info("token_info_manager", &[]); + execute( + deps.as_mut(), + env.clone(), + info.clone(), + ExecuteMsg::SetVestingToken { + vesting_token: token_asset_info(Addr::unchecked("ntrn_token")), + }, + ) + .unwrap(); + + assert_eq!( + from_binary::(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()) + .unwrap(), + Config { + owner: Addr::unchecked("owner"), + token_info_manager: Addr::unchecked(token_info_manager), + vesting_token: Some(token_asset_info(Addr::unchecked("ntrn_token"))), + extensions: Extensions { + historical: false, + managed: false, + with_managers: false + } + } + ); + + // set vesting token second time by the owner -> VestingTokenAlreadySet + assert_eq!( + execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::SetVestingToken { + vesting_token: token_asset_info(Addr::unchecked("not_a_ntrn_token")), + }, + ) + .unwrap_err(), + ContractError::VestingTokenAlreadySet {}, + ); + + assert_eq!( + from_binary::(&query(deps.as_ref(), env, QueryMsg::Config {}).unwrap()).unwrap(), + Config { + owner: Addr::unchecked("owner"), + token_info_manager: Addr::unchecked(token_info_manager), + vesting_token: Some(token_asset_info(Addr::unchecked("ntrn_token"))), + extensions: Extensions { + historical: false, + managed: false, + with_managers: false + } + } + ); +} + +#[test] +fn proper_building_standard() { + let mut deps = mock_dependencies(); + let owner = String::from("owner"); + let token_info_manager = "token_info_manager"; + let env = mock_env(); + let info = mock_info("owner", &[]); + VestingBaseBuilder::default() + .build(deps.as_mut(), owner, String::from(token_info_manager)) + .unwrap(); + + // check initialisation + assert_eq!( + from_binary::(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()) + .unwrap(), + Config { + owner: Addr::unchecked("owner"), + token_info_manager: Addr::unchecked(token_info_manager), + vesting_token: None, + extensions: Extensions { + historical: false, + managed: false, + with_managers: false + } + } + ); + + // make sure with_managers extension is not enabled + assert_eq!( + query( + deps.as_ref(), + env.clone(), + QueryMsg::WithManagersExtension { + msg: QueryMsgWithManagers::VestingManagers {} + } + ) + .unwrap_err(), + ext_unsupported_err("with_managers") + ); + + // make sure historical extension is not enabled + assert_eq!( + query( + deps.as_ref(), + env.clone(), + QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedTotalAmountAtHeight { height: 1000u64 } + } + ) + .unwrap_err(), + ext_unsupported_err("historical") + ); + + // make sure managed extension is not enabled + assert_eq!( + execute( + deps.as_mut(), + env, + info, + ExecuteMsg::ManagedExtension { + msg: ExecuteMsgManaged::RemoveVestingAccounts { + vesting_accounts: vec![], + clawback_account: String::from("clawback") + } + }, + ) + .unwrap_err(), + ext_unsupported_err("managed").into() + ); +} + +#[test] +fn proper_building_managers() { + let mut deps = mock_dependencies(); + let owner = String::from("owner"); + let token_info_manager = "token_info_manager"; + let env = mock_env(); + let info = mock_info("owner", &[]); + let vesting_managers = vec!["manager1".to_string(), "manager2".to_string()]; + VestingBaseBuilder::default() + .with_managers(vesting_managers.clone()) + .build(deps.as_mut(), owner, String::from(token_info_manager)) + .unwrap(); + + // check initialisation + assert_eq!( + from_binary::(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()) + .unwrap(), + Config { + owner: Addr::unchecked("owner"), + token_info_manager: Addr::unchecked(token_info_manager), + vesting_token: None, + extensions: Extensions { + historical: false, + managed: false, + with_managers: true + } + } + ); + + // make sure with_managers extension is enabled + assert_eq!( + from_binary::>( + &query( + deps.as_ref(), + env.clone(), + QueryMsg::WithManagersExtension { + msg: QueryMsgWithManagers::VestingManagers {}, + }, + ) + .unwrap() + ) + .unwrap(), + vesting_managers + ); + + // make sure historical extension is not enabled + assert_eq!( + query( + deps.as_ref(), + env.clone(), + QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedTotalAmountAtHeight { height: 1000u64 } + } + ) + .unwrap_err(), + ext_unsupported_err("historical") + ); + + // make sure managed extension is not enabled + assert_eq!( + execute( + deps.as_mut(), + env, + info, + ExecuteMsg::ManagedExtension { + msg: ExecuteMsgManaged::RemoveVestingAccounts { + vesting_accounts: vec![], + clawback_account: String::from("clawback"), + }, + }, + ) + .unwrap_err(), + ext_unsupported_err("managed").into() + ); +} + +#[test] +fn proper_building_historical() { + let mut deps = mock_dependencies(); + let owner = String::from("owner"); + let token_info_manager = "token_info_manager"; + let env = mock_env(); + let info = mock_info("owner", &[]); + VestingBaseBuilder::default() + .historical() + .build(deps.as_mut(), owner, String::from(token_info_manager)) + .unwrap(); + + // check initialisation + assert_eq!( + from_binary::(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()) + .unwrap(), + Config { + owner: Addr::unchecked("owner"), + token_info_manager: Addr::unchecked(token_info_manager), + vesting_token: None, + extensions: Extensions { + historical: true, + managed: false, + with_managers: false + } + } + ); + + // make sure with_managers extension is not enabled + assert_eq!( + query( + deps.as_ref(), + env.clone(), + QueryMsg::WithManagersExtension { + msg: QueryMsgWithManagers::VestingManagers {} + } + ) + .unwrap_err(), + ext_unsupported_err("with_managers") + ); + + // make sure historical extension is enabled + query( + deps.as_ref(), + env.clone(), + QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedTotalAmountAtHeight { height: 1000u64 }, + }, + ) + .unwrap(); + + // make sure managed extension is not enabled + assert_eq!( + execute( + deps.as_mut(), + env, + info, + ExecuteMsg::ManagedExtension { + msg: ExecuteMsgManaged::RemoveVestingAccounts { + vesting_accounts: vec![], + clawback_account: String::from("clawback") + } + }, + ) + .unwrap_err(), + ext_unsupported_err("managed").into() + ); +} + +#[test] +fn proper_building_managed() { + let mut deps = mock_dependencies(); + let owner = String::from("owner"); + let token_info_manager = "token_info_manager"; + let env = mock_env(); + VestingBaseBuilder::default() + .managed() + .build(deps.as_mut(), owner, String::from(token_info_manager)) + .unwrap(); + + // check initialisation and set vesting token + assert_eq!( + from_binary::(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()) + .unwrap(), + Config { + owner: Addr::unchecked("owner"), + token_info_manager: Addr::unchecked(token_info_manager), + vesting_token: None, + extensions: Extensions { + historical: false, + managed: true, + with_managers: false + } + } + ); + let info = mock_info("token_info_manager", &[]); + execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::SetVestingToken { + vesting_token: token_asset_info(Addr::unchecked("ntrn_token")), + }, + ) + .unwrap(); + + // make sure with_managers extension is not enabled + assert_eq!( + query( + deps.as_ref(), + env.clone(), + QueryMsg::WithManagersExtension { + msg: QueryMsgWithManagers::VestingManagers {} + } + ) + .unwrap_err(), + ext_unsupported_err("with_managers") + ); + + // make sure historical extension is not enabled + assert_eq!( + query( + deps.as_ref(), + env.clone(), + QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedTotalAmountAtHeight { height: 1000u64 } + } + ) + .unwrap_err(), + ext_unsupported_err("historical") + ); + + // make sure managed extension is enabled + let info = mock_info("owner", &[]); + execute( + deps.as_mut(), + env, + info, + ExecuteMsg::ManagedExtension { + msg: ExecuteMsgManaged::RemoveVestingAccounts { + vesting_accounts: vec![], + clawback_account: String::from("clawback"), + }, + }, + ) + .unwrap(); +} + +#[test] +fn proper_building_all_extensions() { + let mut deps = mock_dependencies(); + let owner = String::from("owner"); + let token_info_manager = "token_info_manager"; + let env = mock_env(); + let vesting_managers = vec!["manager1".to_string(), "manager2".to_string()]; + VestingBaseBuilder::default() + .historical() + .managed() + .with_managers(vesting_managers.clone()) + .build(deps.as_mut(), owner, String::from(token_info_manager)) + .unwrap(); + + // check initialisation and set vesting token + assert_eq!( + from_binary::(&query(deps.as_ref(), env.clone(), QueryMsg::Config {}).unwrap()) + .unwrap(), + Config { + owner: Addr::unchecked("owner"), + token_info_manager: Addr::unchecked(token_info_manager), + vesting_token: None, + extensions: Extensions { + historical: true, + managed: true, + with_managers: true + } + } + ); + let info = mock_info("token_info_manager", &[]); + execute( + deps.as_mut(), + env.clone(), + info, + ExecuteMsg::SetVestingToken { + vesting_token: token_asset_info(Addr::unchecked("ntrn_token")), + }, + ) + .unwrap(); + + // make sure with_managers extension is enabled + assert_eq!( + from_binary::>( + &query( + deps.as_ref(), + env.clone(), + QueryMsg::WithManagersExtension { + msg: QueryMsgWithManagers::VestingManagers {}, + }, + ) + .unwrap() + ) + .unwrap(), + vesting_managers + ); + + // make sure historical extension is enabled + query( + deps.as_ref(), + env.clone(), + QueryMsg::HistoricalExtension { + msg: QueryMsgHistorical::UnclaimedTotalAmountAtHeight { height: 1000u64 }, + }, + ) + .unwrap(); + + // make sure managed extension is enabled + let info = mock_info("owner", &[]); + execute( + deps.as_mut(), + env, + info, + ExecuteMsg::ManagedExtension { + msg: ExecuteMsgManaged::RemoveVestingAccounts { + vesting_accounts: vec![], + clawback_account: String::from("clawback"), + }, + }, + ) + .unwrap(); +} diff --git a/packages/vesting-base-pcl/src/types.rs b/packages/vesting-base-pcl/src/types.rs new file mode 100644 index 00000000..f6081025 --- /dev/null +++ b/packages/vesting-base-pcl/src/types.rs @@ -0,0 +1,109 @@ +use astroport::asset::AssetInfo; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Order, Uint128}; + +/// This structure stores the main parameters for the generator vesting contract. +#[cw_serde] +pub struct Config { + /// Address that's allowed to change contract parameters + pub owner: Addr, + /// [`AssetInfo`] of the vested token + pub vesting_token: Option, + /// Address that's allowed to change vesting token + pub token_info_manager: Addr, + /// Contains extensions information of the contract + pub extensions: Extensions, +} + +/// Contains extensions information for the contract. +#[cw_serde] +pub struct Extensions { + /// Whether the historical extension is enabled for the contract. + pub historical: bool, + /// Whether the managed extension is enabled for the contract. + pub managed: bool, + /// Whether the with_managers extension is enabled for the contract. + pub with_managers: bool, +} + +/// This structure stores the accumulated vesting information for all addresses. +#[cw_serde] +#[derive(Default)] +pub struct VestingState { + /// The total amount of tokens granted to the users + pub total_granted: Uint128, + /// The total amount of tokens already claimed + pub total_released: Uint128, +} + +/// This structure stores vesting information for a specific address that is getting tokens. +#[cw_serde] +pub struct VestingAccount { + /// The address that is getting tokens + pub address: String, + /// The vesting schedules targeted at the `address` + pub schedules: Vec, +} + +/// This structure stores parameters for a batch of vesting schedules. +#[cw_serde] +pub struct VestingInfo { + /// The vesting schedules + pub schedules: Vec, + /// The total amount of vested tokens already claimed + pub released_amount: Uint128, +} + +/// This structure stores parameters for a specific vesting schedule +#[cw_serde] +pub struct VestingSchedule { + /// The start date for the vesting schedule + pub start_point: VestingSchedulePoint, + /// The end point for the vesting schedule + pub end_point: Option, +} + +/// This structure stores the parameters used to create a vesting schedule. +#[cw_serde] +pub struct VestingSchedulePoint { + /// The start time for the vesting schedule + pub time: u64, + /// The amount of tokens being vested + pub amount: Uint128, +} + +/// This structure describes a custom struct used to return vesting data about a specific vesting target. +#[cw_serde] +pub struct VestingAccountResponse { + /// The address that's vesting tokens + pub address: Addr, + /// Vesting information + pub info: VestingInfo, +} + +/// This structure describes a custom struct used to return vesting data for multiple vesting targets. +#[cw_serde] +pub struct VestingAccountsResponse { + /// A list of accounts that are vesting tokens + pub vesting_accounts: Vec, +} + +/// This enum describes the types of sorting that can be applied to some piece of data +#[cw_serde] +pub enum OrderBy { + Asc, + Desc, +} + +// We suppress this clippy warning because Order in cosmwasm doesn't implement Debug and +// PartialEq for usage in QueryMsg. We need to use our own OrderBy and convert the result to cosmwasm's Order +#[allow(clippy::from_over_into)] +impl Into for OrderBy { + fn into(self) -> Order { + if self == OrderBy::Asc { + Order::Ascending + } else { + Order::Descending + } + } +}