diff --git a/Cargo.lock b/Cargo.lock index 55b5789a..67563f3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,9 +1089,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd6da19f25979c7270e70fa95ab371ec3b701cd0eefc47667a09785b3c59155" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" dependencies = [ "hermit-abi", "libc", @@ -1690,9 +1690,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "rfc6979" @@ -1716,9 +1716,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.10" +version = "0.36.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe885c3a125aa45213b68cc1472a49880cb5923dc23f522ad2791b882228778" +checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" dependencies = [ "bitflags", "errno", @@ -2144,6 +2144,50 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vesting-base" +version = "1.1.0" +dependencies = [ + "astroport 2.0.0", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw2 0.15.1", + "cw20 0.15.1", + "thiserror", +] + +[[package]] +name = "vesting-lp" +version = "1.1.0" +dependencies = [ + "astroport 2.0.0", + "astroport-token", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw20 0.15.1", + "vesting-base", +] + +[[package]] +name = "vesting-managed" +version = "1.1.0" +dependencies = [ + "astroport 2.0.0", + "astroport-token", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test 0.15.1", + "cw-storage-plus 0.15.1", + "cw-utils 0.15.1", + "cw20 0.15.1", + "vesting-base", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index a7771f11..eff149a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["contracts/auction", "contracts/lockdrop", "contracts/credits", "contracts/cw20-merkle-airdrop", "contracts/price-feed", "contracts/astroport/*"] +members = ["contracts/auction", "contracts/lockdrop", "contracts/credits", "contracts/vesting-lp", "contracts/vesting-managed", "contracts/cw20-merkle-airdrop", "contracts/price-feed", "contracts/astroport/*"] [profile.release] rpath = false diff --git a/contracts/astroport/factory/src/contract.rs b/contracts/astroport/factory/src/contract.rs index e9e95e04..e0d1cedd 100644 --- a/contracts/astroport/factory/src/contract.rs +++ b/contracts/astroport/factory/src/contract.rs @@ -165,14 +165,14 @@ pub fn execute( owner, expires_in, config.owner, - OWNERSHIP_PROPOSAL, + &OWNERSHIP_PROPOSAL, ) .map_err(Into::into) } ExecuteMsg::DropOwnershipProposal {} => { let config = CONFIG.load(deps.storage)?; - drop_ownership_proposal(deps, info, config.owner, OWNERSHIP_PROPOSAL) + drop_ownership_proposal(deps, info, config.owner, &OWNERSHIP_PROPOSAL) .map_err(Into::into) } ExecuteMsg::ClaimOwnership {} => { @@ -183,7 +183,7 @@ pub fn execute( PAIRS_TO_MIGRATE.save(deps.storage, &pairs)?; - claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + claim_ownership(deps, info, env, &OWNERSHIP_PROPOSAL, |deps, new_owner| { CONFIG .update::<_, StdError>(deps.storage, |mut v| { v.owner = new_owner; diff --git a/contracts/astroport/pair_stable/src/contract.rs b/contracts/astroport/pair_stable/src/contract.rs index 5faf0061..f783bb73 100644 --- a/contracts/astroport/pair_stable/src/contract.rs +++ b/contracts/astroport/pair_stable/src/contract.rs @@ -242,7 +242,7 @@ pub fn execute( owner, expires_in, config.owner.unwrap_or(factory_config.owner), - OWNERSHIP_PROPOSAL, + &OWNERSHIP_PROPOSAL, ) .map_err(|e| e.into()) } @@ -254,12 +254,12 @@ pub fn execute( deps, info, config.owner.unwrap_or(factory_config.owner), - OWNERSHIP_PROPOSAL, + &OWNERSHIP_PROPOSAL, ) .map_err(|e| e.into()) } ExecuteMsg::ClaimOwnership {} => { - claim_ownership(deps, info, env, OWNERSHIP_PROPOSAL, |deps, new_owner| { + claim_ownership(deps, info, env, &OWNERSHIP_PROPOSAL, |deps, new_owner| { CONFIG.update::<_, StdError>(deps.storage, |mut config| { config.owner = Some(new_owner); Ok(config) diff --git a/contracts/lockdrop/schema/execute_msg.json b/contracts/lockdrop/schema/execute_msg.json index edb8bbf9..71cc1b69 100644 --- a/contracts/lockdrop/schema/execute_msg.json +++ b/contracts/lockdrop/schema/execute_msg.json @@ -207,6 +207,31 @@ } }, "additionalProperties": false + }, + { + "description": "Sets pool info", + "type": "object", + "required": [ + "set_pool_info" + ], + "properties": { + "set_pool_info": { + "type": "object", + "required": [ + "pool_info", + "pool_type" + ], + "properties": { + "pool_info": { + "$ref": "#/definitions/PoolInfo" + }, + "pool_type": { + "$ref": "#/definitions/PoolType" + } + } + } + }, + "additionalProperties": false } ], "definitions": { @@ -383,6 +408,64 @@ } } }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "PoolInfo": { + "type": "object", + "required": [ + "amount_in_lockups", + "generator_ntrn_per_share", + "generator_proxy_per_share", + "incentives_share", + "is_staked", + "lp_token", + "weighted_amount" + ], + "properties": { + "amount_in_lockups": { + "$ref": "#/definitions/Uint128" + }, + "generator_ntrn_per_share": { + "description": "Ratio of Generator NTRN rewards accured to astroport pool share", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "generator_proxy_per_share": { + "description": "Ratio of Generator Proxy rewards accured to astroport pool share", + "allOf": [ + { + "$ref": "#/definitions/RestrictedVector_for_AssetInfo_and_Decimal" + } + ] + }, + "incentives_share": { + "description": "Share of total NTRN incentives allocated to this pool", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_staked": { + "description": "Boolean value indicating if the LP Tokens are staked with the Generator contract or not", + "type": "boolean" + }, + "lp_token": { + "$ref": "#/definitions/Addr" + }, + "weighted_amount": { + "description": "Weighted LP Token balance used to calculate NTRN rewards a particular user can claim", + "allOf": [ + { + "$ref": "#/definitions/Uint256" + } + ] + } + } + }, "PoolType": { "type": "string", "enum": [ @@ -390,10 +473,31 @@ "ATOM" ] }, + "RestrictedVector_for_AssetInfo_and_Decimal": { + "description": "Vec wrapper for internal use. Some business logic relies on an order of this vector, thus it is forbidden to sort it or remove elements. New values can be added using .update() ONLY.", + "type": "array", + "items": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/AssetInfo" + }, + { + "$ref": "#/definitions/Decimal" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, "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" }, + "Uint256": { + "description": "An implementation of u256 that is using strings for JSON encoding/decoding, such that the full u256 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 out of primitive uint types or `new` to provide big endian bytes:\n\n``` # use cosmwasm_std::Uint256; let a = Uint256::from(258u128); let b = Uint256::new([ 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 0u8, 1u8, 2u8, ]); assert_eq!(a, b); ```", + "type": "string" + }, "UpdateConfigMsg": { "type": "object", "properties": { diff --git a/contracts/lockdrop/schema/instantiate_msg.json b/contracts/lockdrop/schema/instantiate_msg.json index 85fa426d..1ffe02ce 100644 --- a/contracts/lockdrop/schema/instantiate_msg.json +++ b/contracts/lockdrop/schema/instantiate_msg.json @@ -3,7 +3,6 @@ "title": "InstantiateMsg", "type": "object", "required": [ - "atom_token", "auction_contract", "credits_contract", "init_timestamp", @@ -12,14 +11,9 @@ "max_lock_duration", "max_positions_per_user", "min_lock_duration", - "usdc_token", "withdrawal_window" ], "properties": { - "atom_token": { - "description": "Address of ATOM/NTRN token", - "type": "string" - }, "auction_contract": { "description": "Auction contract address", "type": "string" @@ -72,10 +66,6 @@ "null" ] }, - "usdc_token": { - "description": "Address of USDC/NTRN token", - "type": "string" - }, "withdrawal_window": { "description": "Withdrawal Window Length :: Post the deposit window", "type": "integer", diff --git a/contracts/vesting-lp/.cargo/config b/contracts/vesting-lp/.cargo/config new file mode 100644 index 00000000..a79b8fdb --- /dev/null +++ b/contracts/vesting-lp/.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_schema" diff --git a/contracts/vesting-lp/Cargo.toml b/contracts/vesting-lp/Cargo.toml new file mode 100644 index 00000000..17dfc53c --- /dev/null +++ b/contracts/vesting-lp/Cargo.toml @@ -0,0 +1,26 @@ +[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] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +vesting-base = {path = "../../packages/vesting-base"} +astroport = { path = "../../packages/astroport", default-features = false } +cosmwasm-schema = { version = "1.1", default-features = false } +cosmwasm-std = { version = "1.1" } +cw-storage-plus = "0.15" + +[dev-dependencies] +cw-multi-test = "0.15" +astroport-token = {path = "../astroport/token"} +cw20 = { version = "0.15" } +cw-utils = "0.15" \ No newline at end of file diff --git a/contracts/vesting-lp/examples/vesting_schema.rs b/contracts/vesting-lp/examples/vesting_schema.rs new file mode 100644 index 00000000..b1cdebf1 --- /dev/null +++ b/contracts/vesting-lp/examples/vesting_schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use astroport::vesting::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/vesting-lp/schema/raw/execute.json b/contracts/vesting-lp/schema/raw/execute.json new file mode 100644 index 00000000..e5971e64 --- /dev/null +++ b/contracts/vesting-lp/schema/raw/execute.json @@ -0,0 +1,294 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the 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": "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 + } + ], + "definitions": { + "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 + }, + "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/schema/raw/instantiate.json b/contracts/vesting-lp/schema/raw/instantiate.json new file mode 100644 index 00000000..41008292 --- /dev/null +++ b/contracts/vesting-lp/schema/raw/instantiate.json @@ -0,0 +1,88 @@ +{ + "$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", + "vesting_managers", + "vesting_token" + ], + "properties": { + "owner": { + "description": "Address allowed to change contract parameters", + "type": "string" + }, + "vesting_managers": { + "description": "Initial list of whitelisted vesting managers", + "type": "array", + "items": { + "type": "string" + } + }, + "vesting_token": { + "description": "[`AssetInfo`] of the token that's being vested", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "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 + } + ] + } + } +} diff --git a/contracts/vesting-lp/schema/raw/migrate.json b/contracts/vesting-lp/schema/raw/migrate.json new file mode 100644 index 00000000..1b9dcecf --- /dev/null +++ b/contracts/vesting-lp/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/schema/raw/query.json b/contracts/vesting-lp/schema/raw/query.json new file mode 100644 index 00000000..3a89807a --- /dev/null +++ b/contracts/vesting-lp/schema/raw/query.json @@ -0,0 +1,157 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the 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": "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 + } + ], + "definitions": { + "OrderBy": { + "description": "This enum describes the types of sorting that can be applied to some piece of data", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } +} diff --git a/contracts/vesting-lp/schema/raw/response_to_available_amount.json b/contracts/vesting-lp/schema/raw/response_to_available_amount.json new file mode 100644 index 00000000..25b73e8f --- /dev/null +++ b/contracts/vesting-lp/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/schema/raw/response_to_config.json b/contracts/vesting-lp/schema/raw/response_to_config.json new file mode 100644 index 00000000..9ab74329 --- /dev/null +++ b/contracts/vesting-lp/schema/raw/response_to_config.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This structure describes a custom struct used to return the contract configuration.", + "type": "object", + "required": [ + "owner", + "vesting_token" + ], + "properties": { + "owner": { + "description": "Address allowed to set contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "vesting_token": { + "description": "[`AssetInfo`] of the token that's being vested", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "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 + } + ] + } + } +} diff --git a/contracts/vesting-lp/schema/raw/response_to_timestamp.json b/contracts/vesting-lp/schema/raw/response_to_timestamp.json new file mode 100644 index 00000000..7b729a7b --- /dev/null +++ b/contracts/vesting-lp/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/schema/raw/response_to_vesting_account.json b/contracts/vesting-lp/schema/raw/response_to_vesting_account.json new file mode 100644 index 00000000..499ca4de --- /dev/null +++ b/contracts/vesting-lp/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 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/schema/raw/response_to_vesting_accounts.json b/contracts/vesting-lp/schema/raw/response_to_vesting_accounts.json new file mode 100644 index 00000000..5a0dfaf7 --- /dev/null +++ b/contracts/vesting-lp/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 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/schema/raw/response_to_vesting_managers.json b/contracts/vesting-lp/schema/raw/response_to_vesting_managers.json new file mode 100644 index 00000000..bf06b01c --- /dev/null +++ b/contracts/vesting-lp/schema/raw/response_to_vesting_managers.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } +} diff --git a/contracts/vesting-lp/schema/vesting-lp.json b/contracts/vesting-lp/schema/vesting-lp.json new file mode 100644 index 00000000..493f9af4 --- /dev/null +++ b/contracts/vesting-lp/schema/vesting-lp.json @@ -0,0 +1,955 @@ +{ + "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", + "vesting_managers", + "vesting_token" + ], + "properties": { + "owner": { + "description": "Address allowed to change contract parameters", + "type": "string" + }, + "vesting_managers": { + "description": "Initial list of whitelisted vesting managers", + "type": "array", + "items": { + "type": "string" + } + }, + "vesting_token": { + "description": "[`AssetInfo`] of the token that's being vested", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "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 + } + ] + } + } + }, + "execute": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the 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": "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 + } + ], + "definitions": { + "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 + }, + "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 the 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": "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 + } + ], + "definitions": { + "OrderBy": { + "description": "This enum describes the types of sorting that can be applied to some piece of data", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } + }, + "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": "ConfigResponse", + "description": "This structure describes a custom struct used to return the contract configuration.", + "type": "object", + "required": [ + "owner", + "vesting_token" + ], + "properties": { + "owner": { + "description": "Address allowed to set contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "vesting_token": { + "description": "[`AssetInfo`] of the token that's being vested", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "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 + } + ] + } + } + }, + "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 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 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_managers": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Addr", + "type": "array", + "items": { + "$ref": "#/definitions/Addr" + }, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + } + } + }, + "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" + } + } + } + } +} diff --git a/contracts/vesting-lp/src/contract.rs b/contracts/vesting-lp/src/contract.rs new file mode 100644 index 00000000..4d207dbf --- /dev/null +++ b/contracts/vesting-lp/src/contract.rs @@ -0,0 +1,136 @@ +use crate::msg::QueryMsg; +use astroport::vesting::{ExecuteMsg, InstantiateMsg, QueryMsg as QueryBase, VestingInfo}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, + Uint128, +}; +use cw_storage_plus::Strategy; +use vesting_base::{error::ContractError, state::BaseVesting}; + +/// Creates a new contract with the specified parameters packed in the `msg` variable. +/// Returns a [`Response`] with the specified attributes if the operation was successful, or a [`ContractError`] if the contract was not created +/// ## Params +/// * **deps** is an object of type [`DepsMut`]. +/// +/// * **env** is an object of type [`Env`]. +/// +/// * **info** is an object of type [`MessageInfo`]. +/// +/// * **msg** is a message of type [`InstantiateMsg`] which contains the parameters used for creating the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let vest_app = BaseVesting::new(Strategy::EveryBlock); + vest_app.instantiate(deps, env, info, msg) +} + +/// 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 { + let vest_app = BaseVesting::new(Strategy::EveryBlock); + vest_app.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 { + let vest_app = BaseVesting::new(Strategy::EveryBlock); + match msg { + QueryMsg::Config {} => vest_app.query(deps, env, QueryBase::Config {}), + QueryMsg::VestingAccount { address } => { + vest_app.query(deps, env, QueryBase::VestingAccount { address }) + } + QueryMsg::VestingAccounts { + start_after, + limit, + order_by, + } => vest_app.query( + deps, + env, + QueryBase::VestingAccounts { + start_after, + limit, + order_by, + }, + ), + QueryMsg::AvailableAmount { address } => { + vest_app.query(deps, env, QueryBase::AvailableAmount { address }) + } + QueryMsg::Timestamp {} => vest_app.query(deps, env, QueryBase::Timestamp {}), + QueryMsg::VestingManagers {} => vest_app.query(deps, env, QueryBase::VestingManagers {}), + QueryMsg::UnclaimedAmountAtHeight { address, height } => Ok(to_binary( + &query_unclaimed_amount_at_height(&vest_app, deps, address, height)?, + )?), + QueryMsg::UnclaimedTotalAmountAtHeight { height } => Ok(to_binary( + &query_total_unclaimed_amount_at_height(&vest_app, 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 +pub fn query_unclaimed_amount_at_height( + base_app: &BaseVesting, + deps: Deps, + address: String, + height: u64, +) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + let maybe_info = base_app + .vesting_info + .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 +pub fn query_total_unclaimed_amount_at_height( + base_app: &BaseVesting, + deps: Deps, + height: u64, +) -> StdResult { + let maybe_state = base_app + .vesting_state + .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/contracts/vesting-lp/src/lib.rs b/contracts/vesting-lp/src/lib.rs new file mode 100644 index 00000000..08d6d688 --- /dev/null +++ b/contracts/vesting-lp/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +pub mod msg; + +#[cfg(test)] +mod tests; diff --git a/contracts/vesting-lp/src/msg.rs b/contracts/vesting-lp/src/msg.rs new file mode 100644 index 00000000..0c0fc116 --- /dev/null +++ b/contracts/vesting-lp/src/msg.rs @@ -0,0 +1,40 @@ +use astroport::vesting::{ + ConfigResponse, OrderBy, VestingAccountResponse, VestingAccountsResponse, +}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::Uint128; + +/// This structure describes the query messages available in the 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 {}, + /// Returns list of vesting managers + /// (the persons who are able to add/remove vesting schedules) + #[returns(Vec)] + VestingManagers {}, + /// 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 }, +} diff --git a/contracts/vesting-lp/src/tests/integration.rs b/contracts/vesting-lp/src/tests/integration.rs new file mode 100644 index 00000000..6724c8a4 --- /dev/null +++ b/contracts/vesting-lp/src/tests/integration.rs @@ -0,0 +1,1248 @@ +use crate::msg::QueryMsg::{UnclaimedAmountAtHeight, UnclaimedTotalAmountAtHeight}; +use astroport::asset::{native_asset_info, token_asset_info}; +use astroport::querier::query_balance; +use astroport::vesting::{QueryMsg, VestingAccountResponse}; +use astroport::{ + token::InstantiateMsg as TokenInstantiateMsg, + vesting::{ + Cw20HookMsg, ExecuteMsg, InstantiateMsg, VestingAccount, VestingSchedule, + VestingSchedulePoint, + }, +}; +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::state::Config; + +const OWNER1: &str = "owner1"; +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 = 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 = 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 = 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 = 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::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::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::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 vesting_code_id = app.store_code(vesting_contract); + + let init_msg = InstantiateMsg { + owner: OWNER1.to_string(), + vesting_token: token_asset_info(cw20_token_instance.clone()), + vesting_managers: vec![], + }; + + let vesting_instance = app + .instantiate_contract( + vesting_code_id, + owner.clone(), + &init_msg, + &[], + "Vesting", + None, + ) + .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.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 vesting_code_id = app.store_code(vesting_contract); + + let init_msg = InstantiateMsg { + owner: OWNER1.to_string(), + vesting_token: native_asset_info(VESTING_TOKEN.to_string()), + vesting_managers: vec![], + }; + + app.instantiate_contract(vesting_code_id, owner, &init_msg, &[], "Vesting", None) + .unwrap() +} + +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/src/tests/mod.rs b/contracts/vesting-lp/src/tests/mod.rs new file mode 100644 index 00000000..6d3bbe60 --- /dev/null +++ b/contracts/vesting-lp/src/tests/mod.rs @@ -0,0 +1 @@ +mod integration; diff --git a/contracts/vesting-managed/.cargo/config b/contracts/vesting-managed/.cargo/config new file mode 100644 index 00000000..a79b8fdb --- /dev/null +++ b/contracts/vesting-managed/.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_schema" diff --git a/contracts/vesting-managed/Cargo.toml b/contracts/vesting-managed/Cargo.toml new file mode 100644 index 00000000..fb387899 --- /dev/null +++ b/contracts/vesting-managed/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "vesting-managed" +version = "1.1.0" +authors = ["andrei.z@p2p.org"] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# use library feature to disable all init/handle/query exports +library = [] + +[dependencies] +vesting-base = {path = "../../packages/vesting-base"} +astroport = { path = "../../packages/astroport", default-features = false } +cosmwasm-schema = { version = "1.1", default-features = false } +cosmwasm-std = { version = "1.1" } +cw-storage-plus = "0.15" +cw-utils = "0.15" +cw20 = { version = "0.15" } + +[dev-dependencies] +cw-multi-test = "0.15" +astroport-token = {path = "../astroport/token"} +cw20 = { version = "0.15" } +cw-utils = "0.15" \ No newline at end of file diff --git a/contracts/vesting-managed/examples/vesting_schema.rs b/contracts/vesting-managed/examples/vesting_schema.rs new file mode 100644 index 00000000..b1cdebf1 --- /dev/null +++ b/contracts/vesting-managed/examples/vesting_schema.rs @@ -0,0 +1,12 @@ +use cosmwasm_schema::write_api; + +use astroport::vesting::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +fn main() { + write_api! { + instantiate: InstantiateMsg, + query: QueryMsg, + execute: ExecuteMsg, + migrate: MigrateMsg + } +} diff --git a/contracts/vesting-managed/schema/raw/execute.json b/contracts/vesting-managed/schema/raw/execute.json new file mode 100644 index 00000000..e5971e64 --- /dev/null +++ b/contracts/vesting-managed/schema/raw/execute.json @@ -0,0 +1,294 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ExecuteMsg", + "description": "This structure describes the execute messages available in the 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": "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 + } + ], + "definitions": { + "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 + }, + "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-managed/schema/raw/instantiate.json b/contracts/vesting-managed/schema/raw/instantiate.json new file mode 100644 index 00000000..41008292 --- /dev/null +++ b/contracts/vesting-managed/schema/raw/instantiate.json @@ -0,0 +1,88 @@ +{ + "$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", + "vesting_managers", + "vesting_token" + ], + "properties": { + "owner": { + "description": "Address allowed to change contract parameters", + "type": "string" + }, + "vesting_managers": { + "description": "Initial list of whitelisted vesting managers", + "type": "array", + "items": { + "type": "string" + } + }, + "vesting_token": { + "description": "[`AssetInfo`] of the token that's being vested", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "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 + } + ] + } + } +} diff --git a/contracts/vesting-managed/schema/raw/migrate.json b/contracts/vesting-managed/schema/raw/migrate.json new file mode 100644 index 00000000..1b9dcecf --- /dev/null +++ b/contracts/vesting-managed/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-managed/schema/raw/query.json b/contracts/vesting-managed/schema/raw/query.json new file mode 100644 index 00000000..3a89807a --- /dev/null +++ b/contracts/vesting-managed/schema/raw/query.json @@ -0,0 +1,157 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "QueryMsg", + "description": "This structure describes the query messages available in the 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": "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 + } + ], + "definitions": { + "OrderBy": { + "description": "This enum describes the types of sorting that can be applied to some piece of data", + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + } +} diff --git a/contracts/vesting-managed/schema/raw/response_to_available_amount.json b/contracts/vesting-managed/schema/raw/response_to_available_amount.json new file mode 100644 index 00000000..25b73e8f --- /dev/null +++ b/contracts/vesting-managed/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-managed/schema/raw/response_to_config.json b/contracts/vesting-managed/schema/raw/response_to_config.json new file mode 100644 index 00000000..9ab74329 --- /dev/null +++ b/contracts/vesting-managed/schema/raw/response_to_config.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ConfigResponse", + "description": "This structure describes a custom struct used to return the contract configuration.", + "type": "object", + "required": [ + "owner", + "vesting_token" + ], + "properties": { + "owner": { + "description": "Address allowed to set contract parameters", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "vesting_token": { + "description": "[`AssetInfo`] of the token that's being vested", + "allOf": [ + { + "$ref": "#/definitions/AssetInfo" + } + ] + } + }, + "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 + } + ] + } + } +} diff --git a/contracts/vesting-managed/schema/raw/response_to_timestamp.json b/contracts/vesting-managed/schema/raw/response_to_timestamp.json new file mode 100644 index 00000000..7b729a7b --- /dev/null +++ b/contracts/vesting-managed/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-managed/schema/raw/response_to_vesting_account.json b/contracts/vesting-managed/schema/raw/response_to_vesting_account.json new file mode 100644 index 00000000..499ca4de --- /dev/null +++ b/contracts/vesting-managed/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 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-managed/schema/raw/response_to_vesting_accounts.json b/contracts/vesting-managed/schema/raw/response_to_vesting_accounts.json new file mode 100644 index 00000000..5a0dfaf7 --- /dev/null +++ b/contracts/vesting-managed/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 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-managed/src/contract.rs b/contracts/vesting-managed/src/contract.rs new file mode 100644 index 00000000..363b8392 --- /dev/null +++ b/contracts/vesting-managed/src/contract.rs @@ -0,0 +1,220 @@ +use cosmwasm_std::{ + attr, entry_point, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, + StdResult, SubMsg, Uint128, +}; +use cw_storage_plus::{SnapshotItem, SnapshotMap, Strategy}; +use cw_utils::must_pay; + +use astroport::asset::AssetInfo; +use astroport::asset::AssetInfoExt; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use astroport::vesting::{InstantiateMsg, QueryMsg, VestingInfo, VestingState}; +use vesting_base::state::Config; +use vesting_base::{error::ContractError, state::BaseVesting}; + +use crate::msg::ExecuteMsg; + +/// Creates a new contract with the specified parameters packed in the `msg` variable. +/// Returns a [`Response`] with the specified attributes if the operation was successful, or a [`ContractError`] if the contract was not created +/// ## Params +/// * **deps** is an object of type [`DepsMut`]. +/// +/// * **env** is an object of type [`Env`]. +/// +/// * **info** is an object of type [`MessageInfo`]. +/// +/// * **msg** is a message of type [`InstantiateMsg`] which contains the parameters used for creating the contract. +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> StdResult { + let vest_app = BaseVesting::new(Strategy::Never); + vest_app.instantiate(deps, env, info, msg) +} + +/// Exposes execute functions available in the contract. +/// +/// ## Variants +/// * **ExecuteMsg::Claim { recipient, amount }** Claims vested tokens and transfers them to the vesting recipient. +/// +/// * **ExecuteMsg::Receive(msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes it +/// depending on the received template. +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let vest_app = BaseVesting::new(Strategy::Never); + + match msg { + ExecuteMsg::Claim { recipient, amount } => { + vest_app.claim(deps, env, info, recipient, amount) + } + ExecuteMsg::Receive(msg) => vest_app.receive_cw20(deps, env, info, msg), + ExecuteMsg::RegisterVestingAccounts { vesting_accounts } => { + let config = vest_app.config.load(deps.storage)?; + + match &config.vesting_token { + AssetInfo::NativeToken { denom } if info.sender == config.owner => { + let amount = must_pay(&info, denom)?; + vest_app.register_vesting_accounts( + deps, + vesting_accounts, + amount, + env.block.height, + ) + } + _ => Err(ContractError::Unauthorized {}), + } + } + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config: Config = vest_app.config.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + &vest_app.ownership_proposal, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config: Config = vest_app.config.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, &vest_app.ownership_proposal) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => claim_ownership( + deps, + info, + env, + &vest_app.ownership_proposal, + |deps, new_owner| { + vest_app + .config + .update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + })?; + + Ok(()) + }, + ) + .map_err(Into::into), + ExecuteMsg::RemoveVestingAccounts { + vesting_accounts, + clawback_account, + } => { + let config = vest_app.config.load(deps.storage)?; + remove_vesting_accounts( + deps, + info, + env, + config, + vesting_accounts, + vest_app.vesting_state, + vest_app.vesting_info, + clawback_account, + ) + } + } +} + +#[allow(clippy::too_many_arguments)] +fn remove_vesting_accounts( + deps: DepsMut, + info: MessageInfo, + env: Env, + config: Config, + vesting_accounts: Vec, + vesting_state: SnapshotItem<'static, VestingState>, + vesting_info: SnapshotMap<'static, &'static Addr, VestingInfo>, + clawback_account: String, +) -> Result { + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + 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)?; + + if let Some(account_info) = vesting_info.may_load(deps.storage, &account_address)? { + 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 = config + .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.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.clone(), env.block.height)?; + } + } + + Ok(response.add_attributes(vec![ + attr("action", "remove_vesting_accounts"), + attr("sender", &info.sender), + ])) +} + +/// Exposes all the queries available in the contract. +/// +/// ## Queries +/// * **QueryMsg::Config {}** Returns the contract configuration in an object of type [`Config`]. +/// +/// * **QueryMsg::VestingAccount { address }** Returns information about the vesting schedules that have a specific vesting recipient. +/// +/// * **QueryMsg::VestingAccounts { +/// start_after, +/// limit, +/// order_by, +/// }** Returns a list of vesting schedules together with their vesting recipients. +/// +/// * **QueryMsg::AvailableAmount { address }** Returns the available amount of tokens that can be claimed by a specific vesting recipient. +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + let vest_app = BaseVesting::new(Strategy::Never); + vest_app.query(deps, env, msg) +} diff --git a/contracts/vesting-managed/src/lib.rs b/contracts/vesting-managed/src/lib.rs new file mode 100644 index 00000000..ac1b5fd2 --- /dev/null +++ b/contracts/vesting-managed/src/lib.rs @@ -0,0 +1,5 @@ +pub mod contract; +mod msg; + +#[cfg(test)] +mod tests; diff --git a/contracts/vesting-managed/src/msg.rs b/contracts/vesting-managed/src/msg.rs new file mode 100644 index 00000000..efbb1c5e --- /dev/null +++ b/contracts/vesting-managed/src/msg.rs @@ -0,0 +1,47 @@ +use astroport::vesting::VestingAccount; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Uint128; +use cw20::Cw20ReceiveMsg; + +/// This structure describes the execute messages available in the 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 {}, + /// 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, + }, +} diff --git a/contracts/vesting-managed/src/tests/integration.rs b/contracts/vesting-managed/src/tests/integration.rs new file mode 100644 index 00000000..a84e2de0 --- /dev/null +++ b/contracts/vesting-managed/src/tests/integration.rs @@ -0,0 +1,1261 @@ +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 astroport::asset::{native_asset_info, token_asset_info}; +use astroport::querier::query_balance; +use astroport::vesting::{QueryMsg, VestingAccountResponse, VestingState}; +use astroport::{ + token::InstantiateMsg as TokenInstantiateMsg, + vesting::{ + Cw20HookMsg, ExecuteMsg, InstantiateMsg, VestingAccount, VestingSchedule, + VestingSchedulePoint, + }, +}; +use vesting_base::error::ContractError; +use vesting_base::state::Config; + +use crate::msg::ExecuteMsg as ManagedExecuteMsg; + +const OWNER1: &str = "owner1"; +const USER1: &str = "user1"; +const USER2: &str = "user2"; +const TOKEN_INITIAL_AMOUNT: u128 = 1_000_000_000_000_000; +const IBC_ASTRO: &str = "ibc/ASTRO_TOKEN"; + +#[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 astro_token_instance = instantiate_token( + &mut app, + token_code_id, + "ASTRO", + Some(1_000_000_000_000_000), + ); + + let vesting_instance = instantiate_vesting(&mut app, &astro_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(), astro_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(), astro_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, + &astro_token_instance, + &owner, + TOKEN_INITIAL_AMOUNT - 300u128, + ); + + // Check vesting balance + check_token_balance(&mut app, &astro_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, &astro_token_instance, &vesting_instance, 0u128); + + // Check user balance + check_token_balance(&mut app, &astro_token_instance, &user1, 300u128); + + // Owner balance mustn't change after claim + check_token_balance( + &mut app, + &astro_token_instance, + &owner, + 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 query_unclaimed() {} + +#[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, IBC_ASTRO), + ) + .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, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 300u128); + + // Check vesting balance + let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) + .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, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 0); + + // Check user balance + let bal = query_balance(&app.wrap(), &user1, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 300); + + // Owner balance mustn't change after claim + let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) + .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 astro_token_instance = instantiate_token( + &mut app, + token_code_id, + "ASTRO", + 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, &astro_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(), astro_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(), + astro_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 ASTRO 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(), astro_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, + &astro_token_instance, + &owner.clone(), + TOKEN_INITIAL_AMOUNT - 100u128, + ); + check_token_balance(&mut app, &astro_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(), astro_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, + &astro_token_instance, + &owner.clone(), + TOKEN_INITIAL_AMOUNT - 300u128, + ); + check_token_balance(&mut app, &astro_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(), astro_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, + &astro_token_instance, + &owner.clone(), + TOKEN_INITIAL_AMOUNT - 310u128, + ); + check_token_balance(&mut app, &astro_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, &astro_token_instance, &vesting_instance, 200u128); + check_token_balance(&mut app, &astro_token_instance, &user1, 110u128); + + // Owner balance mustn't change after claim + check_token_balance( + &mut app, + &astro_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("ibc/ASTRO_TOKEN".to_string())), + err.downcast().unwrap() + ); + + app.execute_contract( + owner.clone(), + vesting_instance.clone(), + &native_msg, + &coins(100u128, IBC_ASTRO), + ) + .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, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 100u128); + + let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) + .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, IBC_ASTRO), + ) + .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, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 300u128); + let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) + .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, IBC_ASTRO), + ) + .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, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 310u128); + let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) + .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, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 200); + let bal = query_balance(&app.wrap(), &user1, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 110u128); + + let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 310u128); +} + +#[test] +fn remove_vesting_accounts() { + 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); + + //////////////////////////////////////////////////////////////////////////////// + // + // Scenario #1: + // 1. Create vesting schedules + // 2. Check that the user has 400 vesting tokens, check that the owner has spent + // 400 tokens, check that the vesting contract has 400 tokens on its balance + // 3. Remove vesting schedules + // 4. Check that the user has 0 vesting tokens, check that the owner has received + // 400 tokens back, check that the vesting contract has 0 tokens on its balance + // + //////////////////////////////////////////////////////////////////////////////// + + let register_vesting_accounts_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::new(100), + }, + 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::new(100), + }, + end_point: None, + }, + 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(), + ®ister_vesting_accounts_msg, + &coins(400, IBC_ASTRO), + ) + .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(400u128)); + + // Check owner balance + let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 400u128); + + // Check vesting balance + let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 400u128); + + let remove_vesting_accounts_msg = ManagedExecuteMsg::RemoveVestingAccounts { + vesting_accounts: vec![user1.to_string()], + clawback_account: owner.to_string(), + }; + + app.execute_contract( + owner.clone(), + vesting_instance.clone(), + &remove_vesting_accounts_msg, + &[], + ) + .unwrap(); + + // Check that the owner received their tokens back. + let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT); + + // Check that the user has no funds available anymore (will result in a VestingInfo not found + // error). + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + let res: StdResult = app.wrap().query_wasm_smart(vesting_instance.clone(), &msg); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: Querier contract error: astroport::vesting::VestingInfo not found" + ); + + // Check vesting balance + let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 0u128); + + //////////////////////////////////////////////////////////////////////////////// + // + // Scenario #2: + // 1. Create vesting schedules + // 2. Check that the user has 400 vesting tokens, check that the owner has spent + // 400 tokens, check that the vesting contract has 400 tokens on its balance + // 3. Claim 400/2 tokens; + // 4. Check that vesting_state is { total_granted: 400, total_released: 200 } + // 5. Remove vesting schedules + // 6. Check that the user has 0 vesting tokens, check that the owner has received + // 400/2 tokens back, check that the vesting contract has 0 tokens on its balance, + // and check that the user 400/2 tokens on their balance. + // 7. Check that vesting_state is { total_granted: 0, total_released: 0 } + // + //////////////////////////////////////////////////////////////////////////////// + + let register_vesting_accounts_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::new(100), + }, + 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::new(100), + }, + end_point: None, + }, + 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(), + ®ister_vesting_accounts_msg, + &coins(400, IBC_ASTRO), + ) + .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(400u128)); + + // Check owner balance + let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 400u128); + + // Check vesting balance + let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 400u128); + + let msg = ExecuteMsg::Claim { + recipient: None, + amount: Some(Uint128::from(200u128)), + }; + app.execute_contract(user1.clone(), vesting_instance.clone(), &msg, &[]) + .unwrap(); + + let vesting_state: VestingState = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &QueryMsg::VestingState {}) + .unwrap(); + assert_eq!(vesting_state.total_granted.clone(), Uint128::new(400u128)); + assert_eq!(vesting_state.total_released.clone(), Uint128::new(200u128)); + + let remove_vesting_accounts_msg = ManagedExecuteMsg::RemoveVestingAccounts { + vesting_accounts: vec![user1.to_string()], + clawback_account: owner.to_string(), + }; + app.execute_contract( + owner.clone(), + vesting_instance.clone(), + &remove_vesting_accounts_msg, + &[], + ) + .unwrap(); + + // Check that the owner received their tokens back. + let bal = query_balance(&app.wrap(), &owner, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, TOKEN_INITIAL_AMOUNT - 200u128); + + // Check that the user has the tokens that they claimed. + let bal = query_balance(&app.wrap(), &user1, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 200u128); + + // Check that the user has no funds available anymore (will result in a VestingInfo not found + // error). + let msg = QueryMsg::AvailableAmount { + address: user1.to_string(), + }; + + let res: StdResult = app.wrap().query_wasm_smart(vesting_instance.clone(), &msg); + assert_eq!( + res.unwrap_err().to_string(), + "Generic error: Querier contract error: astroport::vesting::VestingInfo not found" + ); + + // Check vesting balance + let bal = query_balance(&app.wrap(), &vesting_instance, IBC_ASTRO) + .unwrap() + .u128(); + assert_eq!(bal, 0u128); + + let vesting_state: VestingState = app + .wrap() + .query_wasm_smart(vesting_instance, &QueryMsg::VestingState {}) + .unwrap(); + assert_eq!(vesting_state.total_granted.clone(), Uint128::new(0u128)); + assert_eq!(vesting_state.total_released.clone(), Uint128::new(0u128)); +} + +fn mock_app(owner: &Addr) -> App { + App::new(|app, _, storage| { + app.bank + .init_balance( + storage, + owner, + vec![ + coin(TOKEN_INITIAL_AMOUNT, IBC_ASTRO), + coin(10_000_000_000u128, "random_coin"), + ], + ) + .unwrap() + }) +} + +fn store_token_code(app: &mut App) -> u64 { + let astro_token_contract = Box::new(ContractWrapper::new_with_empty( + astroport_token::contract::execute, + astroport_token::contract::instantiate, + astroport_token::contract::query, + )); + + app.store_code(astro_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, astro_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 vesting_code_id = app.store_code(vesting_contract); + + let init_msg = InstantiateMsg { + owner: OWNER1.to_string(), + vesting_token: token_asset_info(astro_token_instance.clone()), + vesting_managers: Vec::new(), + }; + + let vesting_instance = app + .instantiate_contract( + vesting_code_id, + owner.clone(), + &init_msg, + &[], + "Vesting", + None, + ) + .unwrap(); + + let res: Config = app + .wrap() + .query_wasm_smart(vesting_instance.clone(), &QueryMsg::Config {}) + .unwrap(); + assert_eq!( + astro_token_instance.to_string(), + res.vesting_token.to_string() + ); + + mint_tokens(app, astro_token_instance, &owner, TOKEN_INITIAL_AMOUNT); + + check_token_balance(app, astro_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 vesting_code_id = app.store_code(vesting_contract); + + let init_msg = InstantiateMsg { + owner: OWNER1.to_string(), + vesting_token: native_asset_info(IBC_ASTRO.to_string()), + vesting_managers: Vec::new(), + }; + + app.instantiate_contract(vesting_code_id, owner, &init_msg, &[], "Vesting", None) + .unwrap() +} + +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-managed/src/tests/mod.rs b/contracts/vesting-managed/src/tests/mod.rs new file mode 100644 index 00000000..6d3bbe60 --- /dev/null +++ b/contracts/vesting-managed/src/tests/mod.rs @@ -0,0 +1 @@ +mod integration; diff --git a/packages/astroport/src/common.rs b/packages/astroport/src/common.rs index bcd56215..82abb459 100644 --- a/packages/astroport/src/common.rs +++ b/packages/astroport/src/common.rs @@ -31,7 +31,7 @@ pub fn propose_new_owner( new_owner: String, expires_in: u64, owner: Addr, - proposal: Item, + proposal: &Item, ) -> StdResult { // Permission check if info.sender != owner { @@ -75,7 +75,7 @@ pub fn drop_ownership_proposal( deps: DepsMut, info: MessageInfo, owner: Addr, - proposal: Item, + proposal: &Item, ) -> StdResult { // Permission check if info.sender != owner { @@ -97,8 +97,8 @@ pub fn claim_ownership( deps: DepsMut, info: MessageInfo, env: Env, - proposal: Item, - cb: fn(DepsMut, Addr) -> StdResult<()>, + proposal: &Item, + cb: impl Fn(DepsMut, Addr) -> StdResult<()>, ) -> StdResult { let p = proposal .load(deps.storage) diff --git a/packages/astroport/src/vesting.rs b/packages/astroport/src/vesting.rs index bea05e5d..84f95d0c 100644 --- a/packages/astroport/src/vesting.rs +++ b/packages/astroport/src/vesting.rs @@ -3,13 +3,17 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Order, Uint128}; use cw20::Cw20ReceiveMsg; +use crate::asset::AssetInfo; + /// This structure describes the parameters used for creating a contract. #[cw_serde] pub struct InstantiateMsg { /// Address allowed to change contract parameters pub owner: String, - /// The address of the token that's being vested - pub token_addr: String, + /// [`AssetInfo`] of the token that's being vested + pub vesting_token: AssetInfo, + /// Initial list of whitelisted vesting managers + pub vesting_managers: Vec, } /// This structure describes the execute messages available in the contract. @@ -24,6 +28,10 @@ pub enum ExecuteMsg { }, /// 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 @@ -41,6 +49,24 @@ pub enum ExecuteMsg { /// ## Executor /// Only the newly proposed owner can execute this ClaimOwnership {}, + /// 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 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. @@ -57,7 +83,7 @@ pub struct VestingAccount { pub struct VestingInfo { /// The vesting schedules pub schedules: Vec, - /// The total amount of ASTRO already claimed + /// The total amount of vested tokens already claimed pub released_amount: Uint128, } @@ -102,6 +128,13 @@ pub enum QueryMsg { /// Timestamp returns the current timestamp #[returns(u64)] Timestamp {}, + /// VestingState returns the current vesting state. + #[returns(VestingState)] + VestingState {}, + /// Returns list of vesting managers + /// (the persons who are able to add/remove vesting schedules) + #[returns(Vec)] + VestingManagers {}, } /// This structure describes a custom struct used to return the contract configuration. @@ -109,8 +142,8 @@ pub enum QueryMsg { pub struct ConfigResponse { /// Address allowed to set contract parameters pub owner: Addr, - /// The address of the token being vested - pub token_addr: Addr, + /// [`AssetInfo`] of the token that's being vested + pub vesting_token: AssetInfo, } /// This structure describes a custom struct used to return vesting data about a specific vesting target. diff --git a/packages/vesting-base/Cargo.toml b/packages/vesting-base/Cargo.toml new file mode 100644 index 00000000..4f0a3307 --- /dev/null +++ b/packages/vesting-base/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] +cw2 = { version = "0.15" } +cw20 = { version = "0.15" } +cosmwasm-std = { version = "1.1" } +cw-storage-plus = "0.15" +astroport = { path = "../astroport", default-features = false } +thiserror = { version = "1.0" } +cw-utils = "0.15" +cosmwasm-schema = { version = "1.1", default-features = false } \ No newline at end of file diff --git a/packages/vesting-base/NOTICE b/packages/vesting-base/NOTICE new file mode 100644 index 00000000..84b1c210 --- /dev/null +++ b/packages/vesting-base/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/README.md b/packages/vesting-base/README.md new file mode 100644 index 00000000..43fcab42 --- /dev/null +++ b/packages/vesting-base/README.md @@ -0,0 +1,162 @@ +# Neutron Vesting + +The Vesting contract progressively unlocks vested tokens that can then be claimed by stakers. + +--- + +## InstantiateMsg + +Initializes the contract with the description of the vested token(cw20/native). + +```json +{ + "vesting_token":{ + "token":{ + "token_addr": "neutron..." + } + } +} +``` + +or + +```json +{ + "vesting_token":{ + "native_token":{ + "denom": "NTRN" + } + } +} +``` + +### `receive` + +CW20 receive msg. + +```json +{ + "receive": { + "sender": "neutron...", + "amount": "123", + "msg": "" + } +} +``` + +#### `RegisterVestingAccounts` + +Creates vesting schedules for the vested token. Each vesting token should have the contract address as the `VestingContractAddress`. Also, each schedule will unlock tokens at a different rate according to its time duration. + +Execute this message by calling the vested token contract address. + +```json +{ + "send": { + "contract": , + "amount": "999", + "msg": "base64-encodedStringOfWithdrawMsg" + } +} +``` + +In `send.msg`, you may encode this JSON string into base64 encoding. + +```json +{ + "RegisterVestingAccounts": { + "vesting_accounts": [ + { + "address": "neutron...", + "schedules": { + "start_point": { + "time": "1634125119000000000", + "amount": "123" + }, + "end_point": { + "time": "1664125119000000000", + "amount": "123" + } + } + } + ] + } +} +``` + +### `claim` + +Transfer vested tokens from all vesting schedules that have the same `VestingContractAddress` (address that's vesting tokens). + +```json +{ + "claim": { + "recipient": "neutron...", + "amount": "123" + } +} +``` + +## QueryMsg + +All query messages are described below. A custom struct is defined for each query response. + +### `config` + +Returns the vesting token contract address (the vested token address). + +```json +{ + "config": {} +} +``` + +### `vesting_account` + +Returns all vesting schedules with their details for a specific vesting recipient. + +```json +{ + "vesting_account": { + "address": "neutron..." + } +} +``` + +### `vesting_accounts` + +Returns a paginated list of vesting schedules in chronological order. Given fields are optional. + +```json +{ + "vesting_accounts": { + "start_after": "neutron...", + "limit": 10, + "order_by": { + "desc": {} + } + } +} +``` + +### `available amount` + +Returns the claimable amount (vested but not yet claimed) of vested tokens that a vesting target can claim. + +```json +{ + "available_amount": { + "address": "neutron..." + } +} +``` + +### `vesting_managers` + +Returns list of vesting managers - the persons who are able to add/remove vesting schedules. + +```json +{ + "vesting_managers": {} +} +``` diff --git a/packages/vesting-base/src/contract.rs b/packages/vesting-base/src/contract.rs new file mode 100644 index 00000000..ac3c13af --- /dev/null +++ b/packages/vesting-base/src/contract.rs @@ -0,0 +1,540 @@ +use cosmwasm_std::{ + attr, from_binary, to_binary, Addr, Attribute, Binary, Deps, DepsMut, Env, MessageInfo, Order, + Response, StdError, StdResult, SubMsg, Uint128, +}; + +use crate::state::{BaseVesting, Config}; + +use crate::error::ContractError; +use astroport::asset::{addr_opt_validate, token_asset_info, AssetInfo, AssetInfoExt}; +use astroport::common::{claim_ownership, drop_ownership_proposal, propose_new_owner}; +use astroport::vesting::{ + ConfigResponse, Cw20HookMsg, ExecuteMsg, InstantiateMsg, MigrateMsg, OrderBy, QueryMsg, + VestingAccount, VestingAccountResponse, VestingAccountsResponse, VestingInfo, VestingSchedule, +}; +use cw2::set_contract_version; +use cw20::Cw20ReceiveMsg; +use cw_utils::must_pay; + +/// Contract name that is used for migration. +const CONTRACT_NAME: &str = "neutron-vesting"; +/// Contract version that is used for migration. +const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + +impl BaseVesting { + /// Creates a new contract with the specified parameters in [`InstantiateMsg`]. + pub fn instantiate( + &self, + deps: DepsMut, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, + ) -> StdResult { + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + msg.vesting_token.check(deps.api)?; + + self.config.save( + deps.storage, + &Config { + owner: deps.api.addr_validate(&msg.owner)?, + vesting_token: msg.vesting_token, + }, + )?; + + for m in msg.vesting_managers { + let ma = deps.api.addr_validate(&m)?; + self.vesting_managers.save(deps.storage, &ma, &())?; + } + + Ok(Response::new()) + } + + /// Exposes execute functions available in the contract. + /// + /// ## Variants + /// * **ExecuteMsg::Claim { recipient, amount }** Claims vested tokens and transfers them to the vesting recipient. + /// + /// * **ExecuteMsg::Receive(msg)** Receives a message of type [`Cw20ReceiveMsg`] and processes it + /// depending on the received template. + pub fn execute( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + match msg { + ExecuteMsg::Claim { recipient, amount } => { + self.claim(deps, env, info, recipient, amount) + } + ExecuteMsg::Receive(msg) => self.receive_cw20(deps, env, info, msg), + ExecuteMsg::RegisterVestingAccounts { vesting_accounts } => { + let config = self.config.load(deps.storage)?; + + match &config.vesting_token { + AssetInfo::NativeToken { denom } + if self.is_sender_whitelisted(deps.as_ref(), &config, &info.sender) => + { + let amount = must_pay(&info, denom)?; + self.register_vesting_accounts( + deps, + vesting_accounts, + amount, + env.block.height, + ) + } + _ => Err(ContractError::Unauthorized {}), + } + } + ExecuteMsg::ProposeNewOwner { owner, expires_in } => { + let config: Config = self.config.load(deps.storage)?; + + propose_new_owner( + deps, + info, + env, + owner, + expires_in, + config.owner, + &self.ownership_proposal, + ) + .map_err(Into::into) + } + ExecuteMsg::DropOwnershipProposal {} => { + let config: Config = self.config.load(deps.storage)?; + + drop_ownership_proposal(deps, info, config.owner, &self.ownership_proposal) + .map_err(Into::into) + } + ExecuteMsg::ClaimOwnership {} => claim_ownership( + deps, + info, + env, + &self.ownership_proposal, + |deps, new_owner| { + self.config.update::<_, StdError>(deps.storage, |mut v| { + v.owner = new_owner; + Ok(v) + })?; + + Ok(()) + }, + ) + .map_err(Into::into), + ExecuteMsg::AddVestingManagers { managers } => { + self.add_vesting_managers(deps, env, info, managers) + } + ExecuteMsg::RemoveVestingManagers { managers } => { + self.remove_vesting_managers(deps, env, info, managers) + } + } + } + + fn is_sender_whitelisted(&self, deps: Deps, config: &Config, sender: &Addr) -> bool { + if *sender == config.owner { + return true; + } + if self.vesting_managers.has(deps.storage, sender) { + return true; + } + false + } + + /// Receives a message of type [`Cw20ReceiveMsg`] and processes it depending on the received template. + /// + /// * **cw20_msg** CW20 message to process. + pub fn receive_cw20( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + cw20_msg: Cw20ReceiveMsg, + ) -> Result { + let config = self.config.load(deps.storage)?; + + // Permission check + if !self.is_sender_whitelisted( + deps.as_ref(), + &config, + &deps.api.addr_validate(&cw20_msg.sender)?, + ) || token_asset_info(info.sender) != config.vesting_token + { + return Err(ContractError::Unauthorized {}); + } + + match from_binary(&cw20_msg.msg)? { + Cw20HookMsg::RegisterVestingAccounts { vesting_accounts } => self + .register_vesting_accounts( + deps, + vesting_accounts, + cw20_msg.amount, + env.block.height, + ), + } + } + + /// Adds new vesting managers, which have a permission to add/remove vesting schedule + /// + /// * **managers** list of accounts to be added to the whitelist. + pub fn add_vesting_managers( + &self, + deps: DepsMut, + _env: Env, + info: MessageInfo, + managers: Vec, + ) -> Result { + let config = self.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 !self.vesting_managers.has(deps.storage, &ma) { + self.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. + pub fn remove_vesting_managers( + &self, + deps: DepsMut, + _env: Env, + info: MessageInfo, + managers: Vec, + ) -> Result { + let config = self.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 self.vesting_managers.has(deps.storage, &ma) { + self.vesting_managers.remove(deps.storage, &ma); + attrs.push(attr("vesting_manager", &m)) + } + } + Ok(Response::new() + .add_attribute("action", "remove_vesting_managers") + .add_attributes(attrs)) + } + + /// 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. + pub fn register_vesting_accounts( + &self, + deps: DepsMut, + vesting_accounts: Vec, + amount: Uint128, + height: u64, + ) -> Result { + let response = Response::new(); + + 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)?; + } + + if let Some(mut old_info) = + self.vesting_info.may_load(deps.storage, &account_address)? + { + released_amount = old_info.released_amount; + vesting_account.schedules.append(&mut old_info.schedules); + } + + self.vesting_info.save( + deps.storage, + &account_address, + &VestingInfo { + schedules: vesting_account.schedules, + released_amount, + }, + height, + )?; + } + + if to_deposit != amount { + return Err(ContractError::VestingScheduleAmountError {}); + } + + self.vesting_state + .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. + pub fn claim( + &self, + deps: DepsMut, + env: Env, + info: MessageInfo, + recipient: Option, + amount: Option, + ) -> Result { + let config = self.config.load(deps.storage)?; + let mut vesting_info = self.vesting_info.load(deps.storage, &info.sender)?; + + let available_amount = compute_available_amount(env.block.time.seconds(), &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 = config.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)); + + vesting_info.released_amount = + vesting_info.released_amount.checked_add(claim_amount)?; + self.vesting_info + .save(deps.storage, &info.sender, &vesting_info, env.block.height)?; + self.vesting_state + .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), + ])) + } + + /// Exposes all the queries available in the contract. + /// + /// ## Queries + /// * **QueryMsg::Config {}** Returns the contract configuration in an object of type [`Config`]. + /// + /// * **QueryMsg::VestingAccount { address }** Returns information about the vesting schedules that have a specific vesting recipient. + /// + /// * **QueryMsg::VestingAccounts { + /// start_after, + /// limit, + /// order_by, + /// }** Returns a list of vesting schedules together with their vesting recipients. + /// + /// * **QueryMsg::AvailableAmount { address }** Returns the available amount of tokens that can be claimed by a specific vesting recipient. + pub fn query(&self, deps: Deps, env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config {} => Ok(to_binary(&self.query_config(deps)?)?), + QueryMsg::VestingAccount { address } => { + Ok(to_binary(&self.query_vesting_account(deps, address)?)?) + } + QueryMsg::VestingAccounts { + start_after, + limit, + order_by, + } => Ok(to_binary(&self.query_vesting_accounts( + deps, + start_after, + limit, + order_by, + )?)?), + QueryMsg::AvailableAmount { address } => Ok(to_binary( + &self.query_vesting_available_amount(deps, env, address)?, + )?), + QueryMsg::Timestamp {} => Ok(to_binary(&self.query_timestamp(env)?)?), + QueryMsg::VestingState {} => Ok(to_binary(&self.vesting_state.load(deps.storage)?)?), + QueryMsg::VestingManagers {} => Ok(to_binary(&self.query_vesting_managers(deps)?)?), + } + } + + /// Returns the vesting contract configuration using a [`ConfigResponse`] object. + pub fn query_config(&self, deps: Deps) -> StdResult { + let config = self.config.load(deps.storage)?; + + Ok(ConfigResponse { + owner: config.owner, + vesting_token: config.vesting_token, + }) + } + + /// Return the current block timestamp (in seconds) + /// * **env** is an object of type [`Env`]. + pub fn query_timestamp(&self, env: Env) -> StdResult { + Ok(env.block.time.seconds()) + } + + /// Returns a list of vesting schedules using a [`VestingAccountsResponse`] object. + pub fn query_vesting_managers(&self, deps: Deps) -> StdResult> { + let managers = self + .vesting_managers + .keys(deps.storage, None, None, Order::Ascending) + .collect::, StdError>>()?; + Ok(managers) + } + + /// Returns the vesting data for a specific vesting recipient using a [`VestingAccountResponse`] object. + /// + /// * **address** vesting recipient for which to return vesting data. + pub fn query_vesting_account( + &self, + deps: Deps, + address: String, + ) -> StdResult { + let address = deps.api.addr_validate(&address)?; + let info = self.vesting_info.load(deps.storage, &address)?; + + 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. + pub fn query_vesting_accounts( + &self, + deps: Deps, + start_after: Option, + limit: Option, + order_by: Option, + ) -> StdResult { + let start_after = addr_opt_validate(deps.api, &start_after)?; + + let vesting_infos = self.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. + pub fn query_vesting_available_amount( + &self, + deps: Deps, + env: Env, + address: String, + ) -> StdResult { + let address = deps.api.addr_validate(&address)?; + + let info = self.vesting_info.load(deps.storage, &address)?; + let available_amount = compute_available_amount(env.block.time.seconds(), &info)?; + Ok(available_amount) + } + + /// Manages contract migration. + pub fn migrate( + &self, + _deps: DepsMut, + _env: Env, + _msg: MigrateMsg, + ) -> Result { + Ok(Response::default()) + } +} + +/// 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/src/error.rs b/packages/vesting-base/src/error.rs new file mode 100644 index 00000000..30c9f4aa --- /dev/null +++ b/packages/vesting-base/src/error.rs @@ -0,0 +1,34 @@ +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 {}, +} + +impl From for ContractError { + fn from(o: OverflowError) -> Self { + StdError::from(o).into() + } +} diff --git a/packages/vesting-base/src/lib.rs b/packages/vesting-base/src/lib.rs new file mode 100644 index 00000000..f082c75f --- /dev/null +++ b/packages/vesting-base/src/lib.rs @@ -0,0 +1,6 @@ +pub mod contract; +pub mod error; +pub mod state; + +#[cfg(test)] +mod testing; diff --git a/packages/vesting-base/src/state.rs b/packages/vesting-base/src/state.rs new file mode 100644 index 00000000..f14501fe --- /dev/null +++ b/packages/vesting-base/src/state.rs @@ -0,0 +1,174 @@ +use cosmwasm_schema::cw_serde; + +use astroport::asset::AssetInfo; +use astroport::common::OwnershipProposal; +use astroport::vesting::{OrderBy, VestingInfo, VestingState}; +use cosmwasm_std::{Addr, Deps, StdResult}; +use cw_storage_plus::{Bound, Item, Map, SnapshotItem, SnapshotMap, Strategy}; + +pub struct BaseVesting { + /// Stores the total granted/claimed amount of tokens + pub vesting_state: SnapshotItem<'static, VestingState>, + /// The first key is the address of an account that's vesting, the second key is an object of type [`VestingInfo`]. + pub vesting_info: SnapshotMap<'static, &'static Addr, VestingInfo>, + /// Stores the contract config at the given key. + pub config: Item<'static, Config>, + /// Contains a proposal to change contract ownership. + pub ownership_proposal: Item<'static, OwnershipProposal>, + /// Contains the list of managers with a permission of adding/removing vesting schedules. + pub vesting_managers: Map<'static, &'static Addr, ()>, +} + +impl BaseVesting { + pub fn new(snapshot_strategy: Strategy) -> Self { + BaseVesting { + vesting_state: SnapshotItem::new( + "vesting_state", + "vesting_state__checkpoints", + "vesting_state__changelog", + snapshot_strategy, + ), + vesting_info: SnapshotMap::new( + "vesting_info", + "vesting_info__checkpoints", + "vesting_info__changelog", + snapshot_strategy, + ), + config: Item::new("config"), + ownership_proposal: Item::new("ownership_proposal"), + vesting_managers: Map::new("vesting_managers"), + } + } +} + +/// 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: AssetInfo, +} + +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +impl BaseVesting { + /// 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 fn read_vesting_infos( + &self, + 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.as_ref().map(Bound::exclusive); + + let (start, end) = match &order_by { + Some(OrderBy::Asc) => (start_after, None), + _ => (None, start_after), + }; + + let info: Vec<(Addr, VestingInfo)> = self + .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 vest_app = BaseVesting::new(Strategy::Never); + + let mut deps = mock_dependencies(); + + let vi_mock = VestingInfo { + released_amount: Uint128::zero(), + schedules: vec![], + }; + + for i in 1..5 { + let key = Addr::unchecked(format! {"address{}", i}); + + vest_app + .vesting_info + .save(&mut deps.storage, &key, &vi_mock, 1) + .unwrap(); + } + + let res = vest_app + .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 = vest_app + .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 = vest_app + .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 = vest_app + .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/src/testing.rs b/packages/vesting-base/src/testing.rs new file mode 100644 index 00000000..8e452744 --- /dev/null +++ b/packages/vesting-base/src/testing.rs @@ -0,0 +1,49 @@ +use astroport::vesting::{ConfigResponse, InstantiateMsg, QueryMsg}; + +use astroport::asset::token_asset_info; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{from_binary, Addr}; +use cw_storage_plus::Strategy; + +use crate::state::BaseVesting; + +#[test] +fn proper_initialization() { + let mut deps = mock_dependencies(); + let vest_app = BaseVesting::new(Strategy::Never); + + let msg = InstantiateMsg { + owner: "owner".to_string(), + vesting_token: token_asset_info(Addr::unchecked("ntrn_token")), + vesting_managers: vec!["manager1".to_string(), "manager2".to_string()], + }; + + let env = mock_env(); + let info = mock_info("addr0000", &[]); + let _res = vest_app + .instantiate(deps.as_mut(), env.clone(), info, msg) + .unwrap(); + + assert_eq!( + from_binary::( + &vest_app + .query(deps.as_ref(), env.clone(), QueryMsg::Config {}) + .unwrap() + ) + .unwrap(), + ConfigResponse { + owner: Addr::unchecked("owner"), + vesting_token: token_asset_info(Addr::unchecked("ntrn_token")), + } + ); + + assert_eq!( + from_binary::>( + &vest_app + .query(deps.as_ref(), env, QueryMsg::VestingManagers {}) + .unwrap() + ) + .unwrap(), + vec![Addr::unchecked("manager1"), Addr::unchecked("manager2")], + ); +}