diff --git a/Cargo.lock b/Cargo.lock index 71a7e425..e11160aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -661,7 +661,7 @@ dependencies = [ [[package]] name = "incentive" -version = "1.0.3" +version = "1.0.6" dependencies = [ "anyhow", "cosmwasm-schema", diff --git a/contracts/liquidity_hub/pool-network/incentive/Cargo.toml b/contracts/liquidity_hub/pool-network/incentive/Cargo.toml index b3d05b9a..37b7d9b9 100644 --- a/contracts/liquidity_hub/pool-network/incentive/Cargo.toml +++ b/contracts/liquidity_hub/pool-network/incentive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "incentive" -version = "1.0.3" +version = "1.0.6" authors = ["kaimen-sano "] edition.workspace = true description = "An incentive manager for an LP token" diff --git a/contracts/liquidity_hub/pool-network/incentive/schema/incentive.json b/contracts/liquidity_hub/pool-network/incentive/schema/incentive.json index 1681fb27..87061140 100644 --- a/contracts/liquidity_hub/pool-network/incentive/schema/incentive.json +++ b/contracts/liquidity_hub/pool-network/incentive/schema/incentive.json @@ -103,22 +103,26 @@ "open_flow": { "type": "object", "required": [ - "curve", - "end_epoch", "flow_asset" ], "properties": { "curve": { - "description": "The type of distribution curve.", - "allOf": [ + "description": "The type of distribution curve. If unspecified, the distribution will be linear.", + "anyOf": [ { "$ref": "#/definitions/Curve" + }, + { + "type": "null" } ] }, "end_epoch": { - "description": "The epoch at which the flow should end.", - "type": "integer", + "description": "The epoch at which the flow should end. If unspecified, the flow will default to end at 14 epochs from the current one.", + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 0.0 }, @@ -130,8 +134,15 @@ } ] }, + "flow_label": { + "description": "If set, the label will be used to identify the flow, in addition to the flow_id.", + "type": [ + "string", + "null" + ] + }, "start_epoch": { - "description": "The epoch at which the flow should start.\n\nIf unspecified, the flow will start at the current epoch.", + "description": "The epoch at which the flow will start. If unspecified, the flow will start at the current epoch.", "type": [ "integer", "null" @@ -155,14 +166,16 @@ "close_flow": { "type": "object", "required": [ - "flow_id" + "flow_identifier" ], "properties": { - "flow_id": { - "description": "The id of the flow to close.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "flow_identifier": { + "description": "The identifier of the flow to close.", + "allOf": [ + { + "$ref": "#/definitions/FlowIdentifier" + } + ] } }, "additionalProperties": false @@ -304,6 +317,51 @@ } }, "additionalProperties": false + }, + { + "description": "Expands an existing flow.", + "type": "object", + "required": [ + "expand_flow" + ], + "properties": { + "expand_flow": { + "type": "object", + "required": [ + "flow_asset", + "flow_identifier" + ], + "properties": { + "end_epoch": { + "description": "The epoch at which the flow should end. If not set, the flow will be expanded a default value of 14 epochs.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "flow_asset": { + "description": "The asset to expand this flow with.", + "allOf": [ + { + "$ref": "#/definitions/Asset" + } + ] + }, + "flow_identifier": { + "description": "The identifier of the flow to expand, whether an id or a label.", + "allOf": [ + { + "$ref": "#/definitions/FlowIdentifier" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -382,6 +440,36 @@ } ] }, + "FlowIdentifier": { + "oneOf": [ + { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "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" @@ -407,7 +495,7 @@ "additionalProperties": false }, { - "description": "Retrieves a specific flow.", + "description": "Retrieves a specific flow. If start_epoch and end_epoch are set, the asset_history and emitted_tokens will be filtered to only include epochs within the range. The maximum gap between the start_epoch and end_epoch is 100 epochs.", "type": "object", "required": [ "flow" @@ -416,12 +504,32 @@ "flow": { "type": "object", "required": [ - "flow_id" + "flow_identifier" ], "properties": { - "flow_id": { + "end_epoch": { + "description": "If set, filters the asset_history and emitted_tokens to only include epochs until end_epoch.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "flow_identifier": { "description": "The id of the flow to find.", - "type": "integer", + "allOf": [ + { + "$ref": "#/definitions/FlowIdentifier" + } + ] + }, + "start_epoch": { + "description": "If set, filters the asset_history and emitted_tokens to only include epochs from start_epoch.", + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 0.0 } @@ -432,7 +540,7 @@ "additionalProperties": false }, { - "description": "Retrieves the current flows.", + "description": "Retrieves the current flows. If start_epoch and end_epoch are set, the asset_history and emitted_tokens will be filtered to only include epochs within the range. The maximum gap between the start_epoch and end_epoch is 100 epochs.", "type": "object", "required": [ "flows" @@ -440,6 +548,26 @@ "properties": { "flows": { "type": "object", + "properties": { + "end_epoch": { + "description": "If set, filters the asset_history and emitted_tokens to only include epochs until end_epoch.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "start_epoch": { + "description": "If set, filters the asset_history and emitted_tokens to only include epochs from start_epoch.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, "additionalProperties": false } }, @@ -539,7 +667,39 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "FlowIdentifier": { + "oneOf": [ + { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } }, "migrate": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -788,6 +948,7 @@ "description": "Represents a flow.", "type": "object", "required": [ + "asset_history", "claimed_amount", "curve", "emitted_tokens", @@ -798,6 +959,25 @@ "start_epoch" ], "properties": { + "asset_history": { + "description": "A map containing the amount of tokens it was expanded to at a given epoch. This is used to calculate the right amount of tokens to distribute at a given epoch when a flow is expanded.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, "claimed_amount": { "description": "The amount of the `flow_asset` that has been claimed so far.", "allOf": [ @@ -849,6 +1029,13 @@ "format": "uint64", "minimum": 0.0 }, + "flow_label": { + "description": "An alternative flow label.", + "type": [ + "string", + "null" + ] + }, "start_epoch": { "description": "The epoch at which the flow starts.", "type": "integer", @@ -965,6 +1152,7 @@ "description": "Represents a flow.", "type": "object", "required": [ + "asset_history", "claimed_amount", "curve", "emitted_tokens", @@ -975,6 +1163,25 @@ "start_epoch" ], "properties": { + "asset_history": { + "description": "A map containing the amount of tokens it was expanded to at a given epoch. This is used to calculate the right amount of tokens to distribute at a given epoch when a flow is expanded.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, "claimed_amount": { "description": "The amount of the `flow_asset` that has been claimed so far.", "allOf": [ @@ -1026,6 +1233,13 @@ "format": "uint64", "minimum": 0.0 }, + "flow_label": { + "description": "An alternative flow label.", + "type": [ + "string", + "null" + ] + }, "start_epoch": { "description": "The epoch at which the flow starts.", "type": "integer", diff --git a/contracts/liquidity_hub/pool-network/incentive/schema/raw/execute.json b/contracts/liquidity_hub/pool-network/incentive/schema/raw/execute.json index 094b99c2..320b66a1 100644 --- a/contracts/liquidity_hub/pool-network/incentive/schema/raw/execute.json +++ b/contracts/liquidity_hub/pool-network/incentive/schema/raw/execute.json @@ -26,22 +26,26 @@ "open_flow": { "type": "object", "required": [ - "curve", - "end_epoch", "flow_asset" ], "properties": { "curve": { - "description": "The type of distribution curve.", - "allOf": [ + "description": "The type of distribution curve. If unspecified, the distribution will be linear.", + "anyOf": [ { "$ref": "#/definitions/Curve" + }, + { + "type": "null" } ] }, "end_epoch": { - "description": "The epoch at which the flow should end.", - "type": "integer", + "description": "The epoch at which the flow should end. If unspecified, the flow will default to end at 14 epochs from the current one.", + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 0.0 }, @@ -53,8 +57,15 @@ } ] }, + "flow_label": { + "description": "If set, the label will be used to identify the flow, in addition to the flow_id.", + "type": [ + "string", + "null" + ] + }, "start_epoch": { - "description": "The epoch at which the flow should start.\n\nIf unspecified, the flow will start at the current epoch.", + "description": "The epoch at which the flow will start. If unspecified, the flow will start at the current epoch.", "type": [ "integer", "null" @@ -78,14 +89,16 @@ "close_flow": { "type": "object", "required": [ - "flow_id" + "flow_identifier" ], "properties": { - "flow_id": { - "description": "The id of the flow to close.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "flow_identifier": { + "description": "The identifier of the flow to close.", + "allOf": [ + { + "$ref": "#/definitions/FlowIdentifier" + } + ] } }, "additionalProperties": false @@ -227,6 +240,51 @@ } }, "additionalProperties": false + }, + { + "description": "Expands an existing flow.", + "type": "object", + "required": [ + "expand_flow" + ], + "properties": { + "expand_flow": { + "type": "object", + "required": [ + "flow_asset", + "flow_identifier" + ], + "properties": { + "end_epoch": { + "description": "The epoch at which the flow should end. If not set, the flow will be expanded a default value of 14 epochs.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "flow_asset": { + "description": "The asset to expand this flow with.", + "allOf": [ + { + "$ref": "#/definitions/Asset" + } + ] + }, + "flow_identifier": { + "description": "The identifier of the flow to expand, whether an id or a label.", + "allOf": [ + { + "$ref": "#/definitions/FlowIdentifier" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ], "definitions": { @@ -305,6 +363,36 @@ } ] }, + "FlowIdentifier": { + "oneOf": [ + { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "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" diff --git a/contracts/liquidity_hub/pool-network/incentive/schema/raw/query.json b/contracts/liquidity_hub/pool-network/incentive/schema/raw/query.json index f1bd81e1..bdd4caf0 100644 --- a/contracts/liquidity_hub/pool-network/incentive/schema/raw/query.json +++ b/contracts/liquidity_hub/pool-network/incentive/schema/raw/query.json @@ -17,7 +17,7 @@ "additionalProperties": false }, { - "description": "Retrieves a specific flow.", + "description": "Retrieves a specific flow. If start_epoch and end_epoch are set, the asset_history and emitted_tokens will be filtered to only include epochs within the range. The maximum gap between the start_epoch and end_epoch is 100 epochs.", "type": "object", "required": [ "flow" @@ -26,12 +26,32 @@ "flow": { "type": "object", "required": [ - "flow_id" + "flow_identifier" ], "properties": { - "flow_id": { + "end_epoch": { + "description": "If set, filters the asset_history and emitted_tokens to only include epochs until end_epoch.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "flow_identifier": { "description": "The id of the flow to find.", - "type": "integer", + "allOf": [ + { + "$ref": "#/definitions/FlowIdentifier" + } + ] + }, + "start_epoch": { + "description": "If set, filters the asset_history and emitted_tokens to only include epochs from start_epoch.", + "type": [ + "integer", + "null" + ], "format": "uint64", "minimum": 0.0 } @@ -42,7 +62,7 @@ "additionalProperties": false }, { - "description": "Retrieves the current flows.", + "description": "Retrieves the current flows. If start_epoch and end_epoch are set, the asset_history and emitted_tokens will be filtered to only include epochs within the range. The maximum gap between the start_epoch and end_epoch is 100 epochs.", "type": "object", "required": [ "flows" @@ -50,6 +70,26 @@ "properties": { "flows": { "type": "object", + "properties": { + "end_epoch": { + "description": "If set, filters the asset_history and emitted_tokens to only include epochs until end_epoch.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + }, + "start_epoch": { + "description": "If set, filters the asset_history and emitted_tokens to only include epochs from start_epoch.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 + } + }, "additionalProperties": false } }, @@ -149,5 +189,37 @@ }, "additionalProperties": false } - ] + ], + "definitions": { + "FlowIdentifier": { + "oneOf": [ + { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string" + } + }, + "additionalProperties": false + } + ] + } + } } diff --git a/contracts/liquidity_hub/pool-network/incentive/schema/raw/response_to_flow.json b/contracts/liquidity_hub/pool-network/incentive/schema/raw/response_to_flow.json index 56a433e3..09990c47 100644 --- a/contracts/liquidity_hub/pool-network/incentive/schema/raw/response_to_flow.json +++ b/contracts/liquidity_hub/pool-network/incentive/schema/raw/response_to_flow.json @@ -100,6 +100,7 @@ "description": "Represents a flow.", "type": "object", "required": [ + "asset_history", "claimed_amount", "curve", "emitted_tokens", @@ -110,6 +111,25 @@ "start_epoch" ], "properties": { + "asset_history": { + "description": "A map containing the amount of tokens it was expanded to at a given epoch. This is used to calculate the right amount of tokens to distribute at a given epoch when a flow is expanded.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, "claimed_amount": { "description": "The amount of the `flow_asset` that has been claimed so far.", "allOf": [ @@ -161,6 +181,13 @@ "format": "uint64", "minimum": 0.0 }, + "flow_label": { + "description": "An alternative flow label.", + "type": [ + "string", + "null" + ] + }, "start_epoch": { "description": "The epoch at which the flow starts.", "type": "integer", diff --git a/contracts/liquidity_hub/pool-network/incentive/schema/raw/response_to_flows.json b/contracts/liquidity_hub/pool-network/incentive/schema/raw/response_to_flows.json index dacb6b25..bf2f9848 100644 --- a/contracts/liquidity_hub/pool-network/incentive/schema/raw/response_to_flows.json +++ b/contracts/liquidity_hub/pool-network/incentive/schema/raw/response_to_flows.json @@ -99,6 +99,7 @@ "description": "Represents a flow.", "type": "object", "required": [ + "asset_history", "claimed_amount", "curve", "emitted_tokens", @@ -109,6 +110,25 @@ "start_epoch" ], "properties": { + "asset_history": { + "description": "A map containing the amount of tokens it was expanded to at a given epoch. This is used to calculate the right amount of tokens to distribute at a given epoch when a flow is expanded.", + "type": "object", + "additionalProperties": { + "type": "array", + "items": [ + { + "$ref": "#/definitions/Uint128" + }, + { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + ], + "maxItems": 2, + "minItems": 2 + } + }, "claimed_amount": { "description": "The amount of the `flow_asset` that has been claimed so far.", "allOf": [ @@ -160,6 +180,13 @@ "format": "uint64", "minimum": 0.0 }, + "flow_label": { + "description": "An alternative flow label.", + "type": [ + "string", + "null" + ] + }, "start_epoch": { "description": "The epoch at which the flow starts.", "type": "integer", diff --git a/contracts/liquidity_hub/pool-network/incentive/src/claim.rs b/contracts/liquidity_hub/pool-network/incentive/src/claim.rs index 199a9de1..9ba23587 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/claim.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/claim.rs @@ -5,11 +5,14 @@ use cosmwasm_std::{ use white_whale::pool_network::asset::AssetInfo; +use crate::helpers::{get_flow_asset_amount_at_epoch, get_flow_current_end_epoch}; use crate::state::{EpochId, ADDRESS_WEIGHT_HISTORY, GLOBAL_WEIGHT_SNAPSHOT, LAST_CLAIMED_EPOCH}; use crate::{error::ContractError, helpers, state::FLOWS}; //todo abstract code in this function as most of it is also used in get_rewards.rs +pub const EPOCH_CLAIM_CAP: u64 = 100u64; + #[allow(unused_assignments)] /// Performs the claim function, returning all the [`CosmosMsg`]'s to run. pub fn claim(deps: &mut DepsMut, info: &MessageInfo) -> Result, ContractError> { @@ -34,8 +37,15 @@ pub fn claim(deps: &mut DepsMut, info: &MessageInfo) -> Result, C let mut last_user_weight_seen: Uint128 = Uint128::zero(); //let mut last_user_weight_seen: (EpochId, Uint128) = (064, Uint128::zero()); for flow in flows.iter_mut() { + let expanded_default_values = (flow.flow_asset.amount, flow.end_epoch); + + let (_, (expanded_asset_amount, expanded_end_epoch)) = flow + .asset_history + .last_key_value() + .unwrap_or((&0u64, &expanded_default_values)); + // check if flow already ended and if everything has been claimed for that flow. - if current_epoch > flow.end_epoch && flow.claimed_amount == flow.flow_asset.amount { + if current_epoch > *expanded_end_epoch && flow.claimed_amount == expanded_asset_amount { // if so, skip flow. continue; } @@ -68,13 +78,21 @@ pub fn claim(deps: &mut DepsMut, info: &MessageInfo) -> Result, C } }; + let mut epoch_count = 0; + // calculate the total reward for this flow, from the first claimable epoch to the current epoch for epoch_id in first_claimable_epoch..=current_epoch { + epoch_count += 1; + + if epoch_count > EPOCH_CLAIM_CAP { + break; + } + // check if the flow is active in this epoch if epoch_id < flow.start_epoch { // the flow is not active yet, skip continue; - } else if epoch_id >= flow.end_epoch { + } else if epoch_id >= *expanded_end_epoch { // this flow has finished // todo maybe we should make end_epoch inclusive? break; @@ -93,18 +111,20 @@ pub fn claim(deps: &mut DepsMut, info: &MessageInfo) -> Result, C // statement above is true. let previous_emission = *flow .emitted_tokens - .get(&(epoch_id - 1u64)) + .get(&(epoch_id.saturating_sub(1u64))) .unwrap_or(&Uint128::zero()); previous_emission }; - // emission = (total_tokens - emitted_tokens_at_epoch) / (flow_start + flow_duration - epoch) = (total_tokens - emitted_tokens_at_epoch) / (flow_end - epoch) - let emission_per_epoch = flow - .flow_asset - .amount + // use the flow asset amount at the current epoch considering flow expansions + let flow_asset_amount = get_flow_asset_amount_at_epoch(flow, epoch_id); + let flow_expanded_end_epoch = get_flow_current_end_epoch(flow, epoch_id); + + // emission = (total_tokens_for_epoch_considering_expansion - emitted_tokens_at_epoch) / (flow_start + flow_duration - epoch) = (total_tokens - emitted_tokens_at_epoch) / (flow_end - epoch) + let emission_per_epoch = flow_asset_amount .saturating_sub(emitted_tokens) - .checked_div(Uint128::from(flow.end_epoch - epoch_id))?; + .checked_div(Uint128::from(flow_expanded_end_epoch - epoch_id))?; // record the emitted tokens for this epoch if it hasn't been recorded before. // emitted tokens for this epoch is the total emitted tokens in previous epoch + the ones @@ -155,7 +175,7 @@ pub fn claim(deps: &mut DepsMut, info: &MessageInfo) -> Result, C // sanity check for user_reward_at_epoch if user_reward_at_epoch > emission_per_epoch - || user_reward_at_epoch.checked_add(flow.claimed_amount)? > flow.flow_asset.amount + || user_reward_at_epoch.checked_add(flow.claimed_amount)? > *expanded_asset_amount { return Err(ContractError::InvalidReward {}); } diff --git a/contracts/liquidity_hub/pool-network/incentive/src/contract.rs b/contracts/liquidity_hub/pool-network/incentive/src/contract.rs index eac9cf96..bd525433 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/contract.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/contract.rs @@ -9,12 +9,13 @@ use white_whale::pool_network::incentive::{ }; use semver::Version; +use white_whale::migrate_guards::check_contract_name; use white_whale::pool_network::asset::AssetInfo; use crate::error::ContractError; use crate::error::ContractError::MigrateInvalidVersion; use crate::state::{CONFIG, FLOW_COUNTER, GLOBAL_WEIGHT}; -use crate::{execute, queries}; +use crate::{execute, migrations, queries}; // version info for migration info const CONTRACT_NAME: &str = "white_whale-incentive"; @@ -83,8 +84,20 @@ pub fn execute( end_epoch, curve, flow_asset, - } => execute::open_flow(deps, env, info, start_epoch, end_epoch, curve, flow_asset), - ExecuteMsg::CloseFlow { flow_id } => execute::close_flow(deps, info, flow_id), + flow_label, + } => execute::open_flow( + deps, + env, + info, + start_epoch, + end_epoch, + curve, + flow_asset, + flow_label, + ), + ExecuteMsg::CloseFlow { flow_identifier } => { + execute::close_flow(deps, info, flow_identifier) + } ExecuteMsg::OpenPosition { amount, unbonding_duration, @@ -100,6 +113,11 @@ pub fn execute( } ExecuteMsg::Withdraw {} => execute::withdraw(deps, env, info), ExecuteMsg::Claim {} => execute::claim(deps, info), + ExecuteMsg::ExpandFlow { + flow_identifier, + end_epoch, + flow_asset, + } => execute::expand_flow(deps, info, env, flow_identifier, end_epoch, flow_asset), } } @@ -108,8 +126,24 @@ pub fn execute( pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result { match msg { QueryMsg::Config {} => Ok(to_binary(&queries::get_config(deps)?)?), - QueryMsg::Flow { flow_id } => Ok(to_binary(&queries::get_flow(deps, flow_id)?)?), - QueryMsg::Flows {} => Ok(to_binary(&queries::get_flows(deps)?)?), + QueryMsg::Flow { + flow_identifier, + start_epoch, + end_epoch, + } => Ok(to_binary(&queries::get_flow( + deps, + flow_identifier, + start_epoch, + end_epoch, + )?)?), + QueryMsg::Flows { + start_epoch, + end_epoch, + } => Ok(to_binary(&queries::get_flows( + deps, + start_epoch, + end_epoch, + )?)?), QueryMsg::Positions { address } => { Ok(to_binary(&queries::get_positions(deps, env, address)?)?) } @@ -125,7 +159,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> Result Result { +pub fn migrate(mut deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + check_contract_name(deps.storage, CONTRACT_NAME.to_string())?; + let version: Version = CONTRACT_VERSION.parse()?; let storage_version: Version = get_contract_version(deps.storage)?.version.parse()?; @@ -136,6 +172,10 @@ pub fn migrate(deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result for ContractError { diff --git a/contracts/liquidity_hub/pool-network/incentive/src/execute/close_flow.rs b/contracts/liquidity_hub/pool-network/incentive/src/execute/close_flow.rs index e46a10d4..3ee09e3f 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/execute/close_flow.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/execute/close_flow.rs @@ -1,8 +1,9 @@ use cosmwasm_std::{ coins, to_binary, BankMsg, CosmosMsg, DepsMut, MessageInfo, Order, Response, StdResult, WasmMsg, }; + use white_whale::pool_network::asset::AssetInfo; -use white_whale::pool_network::incentive::Flow; +use white_whale::pool_network::incentive::{Flow, FlowIdentifier}; use crate::{ error::ContractError, @@ -13,7 +14,7 @@ use crate::{ pub fn close_flow( deps: DepsMut, info: MessageInfo, - flow_id: u64, + flow_identifier: FlowIdentifier, ) -> Result { // validate that user is allowed to close the flow let config = CONFIG.load(deps.storage)?; @@ -27,14 +28,17 @@ pub fn close_flow( .range(deps.storage, None, None, Order::Ascending) .collect::>>()? .into_iter() - .find(|(_, flow)| flow.flow_id == flow_id) + .find(|(_, flow)| match &flow_identifier.clone() { + FlowIdentifier::Id(id) => flow.flow_id == *id, + FlowIdentifier::Label(label) => flow.flow_label.as_ref() == Some(label), + }) .ok_or(ContractError::NonExistentFlow { - invalid_id: flow_id, + invalid_identifier: flow_identifier.clone(), }) .map(|(_, flow)| flow)?; if !(flow.flow_creator == info.sender || info.sender == factory_config.owner) { - return Err(ContractError::UnauthorizedFlowClose { flow_id }); + return Err(ContractError::UnauthorizedFlowClose { flow_identifier }); } let amount_to_return = flow.flow_asset.amount.saturating_sub(flow.claimed_amount); @@ -63,7 +67,7 @@ pub fn close_flow( Ok(Response::default() .add_attributes(vec![ ("action", "close_flow".to_string()), - ("flow_id", flow_id.to_string()), + ("flow_identifier", flow_identifier.to_string()), ]) .add_messages(messages)) } diff --git a/contracts/liquidity_hub/pool-network/incentive/src/execute/expand_flow.rs b/contracts/liquidity_hub/pool-network/incentive/src/execute/expand_flow.rs new file mode 100644 index 00000000..1418fa83 --- /dev/null +++ b/contracts/liquidity_hub/pool-network/incentive/src/execute/expand_flow.rs @@ -0,0 +1,195 @@ +use cosmwasm_std::{ + to_binary, CosmosMsg, DepsMut, Env, MessageInfo, Order, OverflowError, OverflowOperation, + Response, StdResult, Uint128, WasmMsg, +}; + +use white_whale::pool_network::asset::{Asset, AssetInfo}; +use white_whale::pool_network::incentive::{Flow, FlowIdentifier}; + +use crate::error::ContractError; +use crate::execute::open_flow::DEFAULT_FLOW_DURATION; +use crate::helpers; +use crate::helpers::{get_flow_asset_amount_at_epoch, get_flow_end_epoch}; +use crate::state::{EpochId, FlowId, FLOWS}; + +// If the end_epoch is not specified, the flow will be expanded by DEFAULT_FLOW_DURATION when +// the current epoch is within FLOW_EXPANSION_BUFFER epochs from the end_epoch. +const FLOW_EXPANSION_BUFFER: u64 = 5u64; +// A flow can only be expanded for a maximum of FLOW_EXPANSION_LIMIT epochs. If that limit is exceeded, +// the flow is "reset", shifting the start_epoch to the current epoch and the end_epoch to the current_epoch + DEFAULT_FLOW_DURATION. +// Unclaimed assets become the flow.asset and both the flow.asset_history and flow.emitted_tokens is cleared. +const FLOW_EXPANSION_LIMIT: u64 = 180u64; + +/// Expands a flow with the given id. Can be done by anyone. +pub fn expand_flow( + deps: DepsMut, + info: MessageInfo, + env: Env, + flow_identifier: FlowIdentifier, + end_epoch: Option, + flow_asset: Asset, +) -> Result { + let flow: Option<((EpochId, FlowId), Flow)> = FLOWS + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()? + .into_iter() + .find(|(_, flow)| match &flow_identifier.clone() { + FlowIdentifier::Id(id) => flow.flow_id == *id, + FlowIdentifier::Label(label) => flow.flow_label.as_ref() == Some(label), + }); + + if let Some((_, mut flow)) = flow { + // check if the flow has already ended + let current_epoch = helpers::get_current_epoch(deps.as_ref())?; + let expanded_end_epoch = get_flow_end_epoch(&flow); + + if current_epoch > expanded_end_epoch { + return Err(ContractError::FlowAlreadyEnded {}); + } + + if flow.flow_asset.info != flow_asset.info { + return Err(ContractError::FlowAssetNotSent {}); + } + + let mut messages: Vec = vec![]; + + // validate that the flow asset is sent to the contract + match flow_asset.clone().info { + AssetInfo::Token { contract_addr } => { + let allowance: cw20::AllowanceResponse = deps.querier.query_wasm_smart( + contract_addr.clone(), + &cw20::Cw20QueryMsg::Allowance { + owner: info.sender.clone().into_string(), + spender: env.contract.address.clone().into_string(), + }, + )?; + + if allowance.allowance < flow_asset.amount { + return Err(ContractError::FlowAssetNotSent); + } + + // create the transfer message to send the flow asset to the contract + messages.push( + WasmMsg::Execute { + contract_addr, + msg: to_binary(&cw20::Cw20ExecuteMsg::TransferFrom { + owner: info.sender.into_string(), + recipient: env.contract.address.into_string(), + amount: flow_asset.amount, + })?, + funds: vec![], + } + .into(), + ); + } + AssetInfo::NativeToken { denom } => { + let paid_amount = cw_utils::must_pay(&info, &denom)?; + if paid_amount != flow_asset.amount { + return Err(ContractError::MissingPositionDepositNative { + desired_amount: flow_asset.amount, + deposited_amount: paid_amount, + }); + } + // all good, native tokens were sent + } + } + + // expand the flow only if the the epoch is within the expansion buffer. + let expand_until = + if expanded_end_epoch.saturating_sub(current_epoch) < FLOW_EXPANSION_BUFFER { + expanded_end_epoch + .checked_add(DEFAULT_FLOW_DURATION) + .ok_or(ContractError::InvalidEndEpoch {})? + } else { + expanded_end_epoch + }; + + let end_epoch = end_epoch.unwrap_or(expand_until); + + // if the current end_epoch of this flow is greater than the new end_epoch, return error as + // it wouldn't be expanding but contracting a flow. + if expanded_end_epoch > end_epoch { + return Err(ContractError::InvalidEndEpoch {}); + } + + let mut attributes = vec![("action", "expand_flow".to_string())]; + + // check if the flow will be reset, i.e. asset history will be cleared + if expanded_end_epoch.saturating_sub(flow.start_epoch) > FLOW_EXPANSION_LIMIT { + // if the flow is being reset, shift the start_epoch to the current epoch, clear the map histories, + // and make the flow_asset the remaining amount that has not been claimed. + + FLOWS.remove(deps.storage, (flow.start_epoch, flow.flow_id)); + + let flow_amount_default_value = (flow_asset.amount, 0u64); + + let (_, (flow_amount, _)) = flow + .asset_history + .last_key_value() + .unwrap_or((&0u64, &flow_amount_default_value)); + + flow.flow_asset = Asset { + info: flow_asset.info.clone(), + amount: flow_amount.saturating_sub(flow.claimed_amount), + }; + + flow.start_epoch = current_epoch; + flow.end_epoch = expanded_end_epoch; + flow.claimed_amount = Uint128::zero(); + flow.asset_history.clear(); + flow.emitted_tokens.clear(); + + attributes.push(("flow reset", "true".to_string())); + } + + // expand amount and end_epoch for the flow. The expansion happens from the next epoch. + let next_epoch = current_epoch.checked_add(1u64).map_or_else( + || { + Err(OverflowError { + operation: OverflowOperation::Add, + operand1: current_epoch.to_string(), + operand2: 1u64.to_string(), + }) + }, + Ok, + )?; + + if let Some((existing_amount, expanded_end_epoch)) = flow.asset_history.get_mut(&next_epoch) + { + *existing_amount = existing_amount.checked_add(flow_asset.amount)?; + *expanded_end_epoch = end_epoch; + } else { + // if there's no entry for the previous epoch, i.e. it is the first time the flow is expanded, + // default to the original flow asset amount + + let expanded_amount = get_flow_asset_amount_at_epoch(&flow, current_epoch); + + flow.asset_history.insert( + next_epoch, + (expanded_amount.checked_add(flow_asset.amount)?, end_epoch), + ); + } + + FLOWS.save(deps.storage, (flow.start_epoch, flow.flow_id), &flow)?; + + let total_flow_asset = flow + .asset_history + .values() + .map(|&(expanded_amount, _)| expanded_amount) + .sum::() + .checked_add(flow.flow_asset.amount)?; + + attributes.append(&mut vec![ + ("flow_id", flow.flow_id.to_string()), + ("end_epoch", end_epoch.to_string()), + ("expanding_flow_asset", flow_asset.to_string()), + ("total_flow_asset", total_flow_asset.to_string()), + ]); + + Ok(Response::default().add_attributes(attributes)) + } else { + Err(ContractError::NonExistentFlow { + invalid_identifier: flow_identifier, + }) + } +} diff --git a/contracts/liquidity_hub/pool-network/incentive/src/execute/mod.rs b/contracts/liquidity_hub/pool-network/incentive/src/execute/mod.rs index 452096c9..40985398 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/execute/mod.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/execute/mod.rs @@ -1,6 +1,7 @@ mod claim; mod close_flow; mod close_position; +mod expand_flow; mod expand_position; mod open_flow; mod open_position; @@ -10,6 +11,7 @@ mod withdraw; pub use claim::claim; pub use close_flow::close_flow; pub use close_position::close_position; +pub use expand_flow::expand_flow; pub use expand_position::expand_position; pub use open_flow::open_flow; pub use open_position::open_position; diff --git a/contracts/liquidity_hub/pool-network/incentive/src/execute/open_flow.rs b/contracts/liquidity_hub/pool-network/incentive/src/execute/open_flow.rs index bd639b02..b4d566f0 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/execute/open_flow.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/execute/open_flow.rs @@ -19,16 +19,19 @@ use crate::{ }; const MIN_FLOW_AMOUNT: Uint128 = Uint128::new(1_000u128); +pub const DEFAULT_FLOW_DURATION: u64 = 14u64; /// Opens a flow to incentivize liquidity providers +#[allow(clippy::too_many_arguments)] pub fn open_flow( deps: DepsMut, env: Env, info: MessageInfo, start_epoch: Option, - end_epoch: u64, - curve: Curve, + end_epoch: Option, + curve: Option, mut flow_asset: Asset, + flow_label: Option, ) -> Result { // check the user is not trying to create an empty flow if flow_asset.amount < MIN_FLOW_AMOUNT { @@ -319,6 +322,11 @@ pub fn open_flow( } let current_epoch = helpers::get_current_epoch(deps.as_ref())?; + let end_epoch = end_epoch.unwrap_or( + current_epoch + .checked_add(DEFAULT_FLOW_DURATION) + .ok_or(ContractError::InvalidEndEpoch {})?, + ); // ensure the flow is set for a expire date in the future if current_epoch > end_epoch { @@ -344,32 +352,43 @@ pub fn open_flow( let flow_id = FLOW_COUNTER.update::<_, StdError>(deps.storage, |current_id| Ok(current_id + 1u64))?; + let curve = curve.unwrap_or(Curve::Linear); + FLOWS.save( deps.storage, (start_epoch, flow_id), &Flow { flow_creator: info.sender.clone(), flow_id, + flow_label: flow_label.clone(), curve: curve.clone(), flow_asset: flow_asset.clone(), claimed_amount: Uint128::zero(), start_epoch, end_epoch, emitted_tokens: HashMap::new(), + asset_history: Default::default(), }, )?; + let mut attributes = vec![("action", "open_flow".to_string())]; + + if let Some(flow_label) = flow_label { + attributes.push(("flow_label", flow_label)); + } + + attributes.extend(vec![ + ("flow_id", flow_id.to_string()), + ("flow_creator", info.sender.into_string()), + ("flow_asset", flow_asset.info.to_string()), + ("flow_asset_amount", flow_asset.amount.to_string()), + ("start_epoch", start_epoch.to_string()), + ("end_epoch", end_epoch.to_string()), + ("emissions_per_epoch", emissions_per_epoch.to_string()), + ("curve", curve.to_string()), + ]); + Ok(Response::default() - .add_attributes(vec![ - ("action", "open_flow".to_string()), - ("flow_id", flow_id.to_string()), - ("flow_creator", info.sender.into_string()), - ("flow_asset", flow_asset.info.to_string()), - ("flow_asset_amount", flow_asset.amount.to_string()), - ("start_epoch", start_epoch.to_string()), - ("end_epoch", end_epoch.to_string()), - ("emissions_per_epoch", emissions_per_epoch.to_string()), - ("curve", curve.to_string()), - ]) + .add_attributes(attributes) .add_messages(messages)) } diff --git a/contracts/liquidity_hub/pool-network/incentive/src/helpers.rs b/contracts/liquidity_hub/pool-network/incentive/src/helpers.rs index 7493fccc..297185e7 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/helpers.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/helpers.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, Deps, DepsMut, Order, StdResult, Uint128}; +use cosmwasm_std::{Addr, Deps, DepsMut, Order, StdError, StdResult, Uint128}; use white_whale::pool_network::incentive::Flow; @@ -58,3 +58,65 @@ pub fn delete_weight_history_for_user( }); Ok(()) } + +/// Gets the flow asset amount for a given epoch, taking into account the asset history, i.e. flow expansion. +pub fn get_flow_asset_amount_at_epoch(flow: &Flow, epoch: u64) -> Uint128 { + let mut asset_amount = flow.flow_asset.amount; + + if let Some((_, &(change_amount, _))) = flow.asset_history.range(..=epoch).rev().next() { + asset_amount = change_amount; + } + + asset_amount +} + +/// Gets the flow end_epoch, taking into account flow expansion. +pub fn get_flow_end_epoch(flow: &Flow) -> u64 { + let mut end_epoch = flow.end_epoch; + + if let Some((_, &(_, expanded_end_epoch))) = flow.asset_history.last_key_value() { + end_epoch = expanded_end_epoch; + } + + end_epoch +} + +/// Gets the flow end_epoch, taking into account flow expansion. +pub fn get_flow_current_end_epoch(flow: &Flow, epoch: u64) -> u64 { + let mut end_epoch = flow.end_epoch; + + if let Some((_, &(_, current_end_epoch))) = flow.asset_history.range(..=epoch).rev().next() { + end_epoch = current_end_epoch; + } + + end_epoch +} + +pub const MAX_EPOCH_LIMIT: u64 = 100; + +/// Gets a [Flow] filtering the asset history and emitted tokens to the given range of epochs. +pub fn get_filtered_flow( + mut flow: Flow, + start_epoch: Option, + end_epoch: Option, +) -> StdResult { + let start_range = start_epoch.unwrap_or(flow.start_epoch); + let mut end_range = end_epoch.unwrap_or( + start_range + .checked_add(MAX_EPOCH_LIMIT) + .ok_or_else(|| StdError::generic_err("Overflow"))?, + ); + + if end_range.saturating_sub(start_range) > MAX_EPOCH_LIMIT { + end_range = start_range + .checked_add(MAX_EPOCH_LIMIT) + .ok_or_else(|| StdError::generic_err("Overflow"))?; + } + + flow.asset_history + .retain(|&k, _| k >= start_range && k <= end_range); + flow.emitted_tokens + .retain(|k, _| *k >= start_range && *k <= end_range); + + Ok(flow) +} diff --git a/contracts/liquidity_hub/pool-network/incentive/src/migrations.rs b/contracts/liquidity_hub/pool-network/incentive/src/migrations.rs index a5da0d4f..a65fb86c 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/migrations.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/migrations.rs @@ -1,2 +1,67 @@ +#![cfg(not(tarpaulin_include))] + +use std::collections::{BTreeMap, HashMap}; + +use cosmwasm_schema::cw_serde; // currently a stub file // until migrations are needed in the future +use cosmwasm_std::{Addr, DepsMut, Order, StdError, StdResult, Uint128}; +use cw_storage_plus::Map; + +use white_whale::pool_network::asset::Asset; +use white_whale::pool_network::incentive::{Curve, Flow}; + +use crate::state::{EpochId, FlowId, FLOWS}; + +/// Migrates to version 1.0.6, which introduces the [Flow] field asset_history. +pub(crate) fn migrate_to_v106(deps: DepsMut) -> Result<(), StdError> { + #[cw_serde] + pub struct FlowV104 { + /// A unique identifier of the flow. + pub flow_id: u64, + /// The account which opened the flow and can manage it. + pub flow_creator: Addr, + /// The asset the flow was created to distribute. + pub flow_asset: Asset, + /// The amount of the `flow_asset` that has been claimed so far. + pub claimed_amount: Uint128, + /// The type of curve the flow has. + pub curve: Curve, //todo not doing anything for now + /// The epoch at which the flow starts. + pub start_epoch: u64, + /// The epoch at which the flow ends. + pub end_epoch: u64, + /// emitted tokens + pub emitted_tokens: HashMap, + } + + // load old flows map + pub const FLOWS_V104: Map<(EpochId, FlowId), FlowV104> = Map::new("flows"); + + let flows = FLOWS_V104 + .range(deps.storage, None, None, Order::Ascending) + .collect::>>()? + .into_iter() + .map(|(_, flow)| flow) + .collect::>(); + + // add the asset_history field to all available flows + for f in flows.iter() { + let flow = Flow { + flow_id: f.clone().flow_id, + flow_label: None, //new field + flow_creator: f.clone().flow_creator, + flow_asset: f.clone().flow_asset, + claimed_amount: f.clone().claimed_amount, + curve: f.clone().curve, + start_epoch: f.clone().start_epoch, + end_epoch: f.clone().end_epoch, + emitted_tokens: f.clone().emitted_tokens, + asset_history: BTreeMap::new(), //new field + }; + + FLOWS.save(deps.storage, (f.start_epoch, f.flow_id), &flow)?; + } + + Ok(()) +} diff --git a/contracts/liquidity_hub/pool-network/incentive/src/queries/get_flow.rs b/contracts/liquidity_hub/pool-network/incentive/src/queries/get_flow.rs index deca02ac..b36c0d05 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/queries/get_flow.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/queries/get_flow.rs @@ -1,14 +1,29 @@ use cosmwasm_std::{Deps, Order, StdError, StdResult}; -use white_whale::pool_network::incentive::{Flow, FlowResponse}; +use white_whale::pool_network::incentive::{Flow, FlowIdentifier, FlowResponse}; +use crate::helpers::get_filtered_flow; use crate::state::FLOWS; -pub fn get_flow(deps: Deps, flow_id: u64) -> Result, StdError> { - Ok(FLOWS +/// Gets a flow given the [FlowIdentifier]. +pub fn get_flow( + deps: Deps, + flow_identifier: FlowIdentifier, + start_epoch: Option, + end_epoch: Option, +) -> Result, StdError> { + FLOWS .range(deps.storage, None, None, Order::Ascending) .collect::>>()? .into_iter() - .find(|(_, flow)| flow.flow_id == flow_id) - .map(|(_, flow)| FlowResponse { flow: Some(flow) })) + .find(|(_, flow)| match &flow_identifier { + FlowIdentifier::Id(id) => flow.flow_id == *id, + FlowIdentifier::Label(label) => flow.flow_label.as_ref() == Some(label), + }) + .map(|(_, flow)| { + get_filtered_flow(flow, start_epoch, end_epoch).map(|filtered_flow| FlowResponse { + flow: Some(filtered_flow), + }) + }) + .transpose() } diff --git a/contracts/liquidity_hub/pool-network/incentive/src/queries/get_flows.rs b/contracts/liquidity_hub/pool-network/incentive/src/queries/get_flows.rs index ec735950..bf05d81f 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/queries/get_flows.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/queries/get_flows.rs @@ -2,14 +2,19 @@ use cosmwasm_std::{Deps, Order, StdError, StdResult}; use white_whale::pool_network::incentive::Flow; +use crate::helpers::get_filtered_flow; use crate::state::FLOWS; /// Retrieves all the current flows that exist. -pub fn get_flows(deps: Deps) -> Result, StdError> { - Ok(FLOWS +pub fn get_flows( + deps: Deps, + start_epoch: Option, + end_epoch: Option, +) -> Result, StdError> { + FLOWS .range(deps.storage, None, None, Order::Ascending) .collect::>>()? .into_iter() - .map(|(_, flow)| flow) - .collect::>()) + .map(|(_, flow)| get_filtered_flow(flow, start_epoch, end_epoch)) + .collect::>>() } diff --git a/contracts/liquidity_hub/pool-network/incentive/src/queries/get_rewards.rs b/contracts/liquidity_hub/pool-network/incentive/src/queries/get_rewards.rs index 01316e8e..62a457a0 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/queries/get_rewards.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/queries/get_rewards.rs @@ -4,6 +4,7 @@ use white_whale::pool_network::{asset::Asset, incentive::RewardsResponse}; use crate::error::ContractError; use crate::helpers; +use crate::helpers::{get_flow_asset_amount_at_epoch, get_flow_current_end_epoch}; use crate::state::{EpochId, ADDRESS_WEIGHT_HISTORY, GLOBAL_WEIGHT_SNAPSHOT, LAST_CLAIMED_EPOCH}; #[allow(unused_assignments)] @@ -26,9 +27,16 @@ pub fn get_rewards(deps: Deps, address: String) -> Result flow.end_epoch && flow.claimed_amount == flow.flow_asset.amount { + if current_epoch > *expanded_end_epoch && flow.claimed_amount == expanded_asset_amount { // if so, skip flow. continue; } @@ -69,7 +77,7 @@ pub fn get_rewards(deps: Deps, address: String) -> Result= flow.end_epoch { + } else if epoch_id >= *expanded_end_epoch { // this flow has finished // todo maybe we should make end_epoch inclusive? break; @@ -87,18 +95,20 @@ pub fn get_rewards(deps: Deps, address: String) -> Result Result emission_per_epoch - || user_reward_at_epoch.checked_add(flow.claimed_amount)? > flow.flow_asset.amount + || user_reward_at_epoch.checked_add(flow.claimed_amount)? > *expanded_asset_amount { return Err(ContractError::InvalidReward {}); } diff --git a/contracts/liquidity_hub/pool-network/incentive/src/tests/helpers.rs b/contracts/liquidity_hub/pool-network/incentive/src/tests/helpers.rs new file mode 100644 index 00000000..b6ef9f2c --- /dev/null +++ b/contracts/liquidity_hub/pool-network/incentive/src/tests/helpers.rs @@ -0,0 +1,193 @@ +use std::collections::{BTreeMap, HashMap}; + +use cosmwasm_std::{Addr, Uint128}; + +use white_whale::pool_network::asset::{Asset, AssetInfo}; +use white_whale::pool_network::incentive::{Curve, Flow}; + +use crate::helpers::{get_filtered_flow, get_flow_asset_amount_at_epoch}; + +#[test] +fn test_get_flow_asset_amount_at_epoch_with_expansion() { + let mut asset_history = BTreeMap::new(); + asset_history.insert(0, (Uint128::from(10000u128), 105u64)); + asset_history.insert(7, (Uint128::from(20000u128), 110u64)); + asset_history.insert(10, (Uint128::from(50000u128), 115u64)); + let flow = Flow { + flow_id: 1, + flow_label: None, + flow_creator: Addr::unchecked("creator"), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::from(10000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 0, + end_epoch: 100, + emitted_tokens: HashMap::new(), + asset_history, + }; + + // Before any change + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 0), + Uint128::from(10000u128) + ); + + // After first change but before second change + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 6), + Uint128::from(10000u128) + ); + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 7), + Uint128::from(20000u128) + ); + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 9), + Uint128::from(20000u128) + ); + + // After second change + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 10), + Uint128::from(50000u128) + ); + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 11), + Uint128::from(50000u128) + ); + + // After the end epoch + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 101), + Uint128::from(50000u128) + ); +} + +#[test] +fn test_get_flow_asset_amount_at_epoch_without_expansion() { + let asset_history = BTreeMap::new(); + + let flow = Flow { + flow_id: 1, + flow_label: None, + flow_creator: Addr::unchecked("creator"), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::from(10000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 0, + end_epoch: 100, + emitted_tokens: HashMap::new(), + asset_history, + }; + + // Before any change + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 0), + Uint128::from(10000u128) + ); + + // After first change but before second change + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 6), + Uint128::from(10000u128) + ); + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 7), + Uint128::from(10000u128) + ); + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 9), + Uint128::from(10000u128) + ); + + // After second change + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 10), + Uint128::from(10000u128) + ); + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 11), + Uint128::from(10000u128) + ); + + // After the end epoch + assert_eq!( + get_flow_asset_amount_at_epoch(&flow, 101), + Uint128::from(10000u128) + ); +} + +#[test] +fn get_filtered_flow_cases() { + let flow = Flow { + flow_id: 1, + flow_label: None, + flow_creator: Addr::unchecked("creator"), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::from(10000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 1, + end_epoch: 100, + emitted_tokens: HashMap::from_iter((1..105).map(|i| { + ( + i, + (Uint128::from(10000u128) + .checked_add(Uint128::from(i)) + .unwrap()), + ) + })), + asset_history: BTreeMap::from_iter((1..105).map(|i| { + ( + i, + ( + Uint128::from(10000u128) + .checked_add(Uint128::from(i)) + .unwrap(), + 105u64, + ), + ) + })), + }; + + assert!(flow.asset_history.get(&104).is_some()); + + let filtered_flow = get_filtered_flow(flow.clone(), None, None).unwrap(); + assert!(filtered_flow.asset_history.get(&104).is_none()); + assert_eq!(filtered_flow.emitted_tokens.len(), 101usize); + assert_eq!(filtered_flow.asset_history.len(), 101usize); + + let filtered_flow = get_filtered_flow(flow.clone(), Some(55u64), None).unwrap(); + assert!(filtered_flow.asset_history.get(&54).is_none()); + assert_eq!(filtered_flow.emitted_tokens.len(), 50usize); + assert_eq!(filtered_flow.asset_history.len(), 50usize); + + let filtered_flow = get_filtered_flow(flow.clone(), Some(110), None).unwrap(); + assert!(filtered_flow.asset_history.is_empty()); + assert!(filtered_flow.emitted_tokens.is_empty()); + + let filtered_flow = get_filtered_flow(flow.clone(), Some(11u64), Some(30u64)).unwrap(); + assert!(filtered_flow.asset_history.get(&10).is_none()); + assert!(filtered_flow.emitted_tokens.get(&35).is_none()); + assert_eq!(filtered_flow.emitted_tokens.len(), 20usize); + assert_eq!(filtered_flow.asset_history.len(), 20usize); + + let filtered_flow = get_filtered_flow(flow.clone(), None, Some(50u64)).unwrap(); + assert!(filtered_flow.asset_history.get(&1).is_some()); + assert_eq!(filtered_flow.emitted_tokens.len(), 50usize); + assert_eq!(filtered_flow.asset_history.len(), 50usize); +} diff --git a/contracts/liquidity_hub/pool-network/incentive/src/tests/integration.rs b/contracts/liquidity_hub/pool-network/incentive/src/tests/integration.rs index 1b67c727..6e2aa1aa 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/tests/integration.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/tests/integration.rs @@ -1,10 +1,11 @@ use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap}; use cosmwasm_std::{coin, coins, Addr, Decimal256, Timestamp, Uint128}; use white_whale::pool_network::asset::{Asset, AssetInfo}; use white_whale::pool_network::incentive; -use white_whale::pool_network::incentive::{Curve, Flow, RewardsShareResponse}; +use white_whale::pool_network::incentive::{Curve, Flow, FlowIdentifier, RewardsShareResponse}; use white_whale::pool_network::incentive_factory::IncentivesContract; use crate::error::ContractError; @@ -278,14 +279,15 @@ fn try_open_more_flows_than_allowed() { alice.clone(), incentive_addr.clone().into_inner(), None, - 10u64, - Curve::Linear, + Some(10u64), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".to_string(), }, amount: Uint128::new(i * 2_000u128), }, + None, &vec![coin(i * 2_000u128, "uwhale".to_string())], |result| { if i > 7 { @@ -304,7 +306,7 @@ fn try_open_more_flows_than_allowed() { } let incentive_flows = RefCell::new(vec![]); - suite.query_flows(incentive_addr.clone().into_inner(), |result| { + suite.query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); *incentive_flows.borrow_mut() = flows.clone(); @@ -314,6 +316,7 @@ fn try_open_more_flows_than_allowed() { flows.first().unwrap(), &Flow { flow_id: 1, + flow_label: None, flow_creator: alice.clone(), flow_asset: Asset { info: AssetInfo::NativeToken { @@ -326,12 +329,14 @@ fn try_open_more_flows_than_allowed() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); assert_eq!( flows.last().unwrap(), &Flow { flow_id: 7, + flow_label: None, flow_creator: alice.clone(), flow_asset: Asset { info: AssetInfo::NativeToken { @@ -344,6 +349,7 @@ fn try_open_more_flows_than_allowed() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); }); @@ -395,14 +401,15 @@ fn try_open_flows_with_wrong_epochs() { alice.clone(), incentive_addr.clone().into_inner(), None, - past_epoch.clone(), - Curve::Linear, + Some(past_epoch.clone()), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(2_000u128), }, + None, &vec![coin(2_000u128, "uwhale".to_string())], |result| { let err = result.unwrap_err().downcast::().unwrap(); @@ -419,14 +426,15 @@ fn try_open_flows_with_wrong_epochs() { alice.clone(), incentive_addr.clone().into_inner(), Some(future_future_epoch.clone()), - future_epoch.clone(), - Curve::Linear, + Some(future_epoch.clone()), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(2_000u128), }, + None, &vec![coin(2_000u128, "uwhale".to_string())], |result| { let err = result.unwrap_err().downcast::().unwrap(); @@ -445,14 +453,15 @@ fn try_open_flows_with_wrong_epochs() { Some( current_epoch.clone().into_inner() + max_flow_epoch_buffer.clone().into_inner() + 1, ), - current_epoch.clone().into_inner() + 100, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 100), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(2_000u128), }, + None, &vec![coin(2_000u128, "uwhale".to_string())], |result| { let err = result.unwrap_err().downcast::().unwrap(); @@ -467,14 +476,15 @@ fn try_open_flows_with_wrong_epochs() { alice.clone(), incentive_addr.clone().into_inner(), None, - future_epoch.clone(), - Curve::Linear, + Some(future_epoch.clone()), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(2_000u128), }, + None, &vec![coin(2_000u128, "uwhale".to_string())], |result| { result.unwrap(); @@ -522,14 +532,15 @@ fn open_flow_with_fee_native_token_and_flow_same_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(0u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { // this should fail as not enough funds were sent @@ -545,14 +556,15 @@ fn open_flow_with_fee_native_token_and_flow_same_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { // this should fail as not enough funds were sent to cover for fee + MIN_FLOW_AMOUNT @@ -567,14 +579,15 @@ fn open_flow_with_fee_native_token_and_flow_same_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![coin(100u128, "uwhale".to_string())], |result| { // this should fail as not enough funds were sent to cover for fee + MIN_FLOW_AMOUNT @@ -589,14 +602,15 @@ fn open_flow_with_fee_native_token_and_flow_same_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(2_000u128), }, + None, &vec![coin(500u128, "uwhale".to_string())], |result| { // this should fail as we didn't send enough funds to cover for the fee @@ -611,14 +625,15 @@ fn open_flow_with_fee_native_token_and_flow_same_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(2_000u128), }, + None, &vec![coin(2_000u128, "uwhale".to_string())], |result| { // this should succeed as we sent enough funds to cover for fee + MIN_FLOW_AMOUNT @@ -648,32 +663,42 @@ fn open_flow_with_fee_native_token_and_flow_same_native_token() { assert_eq!(funds, Uint128::new(1_000u128)); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow, - Some(Flow { - flow_id: 1, - flow_creator: carol.clone(), - flow_asset: Asset { - info: AssetInfo::NativeToken { - denom: "uwhale".to_string() + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string() + }, + amount: Uint128::new(1_000u128), }, - amount: Uint128::new(1_000u128), - }, - claimed_amount: Uint128::zero(), - curve: Curve::Linear, - start_epoch: 10u64, - end_epoch: 19u64, - emitted_tokens: Default::default(), - }) - ); - }) - .query_flow(incentive_addr.clone().into_inner(), 5u64, |result| { - // this should not work as there is no flow with id 5 - let flow_response = result.unwrap(); - assert_eq!(flow_response, None); - }); + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 10u64, + end_epoch: 19u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(5u64), + |result| { + // this should not work as there is no flow with id 5 + let flow_response = result.unwrap(); + assert_eq!(flow_response, None); + }, + ); } #[test] @@ -718,14 +743,15 @@ fn open_flow_with_fee_native_token_and_flow_different_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "ampWHALE".clone().to_string(), }, amount: Uint128::new(500u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { // this should fail as MIN_FLOW_AMOUNT is not met @@ -741,14 +767,15 @@ fn open_flow_with_fee_native_token_and_flow_different_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "ampWHALE".clone().to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { // this should fail as the flow asset was not sent @@ -763,14 +790,15 @@ fn open_flow_with_fee_native_token_and_flow_different_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "ampWHALE".clone().to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![ coin(1_000u128, "uwhale".to_string()), coin(500u128, "ampWHALE".to_string()), @@ -788,14 +816,15 @@ fn open_flow_with_fee_native_token_and_flow_different_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "ampWHALE".clone().to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![ coin(100u128, "uwhale".to_string()), coin(1_00u128, "ampWHALE".to_string()), @@ -813,14 +842,15 @@ fn open_flow_with_fee_native_token_and_flow_different_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "ampWHALE".clone().to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![ coin(1_000u128, "uwhale".to_string()), coin(1_000u128, "ampWHALE".to_string()), @@ -874,32 +904,42 @@ fn open_flow_with_fee_native_token_and_flow_different_native_token() { assert_eq!(funds, Uint128::zero()); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow, - Some(Flow { - flow_id: 1, - flow_creator: carol.clone(), - flow_asset: Asset { - info: AssetInfo::NativeToken { - denom: "ampWHALE".to_string() + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "ampWHALE".to_string() + }, + amount: Uint128::new(1_000u128), }, - amount: Uint128::new(1_000u128), - }, - claimed_amount: Uint128::zero(), - curve: Curve::Linear, - start_epoch: 1u64, - end_epoch: 10u64, - emitted_tokens: Default::default(), - }) - ); - }) - .query_flow(incentive_addr.clone().into_inner(), 5u64, |result| { - // this should not work as there is no flow with id 5 - let flow_response = result.unwrap(); - assert_eq!(flow_response, None); - }) + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 1u64, + end_epoch: 10u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(5u64), + |result| { + // this should not work as there is no flow with id 5 + let flow_response = result.unwrap(); + assert_eq!(flow_response, None); + }, + ) .query_funds( carol.clone(), AssetInfo::NativeToken { @@ -914,14 +954,15 @@ fn open_flow_with_fee_native_token_and_flow_different_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "ampWHALE".clone().to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![ coin(50_000u128, "uwhale".to_string()), coin(1_000u128, "ampWHALE".to_string()), @@ -992,12 +1033,13 @@ fn open_flow_with_fee_native_token_and_flow_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_incentive.clone(), amount: Uint128::new(500u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { // this should fail as MIN_FLOW_AMOUNT is not met @@ -1013,12 +1055,13 @@ fn open_flow_with_fee_native_token_and_flow_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_incentive.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { // this should fail as the flow asset was not sent, i.e. Allowance was not increased @@ -1040,12 +1083,13 @@ fn open_flow_with_fee_native_token_and_flow_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_incentive.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { // this should succeed as the allowance was increased @@ -1091,25 +1135,31 @@ fn open_flow_with_fee_native_token_and_flow_cw20_token() { assert_eq!(funds, Uint128::zero()); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow, - Some(Flow { - flow_id: 1, - flow_creator: carol.clone(), - flow_asset: Asset { - info: cw20_incentive.clone(), - amount: Uint128::new(1_000u128), - }, - claimed_amount: Uint128::zero(), - curve: Curve::Linear, - start_epoch: 1u64, - end_epoch: 10u64, - emitted_tokens: Default::default(), - }) - ); - }); + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: cw20_incentive.clone(), + amount: Uint128::new(1_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 1u64, + end_epoch: 10u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }, + ); } #[test] @@ -1158,12 +1208,13 @@ fn open_flow_with_fee_cw20_token_and_flow_same_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(500u128), }, + None, &vec![], |result| { // this should fail as not enough funds were sent @@ -1179,12 +1230,13 @@ fn open_flow_with_fee_cw20_token_and_flow_same_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![], |result| { // this should fail as not enough funds were sent to cover for fee @@ -1207,12 +1259,13 @@ fn open_flow_with_fee_cw20_token_and_flow_same_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(1_500u128), }, + None, &vec![], |result| { // this should fail as not enough funds were sent to cover for fee and MIN_FLOW_AMOUNT @@ -1234,12 +1287,13 @@ fn open_flow_with_fee_cw20_token_and_flow_same_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(2_000u128), }, + None, &vec![], |result| { // this should succeed as enough funds were sent to cover for fee and MIN_FLOW_AMOUNT @@ -1267,25 +1321,31 @@ fn open_flow_with_fee_cw20_token_and_flow_same_cw20_token() { assert_eq!(funds, Uint128::new(1_000u128)); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow, - Some(Flow { - flow_id: 1, - flow_creator: carol.clone(), - flow_asset: Asset { - info: cw20_asset.clone(), - amount: Uint128::new(1_000u128), - }, - claimed_amount: Uint128::zero(), - curve: Curve::Linear, - start_epoch: 1u64, - end_epoch: 10u64, - emitted_tokens: Default::default(), - }) - ); - }); + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: cw20_asset.clone(), + amount: Uint128::new(1_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 1u64, + end_epoch: 10u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }, + ); } #[test] @@ -1334,12 +1394,13 @@ fn open_flow_with_fee_cw20_token_and_flow_different_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(500u128), }, + None, &vec![], |result| { // this should fail as not enough funds were sent @@ -1355,12 +1416,13 @@ fn open_flow_with_fee_cw20_token_and_flow_different_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![], |result| { // this should fail as the asset to pay for the fee was not transferred @@ -1383,12 +1445,13 @@ fn open_flow_with_fee_cw20_token_and_flow_different_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![], |result| { // this should fail as not enough funds were sent to cover for fee @@ -1411,12 +1474,13 @@ fn open_flow_with_fee_cw20_token_and_flow_different_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![], |result| { // this should fail as not enough funds were sent to cover the flow asset @@ -1439,12 +1503,13 @@ fn open_flow_with_fee_cw20_token_and_flow_different_cw20_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![], |result| { // this should succeed as both the fee was paid in full and the flow asset amount @@ -1487,30 +1552,40 @@ fn open_flow_with_fee_cw20_token_and_flow_different_cw20_token() { assert_eq!(funds, Uint128::new(1_000u128)); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow, - Some(Flow { - flow_id: 1, - flow_creator: carol.clone(), - flow_asset: Asset { - info: cw20_asset.clone(), - amount: Uint128::new(1_000u128), - }, - claimed_amount: Uint128::zero(), - curve: Curve::Linear, - start_epoch: 1u64, - end_epoch: 10u64, - emitted_tokens: Default::default(), - }) - ); - }) - .query_flow(incentive_addr.clone().into_inner(), 5u64, |result| { - // this should not work as there is no flow with id 5 - let flow_response = result.unwrap(); - assert_eq!(flow_response, None); - }); + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: cw20_asset.clone(), + amount: Uint128::new(1_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 1u64, + end_epoch: 10u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(5u64), + |result| { + // this should not work as there is no flow with id 5 + let flow_response = result.unwrap(); + assert_eq!(flow_response, None); + }, + ); } #[test] @@ -1554,14 +1629,15 @@ fn open_flow_with_fee_cw20_token_and_flow_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "usdc".to_string(), }, amount: Uint128::new(500u128), }, + None, &vec![], |result| { // this should fail as not enough funds were sent @@ -1577,14 +1653,15 @@ fn open_flow_with_fee_cw20_token_and_flow_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "usdc".to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![], |result| { // this should fail as the asset to pay for the fee was not transferred @@ -1607,14 +1684,15 @@ fn open_flow_with_fee_cw20_token_and_flow_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "usdc".to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![], |result| { // this should fail as not enough funds were sent to cover for fee @@ -1637,14 +1715,15 @@ fn open_flow_with_fee_cw20_token_and_flow_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "usdc".to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![], |result| { // this should fail as the flow asset was not sent to the contract @@ -1660,14 +1739,15 @@ fn open_flow_with_fee_cw20_token_and_flow_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "usdc".to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![coin(900u128, "usdc".to_string())], |result| { // this should fail as the flow asset was not sent to the contract @@ -1683,14 +1763,15 @@ fn open_flow_with_fee_cw20_token_and_flow_native_token() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "usdc".to_string(), }, amount: Uint128::new(1_000u128), }, + None, &vec![coin(1_000u128, "usdc".to_string())], |result| { // this should succeed as the flow asset was sent to the contract @@ -1736,32 +1817,42 @@ fn open_flow_with_fee_cw20_token_and_flow_native_token() { assert_eq!(funds, Uint128::new(1_000u128)); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow, - Some(Flow { - flow_id: 1, - flow_creator: carol.clone(), - flow_asset: Asset { - info: AssetInfo::NativeToken { - denom: "usdc".to_string(), + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000u128), }, - amount: Uint128::new(1_000u128), - }, - claimed_amount: Uint128::zero(), - curve: Curve::Linear, - start_epoch: 1u64, - end_epoch: 10u64, - emitted_tokens: Default::default(), - }) - ); - }) - .query_flow(incentive_addr.clone().into_inner(), 5u64, |result| { - // this should not work as there is no flow with id 5 - let flow_response = result.unwrap(); - assert_eq!(flow_response, None); - }); + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 1u64, + end_epoch: 10u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(5u64), + |result| { + // this should not work as there is no flow with id 5 + let flow_response = result.unwrap(); + assert_eq!(flow_response, None); + }, + ); } #[test] @@ -1805,14 +1896,15 @@ fn close_native_token_flows() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(2_000u128), }, + None, &vec![coin(2_000u128, "uwhale".to_string())], |result| { result.unwrap(); @@ -1822,20 +1914,21 @@ fn close_native_token_flows() { alice.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(11_000u128), }, + None, &vec![coin(11_000u128, "uwhale".to_string())], |result| { result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert_eq!(flows.len(), 2usize); @@ -1843,6 +1936,7 @@ fn close_native_token_flows() { flows.first().unwrap(), &Flow { flow_id: 1, + flow_label: None, flow_creator: carol.clone(), flow_asset: Asset { info: AssetInfo::NativeToken { @@ -1855,12 +1949,14 @@ fn close_native_token_flows() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); assert_eq!( flows.last().unwrap(), &Flow { flow_id: 2, + flow_label: None, flow_creator: alice.clone(), flow_asset: Asset { info: AssetInfo::NativeToken { @@ -1873,13 +1969,14 @@ fn close_native_token_flows() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); }) .close_incentive_flow( bob.clone(), incentive_addr.clone().into_inner(), - 1u64, + FlowIdentifier::Id(1u64), |result| { // this should error because bob didn't open the flow, nor he is the owner of the incentive let err = result.unwrap_err().downcast::().unwrap(); @@ -1895,7 +1992,7 @@ fn close_native_token_flows() { .close_incentive_flow( carol.clone(), incentive_addr.clone().into_inner(), - 2u64, + FlowIdentifier::Id(2u64), |result| { // this should error because carol didn't open the flow, nor he is the owner of the incentive let err = result.unwrap_err().downcast::().unwrap(); @@ -1921,13 +2018,13 @@ fn close_native_token_flows() { .close_incentive_flow( alice.clone(), incentive_addr.clone().into_inner(), - 2u64, + FlowIdentifier::Id(2u64), |result| { // this should be fine because carol opened the flow result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert_eq!(flows.len(), 1usize); @@ -1935,6 +2032,7 @@ fn close_native_token_flows() { flows.last().unwrap(), &Flow { flow_id: 1, + flow_label: None, flow_creator: carol.clone(), flow_asset: Asset { info: AssetInfo::NativeToken { @@ -1947,6 +2045,7 @@ fn close_native_token_flows() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); }) @@ -1977,7 +2076,7 @@ fn close_native_token_flows() { .close_incentive_flow( alice.clone(), incentive_addr.clone().into_inner(), - 1u64, + FlowIdentifier::Id(1u64), |result| { result.unwrap(); }, @@ -1995,7 +2094,7 @@ fn close_native_token_flows() { ); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert!(flows.is_empty()); }) @@ -2003,13 +2102,13 @@ fn close_native_token_flows() { .close_incentive_flow( bob.clone(), incentive_addr.clone().into_inner(), - 3u64, + FlowIdentifier::Id(3u64), |result| { let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::NonExistentFlow { invalid_id } => { - assert_eq!(invalid_id, 3u64) + ContractError::NonExistentFlow { invalid_identifier } => { + assert_eq!(invalid_identifier, FlowIdentifier::Id(3u64)) } _ => panic!("Wrong error type, should return ContractError::NonExistentFlow"), } @@ -2019,20 +2118,21 @@ fn close_native_token_flows() { alice.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "uwhale".clone().to_string(), }, amount: Uint128::new(5_000u128), }, + None, &vec![coin(5_000u128, "uwhale".to_string())], |result| { result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert_eq!(flows.len(), 1usize); @@ -2040,6 +2140,7 @@ fn close_native_token_flows() { flows.last().unwrap(), &Flow { flow_id: 3, + flow_label: None, flow_creator: alice.clone(), flow_asset: Asset { info: AssetInfo::NativeToken { @@ -2052,6 +2153,7 @@ fn close_native_token_flows() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); }); @@ -2108,12 +2210,13 @@ fn close_cw20_token_flows() { carol.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { result.unwrap(); @@ -2129,18 +2232,19 @@ fn close_cw20_token_flows() { alice.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(10_000u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert_eq!(flows.len(), 2usize); @@ -2148,6 +2252,7 @@ fn close_cw20_token_flows() { flows.first().unwrap(), &Flow { flow_id: 1, + flow_label: None, flow_creator: carol.clone(), flow_asset: Asset { info: cw20_asset.clone(), @@ -2158,12 +2263,14 @@ fn close_cw20_token_flows() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); assert_eq!( flows.last().unwrap(), &Flow { flow_id: 2, + flow_label: None, flow_creator: alice.clone(), flow_asset: Asset { info: cw20_asset.clone(), @@ -2174,13 +2281,14 @@ fn close_cw20_token_flows() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); }) .close_incentive_flow( bob.clone(), incentive_addr.clone().into_inner(), - 1u64, + FlowIdentifier::Id(1u64), |result| { // this should error because bob didn't open the flow, nor he is the owner of the incentive let err = result.unwrap_err().downcast::().unwrap(); @@ -2196,7 +2304,7 @@ fn close_cw20_token_flows() { .close_incentive_flow( carol.clone(), incentive_addr.clone().into_inner(), - 2u64, + FlowIdentifier::Id(2u64), |result| { // this should error because carol didn't open the flow, nor he is the owner of the incentive let err = result.unwrap_err().downcast::().unwrap(); @@ -2216,13 +2324,13 @@ fn close_cw20_token_flows() { .close_incentive_flow( alice.clone(), incentive_addr.clone().into_inner(), - 2u64, + FlowIdentifier::Id(2u64), |result| { // this should be fine because carol opened the flow result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert_eq!(flows.len(), 1usize); @@ -2230,6 +2338,7 @@ fn close_cw20_token_flows() { flows.last().unwrap(), &Flow { flow_id: 1, + flow_label: None, flow_creator: carol.clone(), flow_asset: Asset { info: cw20_asset.clone(), @@ -2240,6 +2349,7 @@ fn close_cw20_token_flows() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); }) @@ -2258,7 +2368,7 @@ fn close_cw20_token_flows() { .close_incentive_flow( alice.clone(), incentive_addr.clone().into_inner(), - 1u64, + FlowIdentifier::Id(1u64), |result| { result.unwrap(); }, @@ -2270,7 +2380,7 @@ fn close_cw20_token_flows() { Uint128::new(1_000u128) ); }) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert!(flows.is_empty()); }) @@ -2278,13 +2388,13 @@ fn close_cw20_token_flows() { .close_incentive_flow( bob.clone(), incentive_addr.clone().into_inner(), - 3u64, + FlowIdentifier::Id(3u64), |result| { let err = result.unwrap_err().downcast::().unwrap(); match err { - ContractError::NonExistentFlow { invalid_id } => { - assert_eq!(invalid_id, 3u64) + ContractError::NonExistentFlow { invalid_identifier } => { + assert_eq!(invalid_identifier, FlowIdentifier::Id(3u64)) } _ => panic!("Wrong error type, should return ContractError::NonExistentFlow"), } @@ -2300,18 +2410,19 @@ fn close_cw20_token_flows() { alice.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 9, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), Asset { info: cw20_asset.clone(), amount: Uint128::new(5_000u128), }, + None, &vec![coin(1_000u128, "uwhale".to_string())], |result| { result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert_eq!(flows.len(), 1usize); @@ -2319,6 +2430,7 @@ fn close_cw20_token_flows() { flows.last().unwrap(), &Flow { flow_id: 3, + flow_label: None, flow_creator: alice.clone(), flow_asset: Asset { info: cw20_asset.clone(), @@ -2329,6 +2441,7 @@ fn close_cw20_token_flows() { start_epoch: 1u64, end_epoch: 10u64, emitted_tokens: Default::default(), + asset_history: Default::default(), } ); }); @@ -2572,14 +2685,15 @@ fn open_flow_positions_and_claim_native_token_incentive() { alice.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 10, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 10), + Some(Curve::Linear), Asset { info: AssetInfo::NativeToken { denom: "usdc".to_string(), }, amount: Uint128::new(1_000_000_000u128), }, + None, &vec![coin(1_000_000_000u128, "usdc"), coin(1_000u128, "uwhale")], |result| { result.unwrap(); @@ -2638,13 +2752,17 @@ fn open_flow_positions_and_claim_native_token_incentive() { ); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow.unwrap().claimed_amount, - Uint128::new(500_000_000u128) - ); - }); + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap().claimed_amount, + Uint128::new(500_000_000u128) + ); + }, + ); // move 3 more epochs, so carol should have 300 more to claim suite @@ -2712,13 +2830,17 @@ fn open_flow_positions_and_claim_native_token_incentive() { result.unwrap(); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow.unwrap().claimed_amount, - Uint128::new(1_000_000_000u128) - ); - }) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap().claimed_amount, + Uint128::new(1_000_000_000u128) + ); + }, + ) .query_funds( carol.clone(), AssetInfo::NativeToken { @@ -2974,12 +3096,13 @@ fn open_flow_positions_claim_cw20_token_incentive() { alice.clone(), incentive_addr.clone().into_inner(), None, - current_epoch.clone().into_inner() + 10, - Curve::Linear, + Some(current_epoch.clone().into_inner() + 10), + Some(Curve::Linear), Asset { info: flow_asset.clone(), amount: Uint128::new(1_000_000_000u128), }, + None, &vec![coin(1_000u128, "uwhale")], |result| { result.unwrap(); @@ -3022,13 +3145,17 @@ fn open_flow_positions_claim_cw20_token_incentive() { .unwrap(), ); }) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow.unwrap().claimed_amount, - Uint128::new(500_000_000u128) - ); - }); + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap().claimed_amount, + Uint128::new(500_000_000u128) + ); + }, + ); // move 3 more epochs, so carol should have 300 more to claim suite @@ -3047,16 +3174,20 @@ fn open_flow_positions_claim_cw20_token_incentive() { ); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow.unwrap().claimed_amount, - Uint128::new(500_000_000u128) - ); - }) - // move 2 more epochs, so carol should have an additional 200_000_000usdc to claim. - .set_time(time.plus_seconds(172800u64)) - .create_epochs_on_fee_distributor(2, vec![incentive_addr.clone().into_inner()]) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap().claimed_amount, + Uint128::new(500_000_000u128) + ); + }, + ) + // move 2 more epochs, so carol should have an additional 200_000_000usdc to claim. + .set_time(time.plus_seconds(172800u64)) + .create_epochs_on_fee_distributor(2, vec![incentive_addr.clone().into_inner()]) .query_rewards( incentive_addr.clone().into_inner(), carol.clone(), @@ -3093,13 +3224,17 @@ fn open_flow_positions_claim_cw20_token_incentive() { result.unwrap(); }, ) - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow.unwrap().claimed_amount, - Uint128::new(1_000_000_000u128) - ); - }) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap().claimed_amount, + Uint128::new(1_000_000_000u128) + ); + }, + ) .query_funds(carol.clone(), flow_asset.clone(), |result| { assert_eq!( result, @@ -3182,13 +3317,14 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { .open_incentive_flow( alice.clone(), incentive_addr.clone().into_inner(), - None, //epoch 11 - current_epoch.clone().into_inner() + 10, // epoch 21 - Curve::Linear, + None, //epoch 11 + Some(current_epoch.clone().into_inner() + 10), // epoch 21 + Some(Curve::Linear), Asset { info: flow_asset_1.clone(), amount: Uint128::new(1_000_000_000u128), }, + None, &vec![ coin(1_000_000_000u128, "ampWHALE"), coin(1_000u128, "uwhale"), @@ -3201,18 +3337,19 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { alice.clone(), incentive_addr.clone().into_inner(), Some(current_epoch.clone().into_inner() + 10), // epoch 21 - current_epoch.clone().into_inner() + 30, //epoch 41 , ends in 30 epochs from the start, i.e. has a duration of 20 epochs - Curve::Linear, + Some(current_epoch.clone().into_inner() + 30), //epoch 41 , ends in 30 epochs from the start, i.e. has a duration of 20 epochs + Some(Curve::Linear), Asset { info: flow_asset_2.clone(), amount: Uint128::new(10_000_000_000u128), }, + None, &vec![coin(10_000_000_000u128, "usdc"), coin(1_000u128, "uwhale")], |result| { result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); println!("flows created:: {:?}", flows); assert_eq!(flows.len(), 2); @@ -3378,7 +3515,7 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { address_weight: Uint128::new(1_000u128), share: Decimal256::from_ratio( Uint128::new(1_000u128), - Uint128::new(6_000u128) + Uint128::new(6_000u128), ), epoch_id: 16u64, } @@ -3475,14 +3612,18 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { }); println!("all good"); - suite.query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow.unwrap().claimed_amount, - // Uint128::new(250_000_000u128) - Uint128::new(249_999_995u128) - ); - }); + suite.query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap().claimed_amount, + // Uint128::new(250_000_000u128) + Uint128::new(249_999_995u128) + ); + }, + ); // move 10 epochs let time = suite.get_time(); @@ -3548,7 +3689,7 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { address_weight: Uint128::new(1_000u128), share: Decimal256::from_ratio( Uint128::new(1_000u128), - Uint128::new(6_000u128) + Uint128::new(6_000u128), ), epoch_id: 26u64, } @@ -3783,7 +3924,7 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { address_weight: Uint128::new(2_000u128), share: Decimal256::from_ratio( Uint128::new(2_000u128), - Uint128::new(4_000u128) + Uint128::new(4_000u128), ), epoch_id: 27u64, } @@ -3810,26 +3951,30 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { // let's close flow 1 suite - .query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - let total_rewards = flow_response - .clone() - .unwrap() - .flow - .unwrap() - .flow_asset - .amount; - let claimed = flow_response.clone().unwrap().flow.unwrap().claimed_amount; - let expected_claimed = total_rewards - Uint128::new(100_000_000u128); - assert!(total_rewards > claimed); - assert!(expected_claimed >= claimed); - - assert!((expected_claimed.u128() as i128 - claimed.u128() as i128).abs() < 10i128); - }) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + let total_rewards = flow_response + .clone() + .unwrap() + .flow + .unwrap() + .flow_asset + .amount; + let claimed = flow_response.clone().unwrap().flow.unwrap().claimed_amount; + let expected_claimed = total_rewards - Uint128::new(100_000_000u128); + assert!(total_rewards > claimed); + assert!(expected_claimed >= claimed); + + assert!((expected_claimed.u128() as i128 - claimed.u128() as i128).abs() < 10i128); + }, + ) .close_incentive_flow( bob.clone(), incentive_addr.clone().into_inner(), - 1u64, + FlowIdentifier::Id(1u64), |result| { let err = result.unwrap_err().downcast::().unwrap(); @@ -3844,7 +3989,7 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { .close_incentive_flow( bob.clone(), incentive_addr.clone().into_inner(), - 5u64, + FlowIdentifier::Id(5u64), |result| { let err = result.unwrap_err().downcast::().unwrap(); @@ -3857,7 +4002,7 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { .close_incentive_flow( alice.clone(), incentive_addr.clone().into_inner(), - 1u64, + FlowIdentifier::Id(1u64), |result| { result.unwrap(); }, @@ -4075,22 +4220,26 @@ fn open_expand_close_flows_positions_and_claim_native_token_incentive() { ); *alice_usdc_funds.borrow_mut() = result; }) - .query_flow(incentive_addr.clone().into_inner(), 2u64, |result| { - let flow_response = result.unwrap(); - let total_rewards = flow_response - .clone() - .unwrap() - .flow - .unwrap() - .flow_asset - .amount; - let claimed = flow_response.clone().unwrap().flow.unwrap().claimed_amount; - let expected_claimed = total_rewards.clone(); - - assert!(total_rewards > claimed); - assert!(expected_claimed >= claimed); - assert!((expected_claimed.u128() as i128 - claimed.u128() as i128).abs() < 10i128); - }); + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(2u64), + |result| { + let flow_response = result.unwrap(); + let total_rewards = flow_response + .clone() + .unwrap() + .flow + .unwrap() + .flow_asset + .amount; + let claimed = flow_response.clone().unwrap().flow.unwrap().claimed_amount; + let expected_claimed = total_rewards.clone(); + + assert!(total_rewards > claimed); + assert!(expected_claimed >= claimed); + assert!((expected_claimed.u128() as i128 - claimed.u128() as i128).abs() < 10i128); + }, + ); // carol should be able to withdraw, as many epochs has passed let carol_incentive_asset_funds = RefCell::new(Uint128::zero()); @@ -4234,13 +4383,14 @@ fn open_expand_position_with_optional_receiver() { .open_incentive_flow( alice.clone(), incentive_addr.clone().into_inner(), - None, //epoch 11 - current_epoch.clone().into_inner() + 10, // epoch 21 - Curve::Linear, + None, //epoch 11 + Some(current_epoch.clone().into_inner() + 10), // epoch 21 + Some(Curve::Linear), Asset { info: flow_asset_1.clone(), amount: Uint128::new(1_000_000_000u128), }, + None, &vec![ coin(1_000_000_000u128, "ampWHALE"), coin(1_000u128, "uwhale"), @@ -4249,7 +4399,7 @@ fn open_expand_position_with_optional_receiver() { result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); assert_eq!(flows.len(), 1); assert_eq!( @@ -4376,19 +4526,20 @@ fn close_position_if_empty_rewards() { .open_incentive_flow( alice.clone(), incentive_addr.clone().into_inner(), - None, //epoch 11 - current_epoch.clone().into_inner() + 10, // epoch 21 - Curve::Linear, + None, //epoch 11 + Some(current_epoch.clone().into_inner() + 10), // epoch 21 + Some(Curve::Linear), Asset { info: flow_asset_1.clone(), amount: Uint128::new(1_000u128), }, + None, &vec![coin(1_000u128, "ampWHALE"), coin(1_000u128, "uwhale")], |result| { result.unwrap(); }, ) - .query_flows(incentive_addr.clone().into_inner(), |result| { + .query_flows(incentive_addr.clone().into_inner(), None, None, |result| { let flows = result.unwrap(); println!("flows created:: {:?}", flows); assert_eq!(flows.len(), 1); @@ -4618,13 +4769,17 @@ fn close_position_if_empty_rewards() { ); }); - suite.query_flow(incentive_addr.clone().into_inner(), 1u64, |result| { - let flow_response = result.unwrap(); - assert_eq!( - flow_response.unwrap().flow.unwrap().claimed_amount, - Uint128::new(351u128) - ); - }); + suite.query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap().claimed_amount, + Uint128::new(351u128) + ); + }, + ); suite .query_rewards(incentive_addr.clone().into_inner(), bob.clone(), |result| { @@ -4679,3 +4834,1778 @@ fn close_position_if_empty_rewards() { }, ); } + +#[test] +fn open_expand_flow_with_native_token() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "usdc".to_string()), + ]); + let alice = suite.creator(); + let carol = suite.senders[2].clone(); + + suite.instantiate_default_native_fee().create_lp_tokens(); + + let lp_address_1 = AssetInfo::Token { + contract_addr: suite.cw20_tokens.first().unwrap().to_string(), + }; + + let incentive_addr = RefCell::new(Addr::unchecked("")); + + suite + .create_incentive(alice.clone(), lp_address_1.clone(), |result| { + result.unwrap(); + }) + .query_incentive(lp_address_1.clone(), |result| { + let incentive = result.unwrap(); + assert!(incentive.is_some()); + *incentive_addr.borrow_mut() = incentive.unwrap(); + }); + + let current_epoch = RefCell::new(0u64); + suite + .create_epochs_on_fee_distributor(9u64, vec![]) + .query_current_epoch(|result| { + *current_epoch.borrow_mut() = result.unwrap().epoch.id.u64(); + }); + + // open incentive flow + suite + .open_incentive_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + None, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".clone().to_string(), + }, + amount: Uint128::new(2_000u128), + }, + None, + &vec![coin(2_000u128, "uwhale".to_string())], + |result| { + // this should succeed as we sent enough funds to cover for fee + MIN_FLOW_AMOUNT + result.unwrap(); + }, + ) + .query_flow(incentive_addr.clone().into_inner(), FlowIdentifier::Id(1u64), |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string() + }, + amount: Uint128::new(1_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 10u64, + end_epoch: 19u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }) + .expand_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(5u64), // invalid flow id + Some(19u64), + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + coins(1_000u128, "uwhale".to_string()), + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::NonExistentFlow { invalid_identifier } => assert_eq!(invalid_identifier, FlowIdentifier::Id(5u64)), + _ => panic!("Wrong error type, should return ContractError::NonExistentFlow"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(18u64), //invalid epoch + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + coins(1_000u128, "uwhale".to_string()), + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidEndEpoch {} => {} + _ => panic!("Wrong error type, should return ContractError::InvalidEndEpoch"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(19u64), //valid epoch + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + vec![], //invalid funds + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PaymentError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(19u64), //valid epoch + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + coins(500, "uwhale"), //invalid funds + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::MissingPositionDepositNative { .. } => {} + _ => panic!("Wrong error type, should return ContractError::MissingPositionDepositNative"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(19u64), //valid epoch + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + coins(1_000, "usdc"), //invalid funds + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::FlowAssetNotSent { .. } => {} + _ => panic!("Wrong error type, should return ContractError::FlowAssetNotSent"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(19u64), //valid epoch + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + vec![coin(1_000, "uwhale"), coin(1_000, "usdc")], //invalid funds + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::PaymentError { .. } => {} + _ => panic!("Wrong error type, should return ContractError::PaymentError"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(19u64), //valid epoch + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + vec![coin(1_000, "uwhale")], //valid funds + |result| { + result.unwrap(); + }, ) + .query_flow(incentive_addr.clone().into_inner(), FlowIdentifier::Id(1u64), |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string() + }, + amount: Uint128::new(1_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 10u64, + end_epoch: 19u64, + emitted_tokens: Default::default(), + asset_history: BTreeMap::from_iter(vec![(11, (Uint128::new(2_000u128), 19u64))]), + }) + ); + }) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(20u64), //valid epoch + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(0u128), + }, + vec![coin(0, "uwhale")], //valid funds + |result| { + result.unwrap_err(); //can't send 0 coins + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(30u64), //valid epoch + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + vec![coin(1_000u128, "uwhale")], //valid funds + |result| { + result.unwrap(); + }, + ) + .query_flow(incentive_addr.clone().into_inner(), FlowIdentifier::Id(1u64), |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string() + }, + amount: Uint128::new(1_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 10u64, + end_epoch: 19u64, + emitted_tokens: Default::default(), + asset_history: BTreeMap::from_iter(vec![(11, (Uint128::new(3_000u128), 30u64))]), + }) + ); + }); +} + +#[test] +fn open_expand_flow_with_cw20_token() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "usdc".to_string()), + ]); + let alice = suite.creator(); + let carol = suite.senders[2].clone(); + + suite.instantiate_default_native_fee().create_lp_tokens(); + + let incentive_asset = AssetInfo::Token { + contract_addr: suite.cw20_tokens.first().unwrap().to_string(), + }; + + let incentive_addr = RefCell::new(Addr::unchecked("")); + + let flow_asset = AssetInfo::Token { + contract_addr: suite.cw20_tokens.last().unwrap().to_string(), + }; + + let flow_asset_addr = suite.cw20_tokens.last().unwrap().clone(); + + suite + .create_incentive(alice.clone(), incentive_asset.clone(), |result| { + result.unwrap(); + }) + .query_incentive(incentive_asset.clone(), |result| { + let incentive = result.unwrap(); + assert!(incentive.is_some()); + *incentive_addr.borrow_mut() = incentive.unwrap(); + }); + + let current_epoch = RefCell::new(0u64); + suite + .create_epochs_on_fee_distributor(9u64, vec![]) + .query_current_epoch(|result| { + *current_epoch.borrow_mut() = result.unwrap().epoch.id.u64(); + }); + + // open incentive flow + suite + .increase_allowance( + carol.clone(), + flow_asset_addr.clone(), + Uint128::new(2_000u128), // enough allowance + incentive_addr.clone().into_inner(), + ) + .open_incentive_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + None, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), + Asset { + info: flow_asset.clone(), + amount: Uint128::new(2_000u128), + }, + None, + &vec![coin(1_000u128, "uwhale".to_string())], + |result| { + // this should succeed as we sent enough funds to cover for fee + MIN_FLOW_AMOUNT + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: flow_asset.clone(), + amount: Uint128::new(2_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 10u64, + end_epoch: 19u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }, + ) + .expand_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(5u64), // invalid flow id + Some(19u64), + Asset { + info: flow_asset.clone(), + amount: Uint128::new(1_000u128), + }, + coins(1_000u128, "uwhale".to_string()), + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::NonExistentFlow { invalid_identifier } => { + assert_eq!(invalid_identifier, FlowIdentifier::Id(5u64)) + } + _ => panic!("Wrong error type, should return ContractError::NonExistentFlow"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(19u64), //valid epoch + Asset { + info: flow_asset.clone(), + amount: Uint128::new(1_000u128), + }, + vec![], //invalid funds, no allowance + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::FlowAssetNotSent { .. } => {} + _ => panic!("Wrong error type, should return ContractError::FlowAssetNotSent"), + } + }, + ) + .increase_allowance( + carol.clone(), + flow_asset_addr.clone(), + Uint128::new(500u128), // not enough allowance + incentive_addr.clone().into_inner(), + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(19u64), //valid epoch + Asset { + info: flow_asset.clone(), + amount: Uint128::new(1_000u128), + }, + vec![], //invalid funds, not enough allowance + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::FlowAssetNotSent { .. } => {} + _ => panic!("Wrong error type, should return ContractError::FlowAssetNotSent"), + } + }, + ) + .increase_allowance( + carol.clone(), + flow_asset_addr.clone(), + Uint128::new(1_000u128), // enough allowance + incentive_addr.clone().into_inner(), + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(18u64), //invalid epoch + Asset { + info: flow_asset.clone(), + amount: Uint128::new(1_000u128), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + match err { + ContractError::InvalidEndEpoch {} => {} + _ => panic!("Wrong error type, should return ContractError::InvalidEndEpoch"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(19u64), //valid epoch + Asset { + info: flow_asset.clone(), + amount: Uint128::new(1_000u128), + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: flow_asset.clone(), + amount: Uint128::new(2_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 10u64, + end_epoch: 19u64, + emitted_tokens: Default::default(), + asset_history: BTreeMap::from_iter(vec![( + 11, + (Uint128::new(3_000u128), 19u64) + )]), + }) + ); + }, + ) + .increase_allowance( + carol.clone(), + flow_asset_addr.clone(), + Uint128::new(1_000u128), // enough allowance + incentive_addr.clone().into_inner(), + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), // valid flow id + Some(30u64), //valid epoch + Asset { + info: flow_asset.clone(), + amount: Uint128::new(1_000u128), + }, + vec![], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: flow_asset.clone(), + amount: Uint128::new(2_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 10u64, + end_epoch: 19u64, + emitted_tokens: Default::default(), + asset_history: BTreeMap::from_iter(vec![( + 11, + (Uint128::new(4_000u128), 30u64) + )]), + }) + ); + }, + ); +} + +#[test] +fn fail_expand_ended_flow() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(1_000_000_000u128, "uwhale".to_string()), + coin(1_000_000_000u128, "usdc".to_string()), + ]); + let alice = suite.creator(); + let carol = suite.senders[2].clone(); + + suite.instantiate_default_native_fee().create_lp_tokens(); + + let incentive_asset = AssetInfo::Token { + contract_addr: suite.cw20_tokens.first().unwrap().to_string(), + }; + + let incentive_addr = RefCell::new(Addr::unchecked("")); + + let flow_asset = AssetInfo::Token { + contract_addr: suite.cw20_tokens.last().unwrap().to_string(), + }; + + let flow_asset_addr = suite.cw20_tokens.last().unwrap().clone(); + + suite + .create_incentive(alice.clone(), incentive_asset.clone(), |result| { + result.unwrap(); + }) + .query_incentive(incentive_asset.clone(), |result| { + let incentive = result.unwrap(); + assert!(incentive.is_some()); + *incentive_addr.borrow_mut() = incentive.unwrap(); + }); + + let current_epoch = RefCell::new(0u64); + suite + .create_epochs_on_fee_distributor(9u64, vec![]) + .query_current_epoch(|result| { + *current_epoch.borrow_mut() = result.unwrap().epoch.id.u64(); + }); + + // open incentive flow + suite + .increase_allowance( + carol.clone(), + flow_asset_addr.clone(), + Uint128::new(2_000u128), // enough allowance + incentive_addr.clone().into_inner(), + ) + .open_incentive_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + None, + Some(current_epoch.clone().into_inner() + 9), + Some(Curve::Linear), + Asset { + info: flow_asset.clone(), + amount: Uint128::new(2_000u128), + }, + None, + &vec![coin(1_000u128, "uwhale".to_string())], + |result| { + // this should succeed as we sent enough funds to cover for fee + MIN_FLOW_AMOUNT + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow, + Some(Flow { + flow_id: 1, + flow_label: None, + flow_creator: carol.clone(), + flow_asset: Asset { + info: flow_asset.clone(), + amount: Uint128::new(2_000u128), + }, + claimed_amount: Uint128::zero(), + curve: Curve::Linear, + start_epoch: 10u64, + end_epoch: 19u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }) + ); + }, + ) + .create_epochs_on_fee_distributor(20u64, vec![]) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + Some(50u64), + Asset { + info: flow_asset.clone(), + amount: Uint128::new(1_000u128), + }, + vec![], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::FlowAlreadyEnded {} => {} + _ => panic!("Wrong error type, should return ContractError::FlowAlreadyEnded"), + } + }, + ); +} + +#[test] +fn open_expand_flow_with_default_values() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(5_000_000_000u128, "uwhale".to_string()), + coin(50_000_000_000u128, "usdc".to_string()), + coin(5_000_000_000u128, "ampWHALE".to_string()), + coin(5_000_000_000u128, "bWHALE".to_string()), + ]); + let alice = suite.creator(); + let carol = suite.senders[2].clone(); + + suite.instantiate_default_native_fee().create_lp_tokens(); + + let incentive_asset = AssetInfo::NativeToken { + denom: "ampWHALE".to_string(), + }; + + let incentive_addr = RefCell::new(Addr::unchecked("")); + + suite + .create_incentive(alice.clone(), incentive_asset.clone(), |result| { + result.unwrap(); + }) + .query_incentive(incentive_asset.clone(), |result| { + let incentive = result.unwrap(); + assert!(incentive.is_some()); + *incentive_addr.borrow_mut() = incentive.unwrap(); + }) + .query_incentive_config(incentive_addr.clone().into_inner(), |result| { + let config = result.unwrap(); + assert_eq!(config.lp_asset, incentive_asset.clone()); + }); + + let open_position = incentive::OpenPosition { + amount: Uint128::new(1_000u128), + unbonding_duration: 86400u64, + }; + suite + .open_incentive_position( + carol.clone(), + incentive_addr.clone().into_inner(), + open_position.amount, + open_position.unbonding_duration, + None, + vec![coin(1_000u128, "ampWHALE".to_string())], + |result| { + result.unwrap(); + }, + ) + .query_positions( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + assert_eq!( + result.unwrap().positions.first().unwrap(), + &incentive::QueryPosition::OpenPosition { + amount: Uint128::new(1_000u128), + unbonding_duration: open_position.unbonding_duration, + weight: Uint128::new(1_000u128), + } + ); + }, + ); + + let time = Timestamp::from_seconds(1684766796u64); + suite.set_time(time); + + let current_epoch = RefCell::new(0u64); + suite + .create_epochs_on_fee_distributor(10, vec![incentive_addr.clone().into_inner()]) + .query_current_epoch(|result| { + *current_epoch.borrow_mut() = result.unwrap().epoch.id.u64(); + }); + + println!("CURRENT_EPOCH -> {:?}", current_epoch); + + suite + .open_incentive_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + None, + None, + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + Some("alias".to_string()), + &vec![coin(1_000_000_000u128, "usdc"), coin(1_000u128, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 25u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + } + ); + }, + ) + .expand_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + vec![coin(1_000_000_000u128, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::FlowAssetNotSent {} => {} + _ => panic!("Wrong error type, should return ContractError::FlowAssetNotSent"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + vec![coin(1_000_000_000u128, "usdc")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Label("alias".to_string()), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 25u64, + emitted_tokens: Default::default(), + asset_history: vec![(12, (Uint128::new(2_000_000_000u128), 25u64))] + .into_iter() + .collect(), + } + ); + }, + ) + .expand_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + vec![coin(1_000_000_000u128, "usdc")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 25u64, + emitted_tokens: Default::default(), + asset_history: vec![(12, (Uint128::new(3_000_000_000u128), 25u64))] + .into_iter() + .collect(), + } + ); + }, + ) + .create_epochs_on_fee_distributor(9, vec![incentive_addr.clone().into_inner()]) + .expand_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Label("alias".to_string()), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + vec![coin(1_000_000_000u128, "usdc")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 25u64, + emitted_tokens: Default::default(), + asset_history: vec![ + (12, (Uint128::new(3_000_000_000u128), 25u64)), + (21, (Uint128::new(4_000_000_000u128), 25u64)), + ] + .into_iter() + .collect(), + } + ); + }, + ) + .create_epochs_on_fee_distributor(1, vec![incentive_addr.clone().into_inner()]) + .expand_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Label("alias".to_string()), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + vec![coin(1_000_000_000u128, "usdc")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 25u64, //expanded as it goes beyond the epoch expansion BUFFER. + emitted_tokens: Default::default(), + asset_history: vec![ + (12, (Uint128::new(3_000_000_000u128), 25u64)), + (21, (Uint128::new(4_000_000_000u128), 25u64)), + (22, (Uint128::new(5_000_000_000u128), 39u64)), + ] + .into_iter() + .collect(), + } + ); + }, + ); +} + +#[test] +fn open_expand_flow_verify_rewards() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(5_000_000_000u128, "uwhale".to_string()), + coin(50_000_000_000u128, "usdc".to_string()), + coin(5_000_000_000u128, "ampWHALE".to_string()), + coin(5_000_000_000u128, "bWHALE".to_string()), + ]); + let alice = suite.creator(); + let carol = suite.senders[2].clone(); + + suite.instantiate_default_native_fee().create_lp_tokens(); + + let incentive_asset = AssetInfo::NativeToken { + denom: "ampWHALE".to_string(), + }; + + let incentive_addr = RefCell::new(Addr::unchecked("")); + + suite + .create_incentive(alice.clone(), incentive_asset.clone(), |result| { + result.unwrap(); + }) + .query_incentive(incentive_asset.clone(), |result| { + let incentive = result.unwrap(); + assert!(incentive.is_some()); + *incentive_addr.borrow_mut() = incentive.unwrap(); + }) + .query_incentive_config(incentive_addr.clone().into_inner(), |result| { + let config = result.unwrap(); + assert_eq!(config.lp_asset, incentive_asset.clone()); + }); + + let open_position = incentive::OpenPosition { + amount: Uint128::new(1_000u128), + unbonding_duration: 86400u64, + }; + suite + .open_incentive_position( + carol.clone(), + incentive_addr.clone().into_inner(), + open_position.amount, + open_position.unbonding_duration, + None, + vec![coin(1_000u128, "ampWHALE".to_string())], + |result| { + result.unwrap(); + }, + ) + .query_positions( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + assert_eq!( + result.unwrap().positions.first().unwrap(), + &incentive::QueryPosition::OpenPosition { + amount: Uint128::new(1_000u128), + unbonding_duration: open_position.unbonding_duration, + weight: Uint128::new(1_000u128), + } + ); + }, + ); + + suite + .open_incentive_position( + alice.clone(), + incentive_addr.clone().into_inner(), + open_position.amount, + open_position.unbonding_duration, + None, + vec![coin(1_000u128, "ampWHALE".to_string())], + |result| { + result.unwrap(); + }, + ) + .query_positions( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + assert_eq!( + result.unwrap().positions.first().unwrap(), + &incentive::QueryPosition::OpenPosition { + amount: Uint128::new(1_000u128), + unbonding_duration: open_position.unbonding_duration, + weight: Uint128::new(1_000u128), + } + ); + }, + ); + + let time = Timestamp::from_seconds(1684766796u64); + suite.set_time(time); + + let current_epoch = RefCell::new(0u64); + suite + .create_epochs_on_fee_distributor(10, vec![incentive_addr.clone().into_inner()]) + .query_current_epoch(|result| { + *current_epoch.borrow_mut() = result.unwrap().epoch.id.u64(); + }); + + let carol_usdc_funds = RefCell::new(Uint128::zero()); + let alice_usdc_funds = RefCell::new(Uint128::zero()); + println!("CURRENT_EPOCH -> {:?}", current_epoch); + + suite + .open_incentive_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + None, + Some(21u64), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(10_000u128), + }, + Some("alias".to_string()), + &vec![coin(10_000u128, "usdc"), coin(1_000u128, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(10_000u128), + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 21u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + } + ); + }, + ) + .create_epochs_on_fee_distributor(6, vec![incentive_addr.clone().into_inner()]) + .query_funds( + alice.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + *alice_usdc_funds.borrow_mut() = result; + }, + ) + .claim( + incentive_addr.clone().into_inner(), + alice.clone(), + |result| { + result.unwrap(); + }, + ) + .query_funds( + alice.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + assert_eq!( + result, + alice_usdc_funds + .clone() + .into_inner() + .checked_add(Uint128::new(3_500u128)) + .unwrap(), + ); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap().claimed_amount, + Uint128::new(3_500u128) + ); + }, + ) + .expand_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Label("alias".to_string()), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "uwhale".to_string(), + }, + amount: Uint128::new(1_000_000_000u128), + }, + vec![coin(1_000_000_000u128, "uwhale")], + |result| { + let err = result.unwrap_err().downcast::().unwrap(); + + match err { + ContractError::FlowAssetNotSent {} => {} + _ => panic!("Wrong error type, should return ContractError::FlowAssetNotSent"), + } + }, + ) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(31_000u128), + }, + vec![coin(31_000u128, "usdc")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(10_000u128), + }, + claimed_amount: Uint128::new(3_500u128), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 21u64, + emitted_tokens: HashMap::from_iter(vec![ + (11, Uint128::new(1_000u128)), + (12, Uint128::new(2_000u128)), + (13, Uint128::new(3_000u128)), + (14, Uint128::new(4_000u128)), + (15, Uint128::new(5_000u128)), + (16, Uint128::new(6_000u128)), + (17, Uint128::new(7_000u128)), + ]), + asset_history: vec![(18, (Uint128::new(41_000u128), 35u64))] + .into_iter() + .collect(), + } + ); + }, + ) + .create_epochs_on_fee_distributor(10, vec![incentive_addr.clone().into_inner()]) + .query_funds( + alice.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + *alice_usdc_funds.borrow_mut() = result; + }, + ) + .claim( + incentive_addr.clone().into_inner(), + alice.clone(), + |result| { + result.unwrap(); + }, + ) + .query_funds( + alice.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + assert_eq!( + result, + alice_usdc_funds + .clone() + .into_inner() + .checked_add(Uint128::new(10_000u128)) + .unwrap(), + ); + }, + ) + .query_rewards( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + println!("carol rewards: {:?}", result); + assert_eq!( + result.unwrap().rewards, + vec![Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(13_500u128), + },] + ); + }, + ) + //claim with carol + .query_funds( + carol.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + *carol_usdc_funds.borrow_mut() = result; + }, + ) + .claim( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + println!("carol claim result: {:?}", result); + result.unwrap(); + }, + ) + .query_funds( + carol.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + println!("carol funds: {:?}", result); + assert_eq!( + result, + carol_usdc_funds + .clone() + .into_inner() + .checked_add(Uint128::new(13_500u128)) + .unwrap(), + ); + }, + ) + .expand_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + Some(40u64), + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(46_000u128), + }, + vec![coin(46_000u128, "usdc")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(10_000u128), + }, + claimed_amount: Uint128::new(27_000u128), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 21u64, + emitted_tokens: HashMap::from_iter(vec![ + (11, Uint128::new(1_000u128)), + (12, Uint128::new(2_000u128)), + (13, Uint128::new(3_000u128)), + (14, Uint128::new(4_000u128)), + (15, Uint128::new(5_000u128)), + (16, Uint128::new(6_000u128)), + (17, Uint128::new(7_000u128)), + (18, Uint128::new(9_000u128)), + (19, Uint128::new(11_000u128)), + (20, Uint128::new(13_000u128)), + (21, Uint128::new(15_000u128)), + (22, Uint128::new(17_000u128)), + (23, Uint128::new(19_000u128)), + (24, Uint128::new(21_000u128)), + (25, Uint128::new(23_000u128)), + (26, Uint128::new(25_000u128)), + (27, Uint128::new(27_000u128)), + ]), + asset_history: vec![ + (18, (Uint128::new(41_000u128), 35u64)), + (28, (Uint128::new(87_000u128), 40u64)), + ] + .into_iter() + .collect(), + } + ); + }, + ) + .create_epochs_on_fee_distributor(13, vec![incentive_addr.clone().into_inner()]) + .query_rewards( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + println!("carol rewards: {:?}", result); + assert_eq!( + result.unwrap().rewards, + vec![Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(30_000u128), + },] + ); + }, + ) + .query_rewards( + incentive_addr.clone().into_inner(), + alice.clone(), + |result| { + println!("carol rewards: {:?}", result); + assert_eq!( + result.unwrap().rewards, + vec![Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(30_000u128), + },] + ); + }, + ) + //claim with carol + .query_funds( + carol.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + *carol_usdc_funds.borrow_mut() = result; + }, + ) + .claim( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + result.unwrap(); + }, + ) + .query_funds( + carol.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + assert_eq!( + result, + carol_usdc_funds + .clone() + .into_inner() + .checked_add(Uint128::new(30_000u128)) + .unwrap(), + ); + }, + ) + //claim with alice + .query_funds( + alice.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + *alice_usdc_funds.borrow_mut() = result; + }, + ) + .claim( + incentive_addr.clone().into_inner(), + alice.clone(), + |result| { + result.unwrap(); + }, + ) + .query_funds( + alice.clone(), + AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + |result| { + assert_eq!( + result, + alice_usdc_funds + .clone() + .into_inner() + .checked_add(Uint128::new(30_000u128)) + .unwrap(), + ); + }, + ); +} + +#[test] +fn open_expand_flow_over_expand_limit() { + let mut suite = TestingSuite::default_with_balances(vec![ + coin(5_000_000_000u128, "uwhale".to_string()), + coin(50_000_000_000u128, "usdc".to_string()), + coin(5_000_000_000u128, "ampWHALE".to_string()), + coin(5_000_000_000u128, "bWHALE".to_string()), + ]); + let alice = suite.creator(); + let carol = suite.senders[2].clone(); + + suite.instantiate_default_native_fee().create_lp_tokens(); + + let incentive_asset = AssetInfo::NativeToken { + denom: "ampWHALE".to_string(), + }; + + let incentive_addr = RefCell::new(Addr::unchecked("")); + let flow_ref = RefCell::new(Flow { + flow_id: 0, + flow_label: None, + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "".to_string(), + }, + amount: Default::default(), + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 0, + end_epoch: 0, + emitted_tokens: Default::default(), + asset_history: Default::default(), + }); + + suite + .create_incentive(alice.clone(), incentive_asset.clone(), |result| { + result.unwrap(); + }) + .query_incentive(incentive_asset.clone(), |result| { + let incentive = result.unwrap(); + assert!(incentive.is_some()); + *incentive_addr.borrow_mut() = incentive.unwrap(); + }) + .query_incentive_config(incentive_addr.clone().into_inner(), |result| { + let config = result.unwrap(); + assert_eq!(config.lp_asset, incentive_asset.clone()); + }); + + let open_position = incentive::OpenPosition { + amount: Uint128::new(1_000u128), + unbonding_duration: 86400u64, + }; + suite + .open_incentive_position( + carol.clone(), + incentive_addr.clone().into_inner(), + open_position.amount, + open_position.unbonding_duration, + None, + vec![coin(1_000u128, "ampWHALE".to_string())], + |result| { + result.unwrap(); + }, + ) + .query_positions( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + assert_eq!( + result.unwrap().positions.first().unwrap(), + &incentive::QueryPosition::OpenPosition { + amount: Uint128::new(1_000u128), + unbonding_duration: open_position.unbonding_duration, + weight: Uint128::new(1_000u128), + } + ); + }, + ); + + let current_epoch = RefCell::new(0u64); + suite + .create_epochs_on_fee_distributor(10, vec![incentive_addr.clone().into_inner()]) + .query_current_epoch(|result| { + *current_epoch.borrow_mut() = result.unwrap().epoch.id.u64(); + }); + + println!("CURRENT_EPOCH -> {:?}", current_epoch); + + suite + .open_incentive_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + None, + Some(21u64), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(10_000u128), + }, + Some("alias".to_string()), + &vec![coin(10_000u128, "usdc"), coin(1_000u128, "uwhale")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + *flow_ref.borrow_mut() = flow.clone(); + assert_eq!( + flow, + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(10_000u128), + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 11u64, + end_epoch: 21u64, + emitted_tokens: Default::default(), + asset_history: Default::default(), + } + ); + }, + ); + + let claimed_rewards = RefCell::new(Uint128::zero()); + + let mut i = 0; + + // expand the flow until it gets reset + while flow_ref.clone().into_inner().start_epoch == 11u64 { + suite + .create_epochs_on_fee_distributor(1, vec![incentive_addr.clone().into_inner()]) + .expand_flow( + carol.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + None, + Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(1_000u128), + }, + vec![coin(1_000u128, "usdc")], + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow = result.unwrap().unwrap().flow.unwrap(); + *flow_ref.borrow_mut() = flow.clone(); + }, + ); + + if i <= 170 { + suite + .claim( + incentive_addr.clone().into_inner(), + carol.clone(), + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + println!("flow_response -> {:?}", flow_response); + *claimed_rewards.borrow_mut() = + flow_response.unwrap().flow.unwrap().claimed_amount; + }, + ); + } + + i += 1; + } + + suite.query_current_epoch(|result| { + *current_epoch.borrow_mut() = result.unwrap().epoch.id.u64(); + }); + + suite + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Id(1u64), + |result| { + let flow_response = result.unwrap(); + assert_eq!( + flow_response.unwrap().flow.unwrap(), + Flow { + flow_id: 1u64, + flow_label: Some("alias".to_string()), + flow_creator: alice.clone(), + flow_asset: Asset { + info: AssetInfo::NativeToken { + denom: "usdc".to_string(), + }, + amount: Uint128::new(12_005u128), // 184k - ~173k claimed + }, + claimed_amount: Default::default(), + curve: Curve::Linear, + start_epoch: 186u64, + end_epoch: 203u64, + emitted_tokens: Default::default(), + asset_history: BTreeMap::from_iter(vec![( + 187, + (Uint128::new(13_005), 203u64) + )]), + } + ); + }, + ) + .close_incentive_flow( + alice.clone(), + incentive_addr.clone().into_inner(), + FlowIdentifier::Label("alias".to_string()), + |result| { + result.unwrap(); + }, + ) + .query_flow( + incentive_addr.clone().into_inner(), + FlowIdentifier::Label("alias".to_string()), + |result| { + let flow_response = result.unwrap(); + assert!(flow_response.is_none()); + }, + ); +} diff --git a/contracts/liquidity_hub/pool-network/incentive/src/tests/mod.rs b/contracts/liquidity_hub/pool-network/incentive/src/tests/mod.rs index dbcda28d..f662e117 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/tests/mod.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/tests/mod.rs @@ -1,3 +1,4 @@ +mod helpers; #[allow(non_snake_case)] #[allow(dead_code)] mod integration; diff --git a/contracts/liquidity_hub/pool-network/incentive/src/tests/suite.rs b/contracts/liquidity_hub/pool-network/incentive/src/tests/suite.rs index 4f973179..77ef069a 100644 --- a/contracts/liquidity_hub/pool-network/incentive/src/tests/suite.rs +++ b/contracts/liquidity_hub/pool-network/incentive/src/tests/suite.rs @@ -5,8 +5,8 @@ use cw_multi_test::{App, AppBuilder, AppResponse, BankKeeper, Executor}; use white_whale::fee_distributor::EpochResponse; use white_whale::pool_network::asset::{Asset, AssetInfo}; use white_whale::pool_network::incentive::{ - Curve, Flow, FlowResponse, GlobalWeightResponse, PositionsResponse, RewardsResponse, - RewardsShareResponse, + Curve, Flow, FlowIdentifier, FlowResponse, GlobalWeightResponse, PositionsResponse, + RewardsResponse, RewardsShareResponse, }; use white_whale::pool_network::incentive_factory::{ IncentiveResponse, IncentivesResponse, InstantiateMsg, @@ -362,9 +362,10 @@ impl TestingSuite { sender: Addr, incentive_addr: Addr, start_epoch: Option, - end_epoch: u64, - curve: Curve, + end_epoch: Option, + curve: Option, flow_asset: Asset, + flow_label: Option, funds: &Vec, result: impl Fn(Result), ) -> &mut Self { @@ -373,6 +374,7 @@ impl TestingSuite { end_epoch, curve, flow_asset, + flow_label, }; result( @@ -387,10 +389,10 @@ impl TestingSuite { &mut self, sender: Addr, incentive_addr: Addr, - flow_id: u64, + flow_identifier: FlowIdentifier, result: impl Fn(Result), ) -> &mut Self { - let msg = white_whale::pool_network::incentive::ExecuteMsg::CloseFlow { flow_id }; + let msg = white_whale::pool_network::incentive::ExecuteMsg::CloseFlow { flow_identifier }; result( self.app @@ -562,6 +564,30 @@ impl TestingSuite { self } + + pub(crate) fn expand_flow( + &mut self, + sender: Addr, + incentive_addr: Addr, + flow_identifier: FlowIdentifier, + end_epoch: Option, + flow_asset: Asset, + funds: Vec, + result: impl Fn(Result), + ) -> &mut Self { + let msg = white_whale::pool_network::incentive::ExecuteMsg::ExpandFlow { + flow_identifier, + end_epoch, + flow_asset, + }; + + result( + self.app + .execute_contract(sender, incentive_addr.clone(), &msg, &funds), + ); + + self + } } /// queries @@ -655,12 +681,16 @@ impl TestingSuite { pub(crate) fn query_flow( &mut self, incentive_addr: Addr, - flow_id: u64, + flow_identifier: FlowIdentifier, result: impl Fn(StdResult>), ) -> &mut Self { let flow_response: StdResult> = self.app.wrap().query_wasm_smart( incentive_addr, - &white_whale::pool_network::incentive::QueryMsg::Flow { flow_id }, + &white_whale::pool_network::incentive::QueryMsg::Flow { + flow_identifier, + start_epoch: None, + end_epoch: None, + }, ); result(flow_response); @@ -671,11 +701,16 @@ impl TestingSuite { pub(crate) fn query_flows( &mut self, incentive_addr: Addr, + start_epoch: Option, + end_epoch: Option, result: impl Fn(StdResult>), ) -> &mut Self { let flows_response: StdResult> = self.app.wrap().query_wasm_smart( incentive_addr, - &white_whale::pool_network::incentive::QueryMsg::Flows {}, + &white_whale::pool_network::incentive::QueryMsg::Flows { + start_epoch, + end_epoch, + }, ); result(flows_response); diff --git a/packages/white-whale/src/pool_network/incentive.rs b/packages/white-whale/src/pool_network/incentive.rs index bfee1e73..532a318f 100644 --- a/packages/white-whale/src/pool_network/incentive.rs +++ b/packages/white-whale/src/pool_network/incentive.rs @@ -1,7 +1,8 @@ +use std::collections::{BTreeMap, HashMap}; +use std::fmt; + use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Decimal256, Uint128}; -use std::collections::HashMap; -use std::fmt; use crate::pool_network::asset::{Asset, AssetInfo}; @@ -19,23 +20,25 @@ pub enum ExecuteMsg { TakeGlobalWeightSnapshot {}, /// Opens a new liquidity flow OpenFlow { - /// The epoch at which the flow should start. - /// - /// If unspecified, the flow will start at the current epoch. + /// The epoch at which the flow will start. If unspecified, the flow will start at the + /// current epoch. start_epoch: Option, - /// The epoch at which the flow should end. - end_epoch: u64, - /// The type of distribution curve. - curve: Curve, + /// The epoch at which the flow should end. If unspecified, the flow will default to end at + /// 14 epochs from the current one. + end_epoch: Option, + /// The type of distribution curve. If unspecified, the distribution will be linear. + curve: Option, /// The asset to be distributed in this flow. flow_asset: Asset, + /// If set, the label will be used to identify the flow, in addition to the flow_id. + flow_label: Option, }, /// Closes an existing liquidity flow. /// /// Sender of the message must either be the contract admin or the creator of the flow. CloseFlow { - /// The id of the flow to close. - flow_id: u64, + /// The identifier of the flow to close. + flow_identifier: FlowIdentifier, }, /// Creates a new position to earn flow rewards. OpenPosition { @@ -72,6 +75,15 @@ pub enum ExecuteMsg { Withdraw {}, /// Claims the flow rewards. Claim {}, + /// Expands an existing flow. + ExpandFlow { + /// The identifier of the flow to expand, whether an id or a label. + flow_identifier: FlowIdentifier, + /// The epoch at which the flow should end. If not set, the flow will be expanded a default value of 14 epochs. + end_epoch: Option, + /// The asset to expand this flow with. + flow_asset: Asset, + }, } #[cw_serde] @@ -82,6 +94,8 @@ pub struct MigrateMsg {} pub struct Flow { /// A unique identifier of the flow. pub flow_id: u64, + /// An alternative flow label. + pub flow_label: Option, /// The account which opened the flow and can manage it. pub flow_creator: Addr, /// The asset the flow was created to distribute. @@ -89,13 +103,17 @@ pub struct Flow { /// The amount of the `flow_asset` that has been claimed so far. pub claimed_amount: Uint128, /// The type of curve the flow has. - pub curve: Curve, //todo not doing anything for now + pub curve: Curve, + //todo not doing anything for now /// The epoch at which the flow starts. pub start_epoch: u64, /// The epoch at which the flow ends. pub end_epoch: u64, /// emitted tokens pub emitted_tokens: HashMap, + /// A map containing the amount of tokens it was expanded to at a given epoch. This is used + /// to calculate the right amount of tokens to distribute at a given epoch when a flow is expanded. + pub asset_history: BTreeMap, } /// Represents a position that accumulates flow rewards. @@ -136,15 +154,28 @@ pub enum QueryMsg { /// Retrieves the current contract configuration. #[returns(ConfigResponse)] Config {}, - /// Retrieves a specific flow. + /// Retrieves a specific flow. If start_epoch and end_epoch are set, the asset_history and + /// emitted_tokens will be filtered to only include epochs within the range. The maximum gap between + /// the start_epoch and end_epoch is 100 epochs. #[returns(FlowResponse)] Flow { /// The id of the flow to find. - flow_id: u64, + flow_identifier: FlowIdentifier, + /// If set, filters the asset_history and emitted_tokens to only include epochs from start_epoch. + start_epoch: Option, + /// If set, filters the asset_history and emitted_tokens to only include epochs until end_epoch. + end_epoch: Option, }, - /// Retrieves the current flows. + /// Retrieves the current flows. If start_epoch and end_epoch are set, the asset_history and + /// emitted_tokens will be filtered to only include epochs within the range. The maximum gap between + /// the start_epoch and end_epoch is 100 epochs. #[returns(FlowsResponse)] - Flows {}, + Flows { + /// If set, filters the asset_history and emitted_tokens to only include epochs from start_epoch. + start_epoch: Option, + /// If set, filters the asset_history and emitted_tokens to only include epochs until end_epoch. + end_epoch: Option, + }, /// Retrieves the positions for an address. #[returns(PositionsResponse)] Positions { @@ -273,3 +304,18 @@ pub struct RewardsShareResponse { pub share: Decimal256, pub epoch_id: u64, } + +#[cw_serde] +pub enum FlowIdentifier { + Id(u64), + Label(String), +} + +impl fmt::Display for FlowIdentifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FlowIdentifier::Id(flow_id) => write!(f, "flow_id: {}", flow_id), + FlowIdentifier::Label(flow_label) => write!(f, "flow_label: {}", flow_label), + } + } +}